└── Bad_Python_thread.ipynb /Bad_Python_thread.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Этот текст о том, сколько всего в питоне нелогичного и непоследовательного. Целью текста не является показать, что питон ужасен, хотя кого я обманываю. Скорее цель поделиться накипевшим, попытаться в некоторых случаях понять и объяснить причины, почему сделано так, а не иначе. Возможно текст будет полезен тем, кто начинает изучать питон, потому что скучные мануалы запоминаются хуже, чем чужие боль и страдания." 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Начнём с того, как логично определены названия и обозначения структур данных и удачно выбраны названия методов (здесь и далее — на самом деле нет!). Те, кто немного знаком со структурами данных, знают, что list — это стандартное название структуры данных «связный список». \n", 15 | "\n", 16 | "Именно поэтому в питоне имя `list` ности другая структура данных — динамический массив. Есть, конечно ещё абстрактный тип данных, который называется List, и под который попадает и (динамический) массив, но знаете ли вы хоть один язык кроме Python, где list используется в смысле динамического массива? " 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "Стандартная библиотека C++ прекрасна (на самом деле да, но это не точно) тем, что в коде можно легко менять используемую структуру данных, в зависимости от текущих нужд. Например, в графе, описанном через список смежности, последний можно реализовать как динамическим массивом, так и связным списком; разные структуры данных нужны под разные задачи, поэтому их легко можно поменять, поменяв структуру данных как параметр, заменив просто её имя (увы, не всегда, но часто). Для этого важно, чтобы стандартные методы работы со структурами данных (добавление, удаление элементов, итерация по значениям) описывались (и назывались) одинаково. К тому же одинаковые названия проще запомнить.\n", 24 | "\n", 25 | "Именно поэтому в питоне выбрали разные названия для одинаковых методов в разных структурах данных." 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "За годы в математике и программировании были выработанны стандартные обозначения, которые в том числе отражены в питоне. Например, `[1, 2, 3]` — это массив (`list`), `{1,2,3}` — это множество (`set`), `(1, 2, 3)` — это конечная последовательность (набор, `tuple`). Именно поэтому в питоне `[]` — пустой массив, `()` — пустой набор, а `{}` — это пустой словарь (`dictionary`, не `set`!), а не пустое множество; пустое множество — это `set()`. При этом `(1)` — это просто число 1 в скобках, а не набор из одного элемента, а если хочется задать набор из одного элемента, то нужно писать `(1,)`. Можно ещё задать наборы без скобок, так `1,` легальное объявление набора из одного элемента; удобный подход, который приводит к счастливой отладки, если вы где-то в коде нечаянно поставили лишнюю запятую." 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 1, 38 | "metadata": {}, 39 | "outputs": [ 40 | { 41 | "name": "stdout", 42 | "output_type": "stream", 43 | "text": [ 44 | "\n", 45 | "This is a list: [1, 3]\n", 46 | "This is a set: {1, 3}\n", 47 | "type([1, 3]): type({1, 3}): \n", 48 | "\n", 49 | "This is an empty list: [] type([]): \n", 50 | "What is this: {} ?\n", 51 | "type({}): \n", 52 | "Why? Because this is a dict: {'a': 1, 'b': 2} type({'a': 1, 'b': 2}): \n", 53 | "This is an empty set: set()\n", 54 | "And this is another empty list: list() == []\n" 55 | ] 56 | } 57 | ], 58 | "source": [ 59 | "a = list()\n", 60 | "s = set()\n", 61 | "a.append(1)\n", 62 | "a.extend([2, 3])\n", 63 | "a.remove(2)\n", 64 | "#Official tutorial https://docs.python.org/3/tutorial/datastructures.html\n", 65 | "#dedcribes list's methods, but do not describe set metods.\n", 66 | "#Very convenient, so see https://docs.python.org/3/library/stdtypes.html#set\n", 67 | "s.add(1)\n", 68 | "s.update({2, 3})\n", 69 | "s.remove(2)\n", 70 | "#Different method names for different data structures: very convenient!\n", 71 | "s.discard(5)\n", 72 | "#No analog to discard method for list\n", 73 | "###########\n", 74 | "print(\"\\nThis is a list:\", a)\n", 75 | "print(\"This is a set:\", s)\n", 76 | "print(f\"type({a}):\", type(a), f\"type({s}):\", type(s))\n", 77 | "print(\"\\nThis is an empty list:\", [], \"type([]):\", type([]))\n", 78 | "WTF = {}\n", 79 | "print(f\"What is this:\", WTF, \"?\")\n", 80 | "print(\"type({}):\", type({}))\n", 81 | "d = {\"a\": 1, \"b\": 2}\n", 82 | "print(\"Why? Because this is a dict:\", d, f\"type({d}):\", type(d))\n", 83 | "print(\"This is an empty set:\", set())\n", 84 | "print(\"And this is another empty list: list() ==\", list())\n" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 2, 90 | "metadata": {}, 91 | "outputs": [ 92 | { 93 | "name": "stdout", 94 | "output_type": "stream", 95 | "text": [ 96 | " \n" 97 | ] 98 | } 99 | ], 100 | "source": [ 101 | "a = ()\n", 102 | "b = (1)\n", 103 | "c = (1,)\n", 104 | "d = 1,\n", 105 | "print(type(a), type(b), type(c), type(d))" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "Теперь давайте поговорим о последвательности в синтаксисе. Языки программирования стали избавляться от `;` после каждого оператора, потому что все стали писать по одному оператору в строке, а если их больше, то тогда можно и исользовать `;`. В питоне решили отказаться от скобок `begin ... end` в блоках или их аналога `{...}` в си-подобном синтаксисе, потому что все и так пишут блоки с отступами — давайте просто требовать отступы. Очень последовательно требовать обязательно ставить `:` в `for _condition_ :`, `if _condition_ :`, `else:`, когда с новой строчки обязательно начинается блок с отступом. При этом правила хорошего тона всё равно предписывают оставлять пустою строку после окончания блока, но писать на ней `end` для того же улучшения читаемости западло." 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": 3, 118 | "metadata": {}, 119 | "outputs": [ 120 | { 121 | "name": "stdout", 122 | "output_type": "stream", 123 | "text": [ 124 | "0\n", 125 | "1\n", 126 | "2\n", 127 | "3\n", 128 | "4\n" 129 | ] 130 | } 131 | ], 132 | "source": [ 133 | "for i in range(5):\n", 134 | " print(i)\n", 135 | " \n", 136 | "#for i in range(5)\n", 137 | "# print(\"You forgot ':' and will be punished for it!\")" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "Теперь о вечном. Нерадивые европейцы принесли много проблем, когда позаимствовали у арабов цифры. Арабы читают слева направо, поэтому в числе 123 их глаза идут от младших разрядов к старшим и им не нужно знать количество цифр в числе, чтобы их прочесть. Эта беда проявилась при записи композиции функций: `f(g(h(x)))` — сначала применяется h, потом g, потом f. Читать это неудобно, потому что нужно читать справа налево и если у функций несколько аргументов, то нужно ещё смотреть к какой скобке они относятся.\n", 145 | "\n", 146 | "Эта проблема записи была в общем-то решена в ООП — функцию h применяемую к объекту класса x вызывают как `x.h()`. Поэтому гораздо лучше всё читается, если писать `x.h().g().f()` — на каждом этапе видно, какая функция применилась к выходу предыдущей. Можно писать и в первом стиле, но в любом случае лучше соблюдать принцип, по которому какие-то функции являются внешними, а какие-то — объектами класса.\n", 147 | "\n", 148 | "Как хорошо, что в питоне об этом позаботились. " 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 4, 154 | "metadata": {}, 155 | "outputs": [ 156 | { 157 | "name": "stdout", 158 | "output_type": "stream", 159 | "text": [ 160 | "sorted(list(...)) returns the list: [3, 2, 1]\n", 161 | "a.sort() sorts a: [1, 2, 3]\n", 162 | " reversed(a) is a generator, ha-ha\n", 163 | "*reversed(a): 3 2 1 \n", 164 | "\n", 165 | "Want to reverse a? Think a.reverse() is the method?\n", 166 | "Ha-ha! Do a = a[::-1] [3, 2, 1] \n", 167 | "\n", 168 | "s={1, 2, 3}\n", 169 | "s1 = s.union({4, 5}): {1, 2, 3, 4, 5}\n", 170 | "[1, 2] + [3,4]: [1, 2, 3, 4]\n", 171 | "set.union({1,2,3}, {4,5}): {1, 2, 3, 4, 5}\n", 172 | "\n", 173 | "Iterate through a dictionary with keys and values:\n", 174 | "a 1\n", 175 | "b 2\n", 176 | "c 3\n", 177 | "d 2\n", 178 | "\n", 179 | "Iterate through a list with indices and values:\n", 180 | "0 1\n", 181 | "1 2\n", 182 | "2 3\n", 183 | "3 4\n" 184 | ] 185 | } 186 | ], 187 | "source": [ 188 | "d = {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 2}\n", 189 | "a = sorted(list(set(d.values())), reverse = True)\n", 190 | "print(\"sorted(list(...)) returns the list:\", a)\n", 191 | "a.sort() #Now a is sorted in the ascending order\n", 192 | "print(\"a.sort() sorts a:\", a) \n", 193 | "print(reversed(a), \"reversed(a) is a generator, ha-ha\")\n", 194 | "print(\"*reversed(a):\", *reversed(a), \"\\n\")\n", 195 | "\n", 196 | "print(\"Want to reverse a? Think a.reverse() is the method?\")\n", 197 | "a = a[::-1]\n", 198 | "print(\"Ha-ha! Do a = a[::-1]\", a, \"\\n\")\n", 199 | "\n", 200 | "s = {1, 2, 3}\n", 201 | "s1 = s.union({4, 5}) #s was not changed\n", 202 | "print(f\"{s=}\")\n", 203 | "print(\"s1 = s.union({4, 5}):\", s1)\n", 204 | "#s1 = union({1,2,3}, {4, 5}) # Wrong\n", 205 | "#s1 = {1,2,3} + {4, 5} # Wrong\n", 206 | "s1 = set.union({1,2,3}, {4,5}) #Correct\n", 207 | "a = [1, 2] + [3,4] #Ok\n", 208 | "print(\"[1, 2] + [3,4]:\", a) \n", 209 | "print(\"set.union({1,2,3}, {4,5}):\", s1) \n", 210 | "print(\"\\nIterate through a dictionary with keys and values:\")\n", 211 | "for k, v in d.items():\n", 212 | " print(k, v)\n", 213 | " \n", 214 | "print(\"\\nIterate through a list with indices and values:\")\n", 215 | "for i, x in enumerate(a):\n", 216 | " print(i, x)" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "metadata": {}, 222 | "source": [ 223 | "Вернёмся к прекрасной документации. https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range Обещает, что `x in a` вернёт нам `True`, если элемент `x` есть в массиве и `False` иначе. Давайте попробуем " 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 5, 229 | "metadata": {}, 230 | "outputs": [ 231 | { 232 | "name": "stdout", 233 | "output_type": "stream", 234 | "text": [ 235 | "\n", 236 | "a: [[1, 2], [3, 4]] v3: [1, 2]\n", 237 | "Looks ok\n", 238 | "\n", 239 | "a: [[1, 2], [3, 4]] v3: [1, 2]\n", 240 | "Looks ok\n", 241 | "\n", 242 | "a: [array([1, 2]), array([3, 4])] v3: [1 2]\n", 243 | "Looks ok\n", 244 | "\n", 245 | "a: [array([1, 2]), array([3, 4])] v3: [1 2]\n", 246 | "Everybody lies!\n", 247 | "\n", 248 | "a: [array([1, 2]), array([3, 4])] v3: [1 2]\n", 249 | "Looks ok\n", 250 | "\n", 251 | "a: [array([1, 2]), array([3, 4])] v3: [1 2]\n", 252 | "Everybody lies!\n" 253 | ] 254 | } 255 | ], 256 | "source": [ 257 | "import numpy as np\n", 258 | "\n", 259 | "def who_lies(v1, v2, v3):\n", 260 | " a = [v1, v2]\n", 261 | " print(\"\\na:\", a, \"v3:\", v3)\n", 262 | " try:\n", 263 | " if v3 in a:\n", 264 | " print(\"Looks ok\")\n", 265 | " except:\n", 266 | " print(\"Everybody lies!\")\n", 267 | "\n", 268 | "v1 = [1, 2]\n", 269 | "v2 = [3, 4]\n", 270 | "who_lies(v1, v2, v1)\n", 271 | "who_lies(v1, v2, [1,2])\n", 272 | " \n", 273 | "v1 = np.array([1, 2])\n", 274 | "v2 = np.array([3, 4])\n", 275 | "v3 = np.array([1, 2])\n", 276 | "who_lies(v1, v2, v1)\n", 277 | "who_lies(v1, v2, v3)\n", 278 | "\n", 279 | "v3 = v1\n", 280 | "who_lies(v1, v2, v3)\n", 281 | "who_lies(v1, v2, np.array([1, 2]))" 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": 6, 287 | "metadata": {}, 288 | "outputs": [ 289 | { 290 | "name": "stdout", 291 | "output_type": "stream", 292 | "text": [ 293 | "[array([1, 2]), array([3, 4])]\n", 294 | "[array([2, 3]), array([3, 4])]\n", 295 | "[[1, 2], [3, 4]]\n", 296 | "[[1, 2, 1], [3, 4]]\n" 297 | ] 298 | } 299 | ], 300 | "source": [ 301 | "import numpy as np\n", 302 | "\n", 303 | "v1 = np.array([1, 2])\n", 304 | "v2 = np.array([3, 4])\n", 305 | "a = [v1, v2]\n", 306 | "print(a)\n", 307 | "v1 += 1\n", 308 | "print(a)\n", 309 | "####\n", 310 | "v1 = [1,2]\n", 311 | "v2 = [3,4]\n", 312 | "a = [v1, v2]\n", 313 | "print(a)\n", 314 | "v1 += [1]\n", 315 | "print(a)" 316 | ] 317 | }, 318 | { 319 | "cell_type": "markdown", 320 | "metadata": {}, 321 | "source": [ 322 | "Насколько я понимаю, тут это работает так. В случае неизменяемых типов (`tuple`) оператор `in` проверяет непосредственное равенство `x` с объектами в структуре данных. В случае изменяемых типов проверяется либо вхождения объекта с тем же адресом (в случае с `np.array`), либо равенство, если реализован оператор проверки на равенство (`list`). В `np.array` есть оператор `==`, но он возвращает покомпонентное сравнение, что приводит к ошибке в случае проверки на наличие элемента типа `np.array` в массиве. \n", 323 | "\n", 324 | "В любом случае, результат оператора `in` выглядит довольно странно если спрашивать про наличие в массиве переменных `v1` и `v3` (в случае `np.array`). Видимо питон сначала проверяет по адресам, а потом проверяет на равенство, в случае, если проверка на совпадение адресов оказалась неуспешной.\n", 325 | "\n", 326 | "Защитники питона говорят, что это беда не языка, а библиотеки `numpy`, но, конечно, пользовотелям языка в первую очередь важно, кто виноват, а не то, как нелогично и непоследовательно работают в итоге операторы." 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": 7, 332 | "metadata": {}, 333 | "outputs": [ 334 | { 335 | "name": "stdout", 336 | "output_type": "stream", 337 | "text": [ 338 | "v1 == v2: [ True False] \n", 339 | "(v1 == v2).all(): False \n", 340 | "(v1 == v2).any(): True\n" 341 | ] 342 | }, 343 | { 344 | "ename": "ValueError", 345 | "evalue": "The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()", 346 | "output_type": "error", 347 | "traceback": [ 348 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 349 | "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", 350 | "Input \u001b[0;32mIn [7]\u001b[0m, in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28mprint\u001b[39m (\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mv1 == v2:\u001b[39m\u001b[38;5;124m\"\u001b[39m, v1 \u001b[38;5;241m==\u001b[39m v2, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m(v1 == v2).all():\u001b[39m\u001b[38;5;124m\"\u001b[39m, (v1 \u001b[38;5;241m==\u001b[39m v2)\u001b[38;5;241m.\u001b[39mall(), \u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m(v1 == v2).any():\u001b[39m\u001b[38;5;124m\"\u001b[39m, (v1 \u001b[38;5;241m==\u001b[39m v2)\u001b[38;5;241m.\u001b[39many())\n\u001b[1;32m 6\u001b[0m a \u001b[38;5;241m=\u001b[39m [v1, v2]\n\u001b[0;32m----> 7\u001b[0m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43marray\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43ma\u001b[49m\n", 351 | "\u001b[0;31mValueError\u001b[0m: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()" 352 | ] 353 | } 354 | ], 355 | "source": [ 356 | "import numpy as np\n", 357 | "\n", 358 | "v1 = np.array([1, 2])\n", 359 | "v2 = np.array([1, 4])\n", 360 | "print (\"v1 == v2:\", v1 == v2, \"\\n(v1 == v2).all():\", (v1 == v2).all(), \"\\n(v1 == v2).any():\", (v1 == v2).any())\n", 361 | "a = [v1, v2]\n", 362 | "np.array([1, 2]) in a" 363 | ] 364 | }, 365 | { 366 | "cell_type": "markdown", 367 | "metadata": {}, 368 | "source": [ 369 | "Чтобы идти дальше, давайте поговорим о генераторах, итераторах и почему они так называются. В C++ итератором называют специальный указатель, котороый указывает на текущий элемент контейнера, по которому ведётся итерация. Итераторы могут быть принципиально разными: в динамических массивах перейти на k-ый элемент справа от текущего можно за O(1), а в связном списке это будет стоить O(k). Но в целом, итераторы — это инструмент с помощью которых можно перебрать все элементы структуры данных.\n", 370 | "\n", 371 | "Эти знания об итераторах приводят в недоумение, если посмотреть определение из мана https://docs.python.org/3/glossary.html#term-generator и следующий пример." 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": 8, 377 | "metadata": {}, 378 | "outputs": [ 379 | { 380 | "name": "stdout", 381 | "output_type": "stream", 382 | "text": [ 383 | "7\n", 384 | "8\n", 385 | "7\n", 386 | "0\n", 387 | "0\n" 388 | ] 389 | } 390 | ], 391 | "source": [ 392 | "import random\n", 393 | "\n", 394 | "def rand_int():\n", 395 | " while True:\n", 396 | " yield random.randint(0, 10)\n", 397 | "\n", 398 | "g = rand_int()\n", 399 | "for x in range(5):\n", 400 | " print(next(g))" 401 | ] 402 | }, 403 | { 404 | "cell_type": "markdown", 405 | "metadata": {}, 406 | "source": [ 407 | "На самом деле итератор — это «указатель», который пробегает (рекурсивно-)перечислимое множество, а точнее последовательность (а генератор — функция, создающая итератор). Последовательность может быть как конечной, так и бесконечной. Это важное абстрактное понятие из теоретической информатики, которое нашло применение в современных языках программирования. Например, в том же питоне, функция print получает любое число аргументов — это реализовано через обращение к итератору, который последовательно подаёт значения последовательности, пока они не закончатся. Было бы логично назвать генераторы enumerators (сродни из теоретической информатики), но нет." 408 | ] 409 | }, 410 | { 411 | "cell_type": "markdown", 412 | "metadata": {}, 413 | "source": [ 414 | "Питон — интерпретируемый язык. Вам много кто радостно расскажет, что если ошибка оказалась в той ветке, куда алгоритм не ходит, то вылезет она внезапно. А теперь давайте я покажу как сделать пустой генератор." 415 | ] 416 | }, 417 | { 418 | "cell_type": "code", 419 | "execution_count": 9, 420 | "metadata": {}, 421 | "outputs": [ 422 | { 423 | "name": "stdout", 424 | "output_type": "stream", 425 | "text": [ 426 | "\n", 427 | "\n", 428 | "empty_gen() contains:\n", 429 | "non_empty_gen() contains: 1\n", 430 | "Everything is fine\n" 431 | ] 432 | } 433 | ], 434 | "source": [ 435 | "def empty_gen():\n", 436 | " if False:\n", 437 | " yield None\n", 438 | "\n", 439 | "def non_empty_gen():\n", 440 | " yield 1\n", 441 | "\n", 442 | "print(type(empty_gen))\n", 443 | "print(empty_gen())\n", 444 | "print(\"empty_gen() contains:\", *empty_gen())\n", 445 | "print(\"non_empty_gen() contains:\", *non_empty_gen())\n", 446 | "\n", 447 | "if False:\n", 448 | " No + one - cares / what is going + on \n", 449 | " if the + syntax is fine:\n", 450 | " pass\n", 451 | "else:\n", 452 | " print(\"Everything is fine\")\n" 453 | ] 454 | }, 455 | { 456 | "cell_type": "markdown", 457 | "metadata": {}, 458 | "source": [ 459 | "Я слышал, что питон позиционируют как язык, близкий к математикам. В математике есть устоявшийся термин — отображение, `map`. Отображение получает на вход аргумент из множества X и возвращает значение из множества Y. Логично, чтобы отображение, применённое к множеству, возвращало множество, а к массиву — массив значений отображения, но нет! Отображение возвращает генератор, а чтобы получить тип, нужно вызвать конструктор соответствующего объекта (но это не точно)." 460 | ] 461 | }, 462 | { 463 | "cell_type": "code", 464 | "execution_count": 10, 465 | "metadata": {}, 466 | "outputs": [ 467 | { 468 | "name": "stdout", 469 | "output_type": "stream", 470 | "text": [ 471 | "\n", 472 | "[2, 3, 4]\n", 473 | "\n", 474 | "[2 3 4] \n" 475 | ] 476 | } 477 | ], 478 | "source": [ 479 | "import numpy as np\n", 480 | "\n", 481 | "s = {1, 2, 3}\n", 482 | "\n", 483 | "print( map(lambda x : x+1, s) )\n", 484 | "print( list(map(lambda x : x+1, s)) )\n", 485 | "print( np.array(map(lambda x : x+1, s)) )\n", 486 | "na = np.array(list(map(lambda x : x+1, s)))\n", 487 | "print( na, type(na) )" 488 | ] 489 | }, 490 | { 491 | "cell_type": "markdown", 492 | "metadata": {}, 493 | "source": [ 494 | "Преобразование `list` или `set` через `map` выглядит отвратительно и так делать не надо. Для этого есть так называемые comprehensions, в которых можно проводить фильтрацию. Жаль только, что такая фильтрация не доступна в цикле `for`. Можно, конечно, сделать фильтрацию через генераторы, что однако выглядит отвратительно. Можно ещё написать в функциональном стиле, используя `filter`, но внутри for это тоже выглядит отвратительно из-за смешения стилей." 495 | ] 496 | }, 497 | { 498 | "cell_type": "code", 499 | "execution_count": 11, 500 | "metadata": {}, 501 | "outputs": [ 502 | { 503 | "name": "stdout", 504 | "output_type": "stream", 505 | "text": [ 506 | "[2, 3, 4]\n", 507 | "[2, 4]\n", 508 | "1\n", 509 | "3\n", 510 | "\n", 511 | "1\n", 512 | "3\n", 513 | "\n", 514 | "1\n", 515 | "3\n" 516 | ] 517 | } 518 | ], 519 | "source": [ 520 | "s = {1, 2, 3}\n", 521 | "a = [ x + 1 for x in s ]\n", 522 | "print(a)\n", 523 | "print([x + 1 for x in s if (x % 2 == 1)])\n", 524 | "for x in s:\n", 525 | " if (x % 2 == 1):\n", 526 | " print(x) \n", 527 | " \n", 528 | "#for x in s if (x % 2 == 1): #Does not work\n", 529 | "# print(x)\n", 530 | "print(\"\")\n", 531 | "for x in (x for x in s if x % 2 == 1):\n", 532 | " print(x) \n", 533 | " \n", 534 | "print(\"\")\n", 535 | "for x in filter(lambda x: x % 2 == 1, s):\n", 536 | " print(x) " 537 | ] 538 | }, 539 | { 540 | "cell_type": "markdown", 541 | "metadata": {}, 542 | "source": [ 543 | "Давайте поговорим ещё о последовательности питона в плане удобства фич. Оператор `*` перед контейнером превращает его в вывод генератора (в конечную последовательность) или проще, распасковывает содержимое контейнера. Также можно делать распаковку через присваивание, как хорошо, что в питоне это делать легко, удобно, и всюду логика работы распаковки одинаковая!" 544 | ] 545 | }, 546 | { 547 | "cell_type": "code", 548 | "execution_count": 12, 549 | "metadata": {}, 550 | "outputs": [ 551 | { 552 | "name": "stdout", 553 | "output_type": "stream", 554 | "text": [ 555 | "1 2\n", 556 | "3 4\n", 557 | "5 6\n", 558 | "===\n", 559 | "1 2\n", 560 | "3 4\n", 561 | "5 6\n", 562 | "===\n", 563 | "Oops, it does not compile\n", 564 | "===\n", 565 | "2 3\n", 566 | "4 5\n", 567 | "6 7\n", 568 | "===\n", 569 | "2 3\n", 570 | "4 5\n", 571 | "6 7\n", 572 | "\n", 573 | "Generator unpack example:\n", 574 | "1 2 3\n" 575 | ] 576 | } 577 | ], 578 | "source": [ 579 | "a = [(1,2), (3,4), (5,6)]\n", 580 | "\n", 581 | "for x, y in a:\n", 582 | " print(x, y) \n", 583 | "print(\"===\")\n", 584 | "\n", 585 | "for (x, y) in a:\n", 586 | " print(x, y) \n", 587 | "print(\"===\")\n", 588 | " \n", 589 | "try:\n", 590 | " for x, y in map(lambda x, y: (x+1, y+1), a):\n", 591 | " print(x, y)\n", 592 | "except:\n", 593 | " print(\"Oops, it does not compile\")\n", 594 | "print(\"===\")\n", 595 | "\n", 596 | "#for x, y in map(lambda (x, y): (x+1, y+1), a): ## Invalid Syntax\n", 597 | "# print(x, y)\n", 598 | "\n", 599 | "for x, y in map(lambda xy: (xy[0]+1, xy[1]+1), a):\n", 600 | " print(x, y)\n", 601 | "\n", 602 | "print(\"===\")\n", 603 | "\n", 604 | "f = lambda x, y: (x+1, y+1)\n", 605 | "for xy in a:\n", 606 | " print( *f(*xy) )\n", 607 | "\n", 608 | "print(\"\\nGenerator unpack example:\") \n", 609 | " \n", 610 | "def three_ints():\n", 611 | " yield 1\n", 612 | " yield 2\n", 613 | " yield 3\n", 614 | " \n", 615 | "print(*three_ints())\n", 616 | "\n" 617 | ] 618 | }, 619 | { 620 | "cell_type": "markdown", 621 | "metadata": {}, 622 | "source": [ 623 | "Давайте поговорим о классах. Чтобы все понимали, что функция является объектом класса, она не просто пишется внутри класса, а у неё есть первый аргумент, который согласно гайдлайном следует всегда называть `self`. А потом ссылаться на функцию `f` или переменную `x` в классе нужно через `self.f` или `self.x` в других методах класса. А если вы хотите сначала отладить кусок кода в другом файле/юпитере перед тем как поместить его в класс, то перед всеми переменными класса нужно будет ставить `self` — очень удобно." 624 | ] 625 | }, 626 | { 627 | "cell_type": "code", 628 | "execution_count": 13, 629 | "metadata": {}, 630 | "outputs": [ 631 | { 632 | "name": "stdout", 633 | "output_type": "stream", 634 | "text": [ 635 | "3\n" 636 | ] 637 | } 638 | ], 639 | "source": [ 640 | "class my:\n", 641 | " def __init__(self):\n", 642 | " self.x = 1\n", 643 | " def f(self, y):\n", 644 | " return self.x + y\n", 645 | " \n", 646 | "m = my()\n", 647 | "print(m.f(2))" 648 | ] 649 | }, 650 | { 651 | "cell_type": "markdown", 652 | "metadata": {}, 653 | "source": [ 654 | "Причём забавно, что в руби эту проблему решили изящно. Перед переменными объекта класса нужно ставить `@` и ту же `@` можно использовать и с переменными в общем коде. Собственно `@` перед вашими никами в твиттере возникла именно из префикса переменных в руби. Также в руби `map` переводит массив в массив и записывается по-человечески." 655 | ] 656 | }, 657 | { 658 | "cell_type": "markdown", 659 | "metadata": {}, 660 | "source": [ 661 | "Давайте поговорим об удобстве. Питон широко используют для прототипирования. Поэтому в нём отсутствуют такие базовые методы как оставить в массиве только уникальные элементы. Многие скажут, что есть же `list(set(a))`, но это работает, только если у вас элементы имеют hashable type, потому что `set` сделан через хеш. Хотите `set` на красно-чёрных деревьях? Идите ищите его в сторонних библиотеках! На самом деле хранить в листе неизменяемые элементы — это очень хороший тон, если они меняться действительно не будут. Это лишняя защита от выстрела в ногу." 662 | ] 663 | }, 664 | { 665 | "cell_type": "code", 666 | "execution_count": 14, 667 | "metadata": {}, 668 | "outputs": [ 669 | { 670 | "name": "stdout", 671 | "output_type": "stream", 672 | "text": [ 673 | "[1, 2, 3, 4]\n", 674 | "Sorry\n", 675 | "Sorry again\n", 676 | "Not sorry: [(1, 2), (3, 4)]\n", 677 | "Because of: [[2, 2], [3, 4], [1, 2]]\n" 678 | ] 679 | } 680 | ], 681 | "source": [ 682 | "import numpy as np\n", 683 | "a = [1, 2, 3, 1, 4, 2, 1]\n", 684 | "b = list(set(a))\n", 685 | "print(b)\n", 686 | "try:\n", 687 | " a = [ np.array([1, 2]), np.array([3, 4]), np.array([1, 2])]\n", 688 | " b = list(set(a))\n", 689 | "except:\n", 690 | " print(\"Sorry\")\n", 691 | " \n", 692 | "c = [1,2]\n", 693 | "aa = [ c, [3,4], [1,2]] \n", 694 | "try: \n", 695 | " b = list(set(aa))\n", 696 | "except:\n", 697 | " print(\"Sorry again\") \n", 698 | "\n", 699 | "a = [ (1,2), (3,4), (1,2)]\n", 700 | "b = list(set(a))\n", 701 | "print(\"Not sorry:\", b)\n", 702 | "c[0] += 1\n", 703 | "print(\"Because of:\", aa)" 704 | ] 705 | }, 706 | { 707 | "cell_type": "markdown", 708 | "metadata": {}, 709 | "source": [ 710 | "Ещё об удобстве. Последний элемент массива `a` можно получить, вызвав `a[-1]`, а отрезок массива можно получить через `a[i:j]`, причём это работает и с отрицательными индексами! Казалось бы удобно, но есть нюанс. В случае вызова `a[i:-1]` последний элемент будет обезан, то есть если вы хотите обрезать `k` элементов с конца, то нужно писать `a[0:-k]`, но при этом `k` не может быть равно нулю! Да, можно написать `a[0:]`, но это не работает, если параметр `k` определяется алгоритмом, приходится писать `a[0:len(a)-k]`, что уже выглядит уродски." 711 | ] 712 | }, 713 | { 714 | "cell_type": "code", 715 | "execution_count": 15, 716 | "metadata": {}, 717 | "outputs": [ 718 | { 719 | "name": "stdout", 720 | "output_type": "stream", 721 | "text": [ 722 | "a: [1, 2, 3, 4]\n", 723 | "a[-1]: 4\n", 724 | "a[0:-1]: [1, 2, 3]\n", 725 | "a[0:0]: []\n", 726 | "a[0:]: [1, 2, 3, 4]\n", 727 | "a[0:len(a)-2]: [1, 2]\n", 728 | "a[:-2]: [1, 2]\n", 729 | "a[0:len(a)-0]: [1, 2, 3, 4]\n", 730 | "a[:-0]: []\n" 731 | ] 732 | } 733 | ], 734 | "source": [ 735 | "a = [1, 2, 3 ,4]\n", 736 | "print(\"a:\", a)\n", 737 | "print(\"a[-1]:\", a[-1])\n", 738 | "print(\"a[0:-1]:\", a[0:-1])\n", 739 | "print(\"a[0:0]:\", a[0:0])\n", 740 | "print(\"a[0:]:\", a[0:])\n", 741 | "print(\"a[0:len(a)-2]:\", a[0:len(a)-2])\n", 742 | "print(\"a[:-2]:\", a[:-2])\n", 743 | "print(\"a[0:len(a)-0]:\", a[0:len(a)-0])\n", 744 | "print(\"a[:-0]:\", a[:-0])" 745 | ] 746 | }, 747 | { 748 | "cell_type": "markdown", 749 | "metadata": {}, 750 | "source": [ 751 | "Решение с невключением правой границы `r` в слайс `a[l:r]` оказалось настолько неудачным, что в библиотеки Pandas пришлось отойти от этой практики и изменить стандартный подход работы со слайсами на включение правой границы. Причина для этого простая: если вы выбираете столбцы по загловкам или строки таблицы по значению поля, то вы знаете значение первого и последнего заголовка (поля), но не следующего за ним, которого может и не быть!" 752 | ] 753 | }, 754 | { 755 | "cell_type": "markdown", 756 | "metadata": {}, 757 | "source": [ 758 | "Перейдём к дессерту. Как выстрелить себе в ногу, если пишешь интуитивно простой и понятный код?" 759 | ] 760 | }, 761 | { 762 | "cell_type": "code", 763 | "execution_count": 16, 764 | "metadata": {}, 765 | "outputs": [ 766 | { 767 | "name": "stdout", 768 | "output_type": "stream", 769 | "text": [ 770 | "[1]\n", 771 | "[1, 2]\n", 772 | "[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", 773 | "[[1], [1], [1], [1], [1], [1], [1], [1], [1], [1]]\n", 774 | "[[1], [], [], [], [], [], [], [], [], []]\n", 775 | "\n", 776 | "Default dict:\n", 777 | "[1] []\n", 778 | "\n", 779 | "Disaster!\n", 780 | "[0]\n", 781 | "[0, 0, 0] [0, 0, 0]\n", 782 | "[[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]\n" 783 | ] 784 | } 785 | ], 786 | "source": [ 787 | "def f(x, l=[]):\n", 788 | " l.append(x)\n", 789 | " return l\n", 790 | "\n", 791 | "print(f(1))\n", 792 | "print(f(2))\n", 793 | "\n", 794 | "lnum = [0]*10\n", 795 | "lnum[0] += 1\n", 796 | "print(lnum)\n", 797 | "\n", 798 | "\n", 799 | "ll = [[]]*10\n", 800 | "ll[0] += [1]\n", 801 | "print(ll)\n", 802 | "\n", 803 | "ll_good = [[] for _ in range(10)]\n", 804 | "ll_good[0] += [1]\n", 805 | "print(ll_good)\n", 806 | "\n", 807 | "print(\"\\nDefault dict:\")\n", 808 | "from collections import defaultdict\n", 809 | "dd = defaultdict(lambda: [])\n", 810 | "dd[0] += [1]\n", 811 | "print(dd[0], dd[1]) \n", 812 | "\n", 813 | "print(\"\\nDisaster!\")\n", 814 | "def g(l=[]):\n", 815 | " l += [0]\n", 816 | " return l\n", 817 | "\n", 818 | "disaster = defaultdict(g)\n", 819 | "print(disaster[0])\n", 820 | "print(disaster[1], disaster[2])\n", 821 | "print([disaster[i] for i in range(3,6)])" 822 | ] 823 | }, 824 | { 825 | "cell_type": "markdown", 826 | "metadata": {}, 827 | "source": [ 828 | "Что же тут произошло?! В питоне каждый объект — это по-сути словарь, а переменная — ссылка (почти). Когда мы выставили значение по-умолчанию `l=[]` в функции `f`, а потом изменили `l`, то мы изменили пустой список, на который «смотрит» переменная по-умолчанию, а точнее на этот список смотрит строка в словаре, соответствующая этой переменной и она меняется. Поэтому аргументы по-умолчанию в функциях важно делать немутабельными.\n", 829 | "\n", 830 | "Про инициализацию списка пустых списков та же история. Код `[[]]*10` не создаёт пустой список 10 раз, в отличие от `[[] for _ in range(10)]`, а клонирует уже созданный пустой список, что приводит к фатальным последствиям в случае невнимательности. Можно было бы реализовать `[[]]*10` как синтаксический сахар для `[[] for _ in range(10)]`, но это нарушало бы логику языка (что уже, впрочем, не страшно), поэтому в `defaultdict` требуется подавать не объект, который нужно вернуть как значение по-умолчанию, а генератор этого объекта.\n", 831 | "\n", 832 | "Отдельная проблема состоит в том, что оператор `+=` реализован непоследовательно по отношению к переменным как ссылкам. Сравним работу этого оператора на числах и списках." 833 | ] 834 | }, 835 | { 836 | "cell_type": "code", 837 | "execution_count": 17, 838 | "metadata": {}, 839 | "outputs": [ 840 | { 841 | "name": "stdout", 842 | "output_type": "stream", 843 | "text": [ 844 | "x=2,y=1\n", 845 | "plus_eq(x,y)=3\n", 846 | "x=2\n" 847 | ] 848 | } 849 | ], 850 | "source": [ 851 | "def plus_eq(a, b):\n", 852 | " a += b\n", 853 | " return a\n", 854 | "\n", 855 | "x = 1\n", 856 | "y = x\n", 857 | "x += 1\n", 858 | "print(f\"{x=},{y=}\")\n", 859 | "print(f\"{plus_eq(x,y)=}\")\n", 860 | "print(f\"{x=}\")" 861 | ] 862 | }, 863 | { 864 | "cell_type": "code", 865 | "execution_count": 18, 866 | "metadata": {}, 867 | "outputs": [ 868 | { 869 | "name": "stdout", 870 | "output_type": "stream", 871 | "text": [ 872 | "x=[1, 1],y=[1, 1]\n", 873 | "plus_eq(x,y)=[1, 1, 1, 1]\n", 874 | "x=[1, 1, 1, 1]\n" 875 | ] 876 | } 877 | ], 878 | "source": [ 879 | "x = [1]\n", 880 | "y = x\n", 881 | "x += [1]\n", 882 | "print(f\"{x=},{y=}\")\n", 883 | "print(f\"{plus_eq(x,y)=}\")\n", 884 | "print(f\"{x=}\")" 885 | ] 886 | }, 887 | { 888 | "cell_type": "markdown", 889 | "metadata": {}, 890 | "source": [ 891 | "Если бы присваивание `y = x` в случае чисел указывало бы на тот же объект, что и `x`, а оператор `x += 1` изменял бы этот объект, то переменная `y` хранила бы то же значение, что и `x`. Но фишка в том, что `x` указывает на иммутабельный объект — число `1`, поэтому результат оператора `x += 1` не меняет объект, на который указывает `x`, а направляет ссылку переменной `x` на новый объект! Тоже неплохо, особенно в случае чисел, но это приводит к непоследовательности в случае со списками. " 892 | ] 893 | }, 894 | { 895 | "cell_type": "markdown", 896 | "metadata": {}, 897 | "source": [ 898 | "Любители Питона в ответ на изложенную критику часто отвечают, что, мол, надо просто знать язык, тогда проблем не будет, или объясняют, почему конкретный сомнительный кейс работает так как он работает. И то и другое нелепо. Конечно, нужно достаточно хорошо знать язык, на котором пишешь, хотя современные программисты часто этого не делают, а туториалы часто сводятся к набору примеров без аккуратного объяснения, что за ними стоит. Да и что уж тут говорить, документация Питона написана так, что не способствует пониманию устройства языка! Для этого нужно читать книжки, а программисты часто этого не делают. Кстати, в случае Питона могу порекомендовать книжку Лутца, хотя она и занудная.\n", 899 | "\n", 900 | "Возвращаясь к частым ответам любителей Питона на критику. Конечно, программы на языке работают так, как их исполняет интерпретатор (для зануд, я знаю, что у Питона есть ещё фаза прекомпеляции), речь же о том, сколько неудачных решений было допущено при проектировании языка! Да, с этими решениями можно разобраться и жить, но они вечно напоминают о себе, заставляя уродовать код при достижении очевидных и простых целей." 901 | ] 902 | }, 903 | { 904 | "cell_type": "code", 905 | "execution_count": null, 906 | "metadata": {}, 907 | "outputs": [], 908 | "source": [] 909 | } 910 | ], 911 | "metadata": { 912 | "kernelspec": { 913 | "display_name": "Python 3 (ipykernel)", 914 | "language": "python", 915 | "name": "python3" 916 | }, 917 | "language_info": { 918 | "codemirror_mode": { 919 | "name": "ipython", 920 | "version": 3 921 | }, 922 | "file_extension": ".py", 923 | "mimetype": "text/x-python", 924 | "name": "python", 925 | "nbconvert_exporter": "python", 926 | "pygments_lexer": "ipython3", 927 | "version": "3.9.12" 928 | } 929 | }, 930 | "nbformat": 4, 931 | "nbformat_minor": 4 932 | } 933 | --------------------------------------------------------------------------------