What the f*ck Python! 😱

Изучение и понимание Python с помощью нестандартного поведения и "магического" поведения.

Переводы: [English Original](https://github.com/satwikkansal/wtfpython) [Chinese 中文](https://github.com/robertparley/wtfpython-cn) | [Vietnamese Tiếng Việt](https://github.com/vuduclyunitn/wtfptyhon-vi) | [Spanish Español](https://web.archive.org/web/20220511161045/https://github.com/JoseDeFreitas/wtfpython-es) | [Korean 한국어](https://github.com/buttercrab/wtfpython-ko) | [Russian Русский](https://github.com/nifadyev/wtfpython/tree/main/translations/README-ru.md) | [German Deutsch](https://github.com/BenSt099/wtfpython) | [Add translation](https://github.com/satwikkansal/wtfpython/issues/new?title=Add%20translation%20for%20[LANGUAGE]&body=Expected%20time%20to%20finish:%20[X]%20weeks.%20I%27ll%20start%20working%20on%20it%20from%20[Y].) Альтернативные способы: [Интерактивный сайт](https://wtfpython-interactive.vercel.app) | [Интерактивный Jupiter notebook](https://colab.research.google.com/github/satwikkansal/wtfpython/blob/master/irrelevant/wtf.ipynb) | [CLI](https://pypi.python.org/pypi/wtfpython) Python, будучи прекрасно спроектированным высокоуровневым языком программирования, предоставляет множество возможностей для удобства программиста. Но иногда результаты работы Python кода могут показаться неочевидными на первый взгляд. **wtfpython** задуман как проект, пытающийся объяснить, что именно происходит под капотом некоторых неочевидных фрагментов кода и менее известных возможностей Python. Если вы опытный программист на Python, вы можете принять это как вызов и правильно объяснить WTF ситуации с первой попытки. Возможно, вы уже сталкивались с некоторыми из них раньше, и я смогу оживить ваши старые добрые воспоминания! 😅 PS: Если вы уже читали **wtfpython** раньше, с изменениями можно ознакомиться [здесь](https://github.com/satwikkansal/wtfpython/releases/) (примеры, отмеченные звездочкой - это примеры, добавленные в последней основной редакции). Ну что ж, приступим... # Содержание - [Содержание](#содержание) - [Структура примера](#структура-примера) - [Применение](#применение) - [👀 Примеры](#-примеры) - [Секция: Напряги мозги!](#секция-напряги-мозги) - [▶ Первым делом!](#-первым-делом) - [💡 Обьяснение](#-обьяснение) - [▶ Строки иногда ведут себя непредсказуемо](#-строки-иногда-ведут-себя-непредсказуемо) - [💡 Объяснение](#-объяснение) # Структура примера Все примеры имеют следующую структуру: > ### ▶ Какой-то заголовок > > ```py > # Неочевидный фрагмент кода > # Подготовка к магии... > ``` > > **Вывод (Python версия):** > > ```py > >>> triggering_statement > Неожиданные результаты > ``` > > (Опционально): Краткое описание неожиданного результата > > > #### 💡 Объяснение > > * Краткое объяснение того, что происходит и почему это происходит. > > ```py > # Код > # Дополнительные примеры для дальнейшего разъяснения (если необходимо) > ``` > > **Вывод (Python версия):** > > ```py > >>> trigger # какой-нибудь пример, позволяющий легко раскрыть магию > # обоснованный вывод > ``` **Важно:** Все примеры протестированы на интерактивном интерпретаторе Python 3.5.2, и они должны работать для всех версий Python, если это явно не указано перед выводом. # Применение Хороший способ получить максимальную пользу от этих примеров - читать их последовательно, причем для каждого из них важно: - Внимательно изучить исходный код. Если вы опытный программист на Python, то в большинстве случаев сможете предугадать, что произойдет дальше. - Прочитать фрагменты вывода и, - Проверить, совпадают ли выходные данные с вашими ожиданиями. - Убедиться, что вы знаете точную причину, по которой вывод получился именно таким. - Если ответ отрицательный (что совершенно нормально), сделать глубокий вдох и прочитать объяснение (а если пример все еще непонятен, и создайте issue [здесь](https://github.com/satwikkansal/wtfpython/issues/new)). - Если "да", ощутите мощь своих познаний в Python и переходите к следующему примеру. PS: Вы также можете читать WTFPython в командной строке, используя [pypi package](https://pypi.python.org/pypi/wtfpython), ```sh pip install wtfpython -U wtfpython ``` # 👀 Примеры ## Секция: Напряги мозги! ### ▶ Первым делом! По какой-то причине "моржовый оператор" (англ. walrus) `:=` в Python 3.8 стал довольно популярным. Давайте проверим его, 1\. ```py # Python version 3.8+ >>> a = "wtf_walrus" >>> a 'wtf_walrus' >>> a := "wtf_walrus" File "", line 1 a := "wtf_walrus" ^ SyntaxError: invalid syntax >>> (a := "wtf_walrus") # А этот код работает 'wtf_walrus' >>> a 'wtf_walrus' ``` 2 \. ```py # Python version 3.8+ >>> a = 6, 9 >>> a (6, 9) >>> (a := 6, 9) (6, 9) >>> a 6 >>> a, b = 6, 9 # Типичная распаковка >>> a, b (6, 9) >>> (a, b = 16, 19) # Упс File "", line 1 (a, b = 16, 19) ^ SyntaxError: invalid syntax >>> (a, b := 16, 19) # На выводе получаем странный кортеж из 3 элементов (6, 16, 19) >>> a # Значение переменной остается неизменной? 6 >>> b 16 ``` #### 💡 Обьяснение **Быстрый разбор что такое "моржовый оператор"** "Моржовый оператор" (`:=`) был представлен в Python 3.8, может быть полезен в ситуациях, когда вы хотите присвоить значения переменным в выражении. ```py def some_func(): # Предположим, что здесь выполняются требовательные к ресурсам вычисления # time.sleep(1000) return 5 # Поэтому вместо, if some_func(): print(some_func()) # Плохая практика, поскольку вычисления происходят дважды. # Или a = some_func() if a: print(a) # Можно лаконично написать if a := some_func(): print(a) ``` **Вывод (> 3.8):** ```py 5 5 5 ``` Использование `:=` сэкономило одну строку кода и неявно предотвратило вызов `some_func` дважды. - "выражение присваивания", не обернутое в скобки, иначе говоря использование моржового оператора, ограничено на верхнем уровне, отсюда `SyntaxError` в выражении `a := "wtf_walrus"` в первом фрагменте. После оборачивания в скобки, `a` было присвоено значение, как и ожидалось. - В то же время оборачивание скобками выражения, содержащего оператор `=`, не допускается. Отсюда синтаксическая ошибка в `(a, b = 6, 9)`. - Синтаксис моржового оператора имеет вид `NAME:= expr`, где `NAME` - допустимый идентификатор, а `expr` - допустимое выражение. Следовательно, упаковка и распаковка итерируемых объектов не поддерживается, что означает, - `(a := 6, 9)` эквивалентно `((a := 6), 9)` и в конечном итоге `(a, 9)` (где значение `a` равно `6`) ```py >>> (a := 6, 9) == ((a := 6), 9) True >>> x = (a := 696, 9) >>> x (696, 9) >>> x[0] is a # Оба ссылаются на одну и ту же ячейку памяти True ``` - Аналогично, `(a, b := 16, 19)` эквивалентно `(a, (b := 16), 19)`, которое есть не что иное, как кортеж из 3 элементов. --- ### ▶ Строки иногда ведут себя непредсказуемо 1\. ```py >>> a = "some_string" >>> id(a) 140420665652016 >>> id("some" + "_" + "string") # Обратите внимание, оба идентификатора одинаковы 140420665652016 ``` 2\. ```py >>> a = "wtf" >>> b = "wtf" >>> a is b True >>> a = "wtf!" >>> b = "wtf!" >>> a is b False ``` 3\. ```py >>> a, b = "wtf!", "wtf!" >>> a is b # Актуально для версий Python, кроме 3.7.x True >>> a = "wtf!"; b = "wtf!" >>> a is b # Выражение вернет True или False в зависимости вызываемой среды (python shell / ipython / скрипт). False ``` ```py # На этот раз в файле a = "wtf!" b = "wtf!" print(a is b) # Выводит True при запуске модуля ``` 4\. **Output (< Python3.7 )** ```py >>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa' True >>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa' False ``` Логично, правда? #### 💡 Объяснение - Поведение в первом и втором фрагментах связано с оптимизацией CPython (называемой интернированием строк ((англ. string interning))), которая пытается использовать существующие неизменяемые объекты в некоторых случаях вместо того, чтобы каждый раз создавать новый объект. - После "интернирования" многие переменные могут ссылаться на один и тот же строковый объект в памяти (тем самым экономя память). - В приведенных выше фрагментах строки неявно интернированы. Решение о том, когда неявно интернировать строку, зависит от реализации. Правила для интернирования строк следующие: - Все строки длиной 0 или 1 символа интернируются. - Строки интернируются во время компиляции (`'wtf'` будет интернирована, но `''.join(['w'', 't', 'f'])` - нет) - Строки, не состоящие из букв ASCII, цифр или знаков подчеркивания, не интернируются. В примере выше `'wtf!'` не интернируется из-за `!`. Реализацию этого правила в CPython можно найти [здесь](https://github.com/python/cpython/blob/3.6/Objects/codeobject.c#L19) ![image](/images/string-intern/string_intern.png) - Когда переменные `a` и `b` принимают значение `"wtf!"` в одной строке, интерпретатор Python создает новый объект, а затем одновременно ссылается на вторую переменную. Если это выполняется в отдельных строках, он не "знает", что уже существует `"wtf!"` как объект (потому что `"wtf!"` не является неявно интернированным в соответствии с фактами, упомянутыми выше). Это оптимизация во время компиляции, не применяется к версиям CPython 3.7.x (более подробное обсуждение смотрите здесь [issue](https://github.com/satwikkansal/wtfpython/issues/100)). - Единица компиляции в интерактивной среде IPython состоит из одного оператора, тогда как в случае модулей она состоит из всего модуля. `a, b = "wtf!", "wtf!"` - это одно утверждение, тогда как `a = "wtf!"; b = "wtf!"` - это два утверждения в одной строке. Это объясняет, почему тождества различны в `a = "wtf!"; b = "wtf!"`, но одинаковы при вызове в модуле. - Резкое изменение в выводе четвертого фрагмента связано с [peephole optimization](https://en.wikipedia.org/wiki/Peephole_optimization) техникой, известной как складывание констант (англ. Constant folding). Это означает, что выражение `'a'*20` заменяется на `'aaaaaaaaaaaaaaaaaaaa'` во время компиляции, чтобы сэкономить несколько тактов во время выполнения. Складывание констант происходит только для строк длиной менее 21. (Почему? Представьте себе размер файла `.pyc`, созданного в результате выполнения выражения `'a'*10**10`). [Вот](https://github.com/python/cpython/blob/3.6/Python/peephole.c#L288) исходный текст реализации для этого. - Примечание: В Python 3.7 складывание констант было перенесено из оптимизатора peephole в новый оптимизатор AST с некоторыми изменениями в логике, поэтому четвертый фрагмент не работает в Python 3.7. Подробнее об изменении можно прочитать [здесь](https://bugs.python.org/issue11549). --- ### ▶ Осторожнее с цепочкой операций ```py >>> (False == False) in [False] # логично False >>> False == (False in [False]) # все еще логично False >>> False == False in [False] # а теперь что? True >>> True is False == False False >>> False is False is False True >>> 1 > 0 < 1 True >>> (1 > 0) < 1 False >>> 1 > (0 < 1) False ``` #### 💡 Объяснение: Согласно https://docs.python.org/3/reference/expressions.html#comparisons > Формально, если a, b, c, ..., y, z - выражения, а op1, op2, ..., opN - операторы сравнения, то a op1 b op2 c ... y opN z эквивалентно a op1 b и b op2 c и ... y opN z, за исключением того, что каждое выражение оценивается не более одного раза. Хотя такое поведение может показаться глупым в приведенных выше примерах, оно просто фантастично для таких вещей, как `a == b == c` и `0 <= x <= 100`. * `False is False is False` эквивалентно `(False is False) и (False is False)`. * `True is False == False` эквивалентно `(True is False) and (False == False)` и так как первая часть высказывания (`True is False`) оценивается в `False`, то все выражение приводится к `False`. * `1 > 0 < 1` эквивалентно `(1 > 0) и (0 < 1)`, которое приводится к `True`. * Выражение `(1 > 0) < 1` эквивалентно `True < 1` и ```py >>> int(True) 1 >>> True + 1 # не относится к данному примеру, но просто для интереса 2 ``` В итоге, `1 < 1` выполняется и дает результат `False` --- ### ▶ Как не надо использовать оператор `is` Ниже приведен очень известный пример. 1\. ```py >>> a = 256 >>> b = 256 >>> a is b True >>> a = 257 >>> b = 257 >>> a is b False ``` 2\. ```py >>> a = [] >>> b = [] >>> a is b False >>> a = tuple() >>> b = tuple() >>> a is b True ``` 3\. **Результат** ```py >>> a, b = 257, 257 >>> a is b True ``` **Вывод (только для Python 3.7.x)** ```py >>> a, b = 257, 257 >>> a is b False ``` #### 💡 Объяснение: **Разница между `is` и `==`**. * Оператор `is` проверяет, ссылаются ли оба операнда на один и тот же объект (т.е. проверяет, совпадают ли идентификаторы операндов или нет). * Оператор `==` сравнивает значения обоих операндов и проверяет, одинаковы ли они. * Таким образом, оператор `is` предназначен для равенства ссылок, а `==` - для равенства значений. Пример, чтобы прояснить ситуацию, ```py >>> class A: pass >>> A() is A() # 2 пустых объекта в разных ячейках памяти False ``` **`256` - существующий объект, а `257` - нет**. При запуске python числа от `-5` до `256` записываются в память. Эти числа используются часто, поэтому имеет смысл просто иметь их наготове. Перевод цитаты из [документации](https://docs.python.org/3/c-api/long.html) > Текущая реализация хранит массив целочисленных объектов для всех целых чисел от -5 до 256, когда вы создаете int в этом диапазоне, вы просто получаете обратно ссылку на существующий объект. ```py >>> id(256) 10922528 >>> a = 256 >>> b = 256 >>> id(a) 10922528 >>> id(b) 10922528 >>> id(257) 140084850247312 >>> x = 257 >>> y = 257 >>> id(x) 140084850247440 >>> id(y) 140084850247344 ``` Интерпретатор не понимает, что до выполнения выражения `y = 257` целое число со значением `257` уже создано, и поэтому он продолжает создавать другой объект в памяти. Подобная оптимизация применима и к другим **изменяемым** объектам, таким как пустые кортежи. Поскольку списки являются изменяемыми, поэтому `[] is []` вернет `False`, а `() is ()` вернет `True`. Это объясняет наш второй фрагмент. Перейдем к третьему, **И `a`, и `b` ссылаются на один и тот же объект при инициализации одним и тем же значением в одной и той же строкеi**. **Вывод** ```py >>> a, b = 257, 257 >>> id(a) 140640774013296 >>> id(b) 140640774013296 >>> a = 257 >>> b = 257 >>> id(a) 140640774013392 >>> id(b) 140640774013488 ``` * Когда a и b инициализируются со значением `257` в одной строке, интерпретатор Python создает новый объект, а затем одновременно ссылается на него во второй переменной. Если делать это в отдельных строках, интерпретатор не "знает", что объект `257` уже существует. * Это оптимизация компилятора и относится именно к интерактивной среде. Когда вы вводите две строки в интерпретаторе, они компилируются отдельно, поэтому оптимизируются отдельно. Если выполнить этот пример в файле `.py', поведение будет отличаться, потому что файл компилируется весь сразу. Эта оптимизация не ограничивается целыми числами, она работает и для других неизменяемых типов данных, таких как строки (проверьте пример "Строки - это сложно") и плавающие числа, ```py >>> a, b = 257.0, 257.0 >>> a is b True ``` * Почему это не сработало в Python 3.7? Абстрактная причина в том, что такие оптимизации компилятора зависят от реализации (т.е. могут меняться в зависимости от версии, ОС и т.д.). Я все еще выясняю, какое именно изменение реализации вызвало проблему, вы можете проверить этот [issue](https://github.com/satwikkansal/wtfpython/issues/100) для получения обновлений. --- ### ▶ Мистическое хэширование 1\. ```py some_dict = {} some_dict[5.5] = "JavaScript" some_dict[5.0] = "Ruby" some_dict[5] = "Python" ``` **Вывод:** ```py >>> some_dict[5.5] "JavaScript" >>> some_dict[5.0] # "Python" уничтожил "Ruby"? "Python" >>> some_dict[5] "Python" >>> complex_five = 5 + 0j >>> type(complex_five) complex >>> some_dict[complex_five] "Python" ``` Так почему же Python повсюду? #### 💡 Объяснение * Уникальность ключей в словаре Python определяется *эквивалентностью*, а не тождеством. Поэтому, даже если `5`, `5.0` и `5 + 0j` являются различными объектами разных типов, поскольку они равны, они не могут находиться в одном и том же `dict` (или `set`). Как только вы вставите любой из них, попытка поиска по любому другому, но эквивалентному ключу будет успешной с исходным сопоставленным значением (а не завершится ошибкой `KeyError`): ```py >>> 5 == 5.0 == 5 + 0j True >>> 5 is not 5.0 is not 5 + 0j True >>> some_dict = {} >>> some_dict[5.0] = "Ruby" >>> 5.0 in some_dict True >>> (5 in some_dict) and (5 + 0j in some_dict) True ``` * Это применимо и во время присваения значения элементу. Поэтому, в выражении `some_dict[5] = "Python"` Python находит существующий элемент с эквивалентным ключом `5.0 -> "Ruby"`, перезаписывает его значение на место, а исходный ключ оставляет в покое. ```py >>> some_dict {5.0: 'Ruby'} >>> some_dict[5] = "Python" >>> some_dict {5.0: 'Python'} ``` * Итак, как мы можем обновить ключ до `5` (вместо `5.0`)? На самом деле мы не можем сделать это обновление на месте, но что мы можем сделать, так это сначала удалить ключ (`del some_dict[5.0]`), а затем установить его (`some_dict[5]`), чтобы получить целое число `5` в качестве ключа вместо плавающего `5.0`, хотя это нужно в редких случаях. * Как Python нашел `5` в словаре, содержащем `5.0`? Python делает это за постоянное время без необходимости сканирования каждого элемента, используя хэш-функции. Когда Python ищет ключ `foo` в словаре, он сначала вычисляет `hash(foo)` (что выполняется в постоянном времени). Поскольку в Python требуется, чтобы объекты, которые сравниваются одинаково, имели одинаковое хэш-значение ([docs](https://docs.python.org/3/reference/datamodel.html#object.__hash__) здесь), `5`, `5.0` и `5 + 0j` имеют одинаковое хэш-значение. ```py >>> 5 == 5.0 == 5 + 0j True >>> hash(5) == hash(5.0) == hash(5 + 0j) True ``` **Примечание:** Обратное не обязательно верно: Объекты с одинаковыми хэш-значениями сами могут быть неравными. (Это вызывает так называемую [хэш-коллизию](https://en.wikipedia.org/wiki/Collision_(computer_science)) и ухудшает производительность постоянного времени, которую обычно обеспечивает хэширование). --- ### ▶ В глубине души мы все одинаковы. ```py class WTF: pass ``` **Вывод:** ```py >>> WTF() == WTF() # разные экземпляры класса не могут быть равны False >>> WTF() is WTF() # идентификаторы также различаются False >>> hash(WTF()) == hash(WTF()) # хэши тоже должны отличаться True >>> id(WTF()) == id(WTF()) True ``` #### 💡 Объяснение: * При вызове `id` Python создал объект класса `WTF` и передал его функции `id`. Функция `id` забирает свой `id` (местоположение в памяти) и выбрасывает объект. Объект уничтожается. * Когда мы делаем это дважды подряд, Python выделяет ту же самую область памяти и для второго объекта. Поскольку (в CPython) `id` использует участок памяти в качестве идентификатора объекта, идентификатор двух объектов одинаков. * Таким образом, id объекта уникален только во время жизни объекта. После уничтожения объекта или до его создания, другой объект может иметь такой же id. * Но почему выражение с оператором `is` равно `False`? Давайте посмотрим с помощью этого фрагмента. ```py class WTF(object): def __init__(self): print("I") def __del__(self): print("D") ``` **Вывод:** ```py >>> WTF() is WTF() I I D D False >>> id(WTF()) == id(WTF()) I D I D True ``` Как вы можете заметить, все дело в порядке уничтожения объектов. --- ### ▶ Беспорядок внутри порядка * ```py from collections import OrderedDict dictionary = dict() dictionary[1] = 'a'; dictionary[2] = 'b'; ordered_dict = OrderedDict() ordered_dict[1] = 'a'; ordered_dict[2] = 'b'; another_ordered_dict = OrderedDict() another_ordered_dict[2] = 'b'; another_ordered_dict[1] = 'a'; class DictWithHash(dict): """ A dict that also implements __hash__ magic. """ __hash__ = lambda self: 0 class OrderedDictWithHash(OrderedDict): """ An OrderedDict that also implements __hash__ magic. """ __hash__ = lambda self: 0 ``` **Вывод** ```py >>> dictionary == ordered_dict # a == b True >>> dictionary == another_ordered_dict # b == c True >>> ordered_dict == another_ordered_dict # почему же c != a ?? False # Мы все знаем, что множество состоит только из уникальных элементов, # давайте попробуем составить множество из этих словарей и посмотрим, что получится... >>> len({dictionary, ordered_dict, another_ordered_dict}) Traceback (most recent call last): File "", line 1, in TypeError: unhashable type: 'dict' # Логично, поскольку в словаре не реализовано магический метод __hash__, попробуем использовать # наши классы-обертки. >>> dictionary = DictWithHash() >>> dictionary[1] = 'a'; dictionary[2] = 'b'; >>> ordered_dict = OrderedDictWithHash() >>> ordered_dict[1] = 'a'; ordered_dict[2] = 'b'; >>> another_ordered_dict = OrderedDictWithHash() >>> another_ordered_dict[2] = 'b'; another_ordered_dict[1] = 'a'; >>> len({dictionary, ordered_dict, another_ordered_dict}) 1 >>> len({ordered_dict, another_ordered_dict, dictionary}) # changing the order 2 ``` Что здесь происходит? #### 💡 Объяснение: - Переходное (интрантизивное) равенство между `dictionary`, `ordered_dict` и `another_ordered_dict` не выполняется из-за реализации магического метода `__eq__` в классе `OrderedDict`. Перевод цитаты из [документации](https://docs.python.org/3/library/collections.html#ordereddict-objects) > Тесты равенства между объектами OrderedDict чувствительны к порядку и реализуются как `list(od1.items())==list(od2.items())`. Тесты на равенство между объектами `OrderedDict` и другими объектами Mapping нечувствительны к порядку, как обычные словари. - Причина такого поведения равенства в том, что оно позволяет напрямую подставлять объекты `OrderedDict` везде, где используется обычный словарь. - Итак, почему изменение порядка влияет на длину генерируемого объекта `set`? Ответ заключается только в отсутствии переходного равенства. Поскольку множества являются "неупорядоченными" коллекциями уникальных элементов, порядок вставки элементов не должен иметь значения. Но в данном случае он имеет значение. Давайте немного разберемся в этом, ```py >>> some_set = set() >>> some_set.add(dictionary) # используем объекты из фрагмента кода выше >>> ordered_dict in some_set True >>> some_set.add(ordered_dict) >>> len(some_set) 1 >>> another_ordered_dict in some_set True >>> some_set.add(another_ordered_dict) >>> len(some_set) 1 >>> another_set = set() >>> another_set.add(ordered_dict) >>> another_ordered_dict in another_set False >>> another_set.add(another_ordered_dict) >>> len(another_set) 2 >>> dictionary in another_set True >>> another_set.add(another_ordered_dict) >>> len(another_set) 2 ``` Таким образом, выражение `another_ordered_dict` в `another_set` равно `False`, потому что `ordered_dict` уже присутствовал в `another_set` и, как было замечено ранее, `ordered_dict == another_ordered_dict` равно `False`. --- ### ▶ Продолжай пытаться... * ```py def some_func(): try: return 'from_try' finally: return 'from_finally' def another_func(): for _ in range(3): try: continue finally: print("Finally!") def one_more_func(): # Попался! try: for i in range(3): try: 1 / i except ZeroDivisionError: # Вызовем исключение и обработаем его за пределами цикла raise ZeroDivisionError("A trivial divide by zero error") finally: print("Iteration", i) break except ZeroDivisionError as e: print("Zero division error occurred", e) ``` **Результат:** ```py >>> some_func() 'from_finally' >>> another_func() Finally! Finally! Finally! >>> 1 / 0 Traceback (most recent call last): File "", line 1, in ZeroDivisionError: division by zero >>> one_more_func() Iteration 0 ``` #### 💡 Объяснение: - Когда один из операторов `return`, `break` или `continue` выполняется в блоке `try` оператора "try...finally", на выходе также выполняется пункт `finally`. - Возвращаемое значение функции определяется последним выполненным оператором `return`. Поскольку блок `finally` выполняется всегда, оператор `return`, выполненный в блоке `finally`, всегда будет последним. - Предостережение - если в блоке `finally` выполняется оператор `return` или `break`, то временно сохраненное исключение отбрасывается. --- ### ▶ Для чего? ```py some_string = "wtf" some_dict = {} for i, some_dict[i] in enumerate(some_string): i = 10 ``` **Вывод:** ```py >>> some_dict # Словарь с индексами {0: 'w', 1: 't', 2: 'f'} ``` #### 💡 Объяснение: * Оператор `for` определяется в [грамматике Python](https://docs.python.org/3/reference/grammar.html) как: ``` for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] ``` Где `exprlist` - цель присваивания. Это означает, что эквивалент `{exprlist} = {next_value}` **выполняется для каждого элемента** в итерируемом объекте. Интересный пример, иллюстрирующий это: ```py for i in range(4): print(i) i = 10 ``` **Результат:** ``` 0 1 2 3 ``` Не ожидали, что цикл будет запущен только один раз? **💡 Объяснение:**. - Оператор присваивания `i = 10` никогда не влияет на итерации цикла из-за того, как циклы for работают в Python. Перед началом каждой итерации следующий элемент, предоставляемый итератором (в данном случае `range(4)`), распаковывается и присваивается переменной целевого списка (в данном случае `i`). * Функция `enumerate(some_string)` на каждой итерации выдает новое значение `i` (счетчик-инкремент) и символ из `some_string`. Затем она устанавливает (только что присвоенный) ключ `i` словаря `some_dict` на этот символ. Развертывание цикла можно упростить следующим образом: ```py >>> i, some_dict[i] = (0, 'w') >>> i, some_dict[i] = (1, 't') >>> i, some_dict[i] = (2, 'f') >>> some_dict ``` --- ### ▶ Расхождение во времени исполнения 1\. ```py array = [1, 8, 15] # Типичный генератор gen = (x for x in array if array.count(x) > 0) array = [2, 8, 22] ``` **Вывод:** ```py >>> print(list(gen)) # Куда подевались остальные значения? [8] ``` 2\. ```py array_1 = [1,2,3,4] gen_1 = (x for x in array_1) array_1 = [1,2,3,4,5] array_2 = [1,2,3,4] gen_2 = (x for x in array_2) array_2[:] = [1,2,3,4,5] ``` **Вывод:** ```py >>> print(list(gen_1)) [1, 2, 3, 4] >>> print(list(gen_2)) [1, 2, 3, 4, 5] ``` 3\. ```py array_3 = [1, 2, 3] array_4 = [10, 20, 30] gen = (i + j for i in array_3 for j in array_4) array_3 = [4, 5, 6] array_4 = [400, 500, 600] ``` **Вывод:** ```py >>> print(list(gen)) [401, 501, 601, 402, 502, 602, 403, 503, 603] ``` #### 💡 Объяснение - В выражении [генераторе](https://wiki.python.org/moin/Generators) условие `in` оценивается во время объявления, но условие `if` оценивается во время выполнения. - Перед выполнением кода, значение переменной `array` изменяется на список `[2, 8, 22]`, а поскольку из `1`, `8` и `15` только счетчик `8` больше `0`, генератор выдает только `8`. - Различия в выводе `g1` и `g2` во второй части связаны с тем, как переменным `array_1` и `array_2` присваиваются новые значения. - В первом случае `array_1` привязывается к новому объекту `[1,2,3,4,5]`, а поскольку `in` выражение исполняется во время объявления, оно по-прежнему ссылается на старый объект `[1,2,3,4]` (который не уничтожается). - Во втором случае присвоение среза `array_2` обновляет тот же старый объект `[1,2,3,4]` до `[1,2,3,4,5]`. Следовательно, и `g2`, и `array_2` по-прежнему имеют ссылку на один и тот же объект (который теперь обновлен до `[1,2,3,4,5]`). - Хорошо, следуя приведенной выше логике, не должно ли значение `list(gen)` в третьем фрагменте быть `[11, 21, 31, 12, 22, 32, 13, 23, 33]`? (потому что `array_3` и `array_4` будут вести себя так же, как `array_1`). Причина, по которой (только) значения `array_4` обновляются, объясняется в [PEP-289](https://www.python.org/dev/peps/pep-0289/#the-details) > Только крайнее for-выражение исполняется немедленно, остальные выражения откладываются до запуска генератора. --- ### ▶ `is not ...` не является `is (not ...)` ```py >>> 'something' is not None True >>> 'something' is (not None) False ``` #### 💡 Объяснение - `is not` является единым бинарным оператором, и его поведение отличается от раздельного использования `is` и `not`. - `is not` имеет значение `False`, если переменные по обе стороны оператора указывают на один и тот же объект, и `True` в противном случае. - В примере `(not None)` оценивается в `True`, поскольку значение `None` является `False` в булевом контексте, поэтому выражение становится `'something' is True`. --- ### ▶ Крестики-нолики, где X побеждает с первой попытки! ```py # Инициализируем переменную row row = [""] * 3 #row i['', '', ''] # Инициализируем игровую сетку board = [row] * 3 ``` **Результат:** ```py >>> board [['', '', ''], ['', '', ''], ['', '', '']] >>> board[0] ['', '', ''] >>> board[0][0] '' >>> board[0][0] = "X" >>> board [['X', '', ''], ['X', '', ''], ['X', '', '']] ``` Мы же не назначили три `"Х"`? #### 💡 Объяснение: Когда мы инициализируем переменную `row`, эта визуализация объясняет, что происходит в памяти ![image](/images/tic-tac-toe/after_row_initialized.png) А когда переменная `board` инициализируется путем умножения `row`, вот что происходит в памяти (каждый из элементов `board[0]`, `board[1]` и `board[2]` является ссылкой на тот же список, на который ссылается `row`) ![image](/images/tic-tac-toe/after_board_initialized.png) Мы можем избежать этого сценария, не используя переменную `row` для генерации `board`. (Подробнее в [issue](https://github.com/satwikkansal/wtfpython/issues/68)). ```py >>> board = [['']*3 for _ in range(3)] >>> board[0][0] = "X" >>> board [['X', '', ''], ['', '', ''], ['', '', '']] ``` --- ### ▶ Переменная Шредингера * ```py funcs = [] results = [] for x in range(7): def some_func(): return x funcs.append(some_func) results.append(some_func()) # обратите внимание на вызов функции funcs_results = [func() for func in funcs] ``` **Вывод (Python version):** ```py >>> results [0, 1, 2, 3, 4, 5, 6] >>> funcs_results [6, 6, 6, 6, 6, 6, 6] ``` Значения `x` были разными в каждой итерации до добавления `some_func` к `funcs`, но все функции возвращают `6`, когда они исполняются после завершения цикла. 2. ```py >>> powers_of_x = [lambda x: x**i for i in range(10)] >>> [f(2) for f in powers_of_x] [512, 512, 512, 512, 512, 512, 512, 512, 512, 512] ``` #### 💡 Объяснение: * При определении функции внутри цикла, которая использует переменную цикла в своем теле, цикл функции привязывается к *переменной*, а не к ее *значению*. Функция ищет `x` в окружающем контексте, а не использует значение `x` на момент создания функции. Таким образом, все функции используют для вычислений последнее значение, присвоенное переменной. Мы можем видеть, что используется `x` из глобального контекста (т.е. *не* локальная переменная): ```py >>> import inspect >>> inspect.getclosurevars(funcs[0]) ClosureVars(nonlocals={}, globals={'x': 6}, builtins={}, unbound=set()) ``` Так как `x` - глобальная переменная, можно изменить ее значение, которое будет использовано и возвращено из `funcs` ```py >>> x = 42 >>> [func() for func in funcs] [42, 42, 42, 42, 42, 42, 42] ``` * Чтобы получить желаемое поведение, вы можете передать переменную цикла как именованную переменную в функцию. **Почему это работает?** Потому что это определит переменную *внутри* области видимости функции. Она больше не будет обращаться к глобальной области видимости для поиска значения переменной, а создаст локальную переменную, которая будет хранить значение `x` в данный момент времени. ```py funcs = [] for x in range(7): def some_func(x=x): return x funcs.append(some_func) ``` **Вывод:** ```py >>> funcs_results = [func() for func in funcs] >>> funcs_results [0, 1, 2, 3, 4, 5, 6] ``` `x` больше не используется в глобальной области видимости ```py >>> inspect.getclosurevars(funcs[0]) ClosureVars(nonlocals={}, globals={}, builtins={}, unbound=set()) ``` --- ### ▶ Проблема курицы и яйца * 1\. ```py >>> isinstance(3, int) True >>> isinstance(type, object) True >>> isinstance(object, type) True ``` Так какой же базовый класс является "окончательным"? Кстати, это еще не все, 2\. ```py >>> class A: pass >>> isinstance(A, A) False >>> isinstance(type, type) True >>> isinstance(object, object) True ``` 3\. ```py >>> issubclass(int, object) True >>> issubclass(type, object) True >>> issubclass(object, type) False ``` #### 💡 Объяснение - `type` - это [метакласс](https://realpython.com/python-metaclasses/) в Python. - **Все** в Python является `объектом`, что включает в себя как классы, так и их объекты (экземпляры). - Класс `type` является метаклассом класса `object`, и каждый класс (включая `type`) наследуется прямо или косвенно от `object`. - У `object` и `type` нет реального базового класса. Путаница в приведенных выше фрагментах возникает потому, что мы думаем об этих отношениях (`issubclass` и `isinstance`) в терминах классов Python. Отношения между `object` и `type` не могут быть воспроизведены в чистом Python. Точнее говоря, следующие отношения не могут быть воспроизведены в чистом Python, + класс A является экземпляром класса B, а класс B является экземпляром класса A. + класс A является экземпляром самого себя. - Эти отношения между `object` и `type` (оба являются экземплярами друг друга, а также самих себя) существуют в Python из-за "обмана" на уровне реализации. --- ### ▶ Отношения между подклассами **Вывод:** ```py >>> from collections import Hashable >>> issubclass(list, object) True >>> issubclass(object, Hashable) True >>> issubclass(list, Hashable) False ``` Предполагается, что отношения подклассов должны быть транзитивными, верно? (т.е. если `A` является подклассом `B`, а `B` является подклассом `C`, то `A` _должен_ быть подклассом `C`) #### 💡 Объяснение * Отношения подклассов не обязательно являются транзитивными в Python. Можно переопределить магический метод `__subclasscheck__` в метаклассе. * Когда вызывается `issubclass(cls, Hashable)`, он просто ищет не-фальшивый метод "`__hash__`" в `cls` или во всем, от чего он наследуется. * Поскольку `object` является хэшируемым, а `list` - нехэшируемым, это нарушает отношение транзитивности. * Более подробное объяснение можно найти [здесь] (https://www.naftaliharris.com/blog/python-subclass-intransitivity/). --- ### ▶ Равенство и тождество методов 1. ```py class SomeClass: def method(self): pass @classmethod def classm(cls): pass @staticmethod def staticm(): pass ``` **Результат:** ```py >>> print(SomeClass.method is SomeClass.method) True >>> print(SomeClass.classm is SomeClass.classm) False >>> print(SomeClass.classm == SomeClass.classm) True >>> print(SomeClass.staticm is SomeClass.staticm) True ``` Обращаясь к `classm` дважды, мы получаем одинаковый объект, но не *тот же самый*? Давайте посмотрим, что происходит с экземплярами `SomeClass`: 2. ```py o1 = SomeClass() o2 = SomeClass() ``` **Вывод:** ```py >>> print(o1.method == o2.method) False >>> print(o1.method == o1.method) True >>> print(o1.method is o1.method) False >>> print(o1.classm is o1.classm) False >>> print(o1.classm == o1.classm == o2.classm == SomeClass.classm) True >>> print(o1.staticm is o1.staticm is o2.staticm is SomeClass.staticm) True ``` Повторный доступ к `классу` или `методу` создает одинаковые, но не *те же самые* объекты для одного и того же экземпляра `какого-либо класса`. #### 💡 Объяснение * Функции являются [дескрипторами](https://docs.python.org/3/howto/descriptor.html). Всякий раз, когда к функции обращаются как к атрибута, вызывается дескриптор, создавая объект метода, который "связывает" функцию с объектом, владеющим атрибутом. При вызове метод вызывает функцию, неявно передавая связанный объект в качестве первого аргумента (именно так мы получаем `self` в качестве первого аргумента, несмотря на то, что не передаем его явно). ```py >>> o1.method > ``` * При многократном обращении к атрибуту каждый раз создается объект метода! Поэтому `o1.method is o1.method` всегда ложно. Однако доступ к функциям как к атрибутам класса (в отличие от экземпляра) не создает методов; поэтому `SomeClass.method is SomeClass.method` является истинным. ```py >>> SomeClass.method ``` * `classmethod` преобразует функции в методы класса. Методы класса - это дескрипторы, которые при обращении к ним создают объект метода, который связывает *класс* (тип) объекта, а не сам объект. ```py >>> o1.classm > ``` * В отличие от функций, `classmethod` будет создавать метод и при обращении к нему как к атрибуту класса (в этом случае они привязываются к классу, а не к его типу). Поэтому `SomeClass.classm is SomeClass.classm` является ошибочным. ```py >>> SomeClass.classm > ``` * Объект-метод равен, если обе функции равны, а связанные объекты одинаковы. Поэтому `o1.method == o1.method` является истинным, хотя и не является одним и тем же объектом в памяти. * `staticmethod` преобразует функции в дескриптор "no-op", который возвращает функцию как есть. Методы-объекты никогда не создается, поэтому сравнение с `is` является истинным. ```py >>> o1.staticm >>> SomeClass.staticm ``` * Необходимость создавать новые объекты "метод" каждый раз, когда Python вызывает методы экземпляра, и необходимость изменять аргументы каждый раз, чтобы вставить `self`, сильно сказывается на производительности. CPython 3.7 [решил эту проблему](https://bugs.python.org/issue26110), введя новые опкоды, которые работают с вызовом методов без создания временных объектов методов. Это используется только при фактическом вызове функции доступа, так что приведенные здесь фрагменты не затронуты и по-прежнему генерируют методы :) --- ### ▶ All-true-ation (непереводимая игра слов) * ```py >>> all([True, True, True]) True >>> all([True, True, False]) False >>> all([]) True >>> all([[]]) False >>> all([[[]]]) True ``` Почему это изменение True-False? #### 💡 Объяснение: - Реализация функции `all`: - ```py def all(iterable): for element in iterable: if not element: return False return True ``` - `all([])` возвращает `True`, поскольку итерируемый массив пуст. - `all([[]])` возвращает `False`, поскольку переданный массив имеет один элемент, `[]`, а в python пустой список является ложным. - `all([[[[]]])` и более высокие рекурсивные варианты всегда `True`. Это происходит потому, что единственный элемент переданного массива (`[[...]]`) уже не пуст, а списки со значениями являются истинными. --- ### ▶ Неожиданная запятая **Вывод (< 3.6):** ```py >>> def f(x, y,): ... print(x, y) ... >>> def g(x=4, y=5,): ... print(x, y) ... >>> def h(x, **kwargs,): File "", line 1 def h(x, **kwargs,): ^ SyntaxError: invalid syntax >>> def h(*args,): File "", line 1 def h(*args,): ^ SyntaxError: invalid syntax ``` #### 💡 Объяснение: - Запятая в конце списка аргументов функции Python не всегда законна. - В Python список аргументов определяется частично с помощью ведущих запятых, а частично с помощью запятых в конце списка. Этот конфликт приводит к ситуациям, когда запятая оказывается в середине, и ни одно из правил не выполняется. - **Примечание:** Проблема с запятыми в конце списка аргументов [исправлена в Python 3.6](https://bugs.python.org/issue9232). Варианты использования запятых в конце выражения приведены в [обсуждении](https://bugs.python.org/issue9232#msg248399). --- ### ▶ Строки и обратные слэши **Вывод:** ```py >>> print("\"") " >>> print(r"\"") \" >>> print(r"\") File "", line 1 print(r"\") ^ SyntaxError: EOL while scanning string literal >>> r'\'' == "\\'" True ``` #### 💡 Объяснение - В обычной строке обратная слэш используется для экранирования символов, которые могут иметь специальное значение (например, одинарная кавычка, двойная кавычка и сам обратный слэш). ```py >>> "wt\"f" 'wt"f' ``` - В необработанном строковом литерале (на что указывает префикс `r`) обратный слэш передается как есть, вместе с поведением экранирования следующего символа. ```py >>> r'wt\"f' == 'wt\\"f' True >>> print(repr(r'wt\"f') 'wt\\"f' >>> print("\n") >>> print(r"\\n") '\\n' ``` - Это означает, что когда синтаксический анализатор встречает обратный слэш в необработанной строке, он ожидает, что за ней последует другой символ. А в нашем случае (`print(r"\")`) обратная слэш экранирует двойную кавычку, оставив парсер без завершающей кавычки (отсюда `SyntaxError`). Вот почему обратный слеш не работает в конце необработанной строки. -- ### ▶ Не узел! (eng. not knot!) ```py x = True y = False ``` **Результат:** ```py >>> not x == y True >>> x == not y File "", line 1 x == not y ^ SyntaxError: invalid syntax ``` #### 💡 Объяснение * Старшинство операторов влияет на выполнение выражения, и оператор `==` имеет более высокий приоритет, чем оператор `not` в Python. * Поэтому `not x == y` эквивалентно `not (x == y)`, что эквивалентно `not (True == False)`, в итоге равное `True`. * Но `x == not y` вызывает `SyntaxError`, потому что его можно считать эквивалентным `(x == not) y`, а не `x == (not y)`, что можно было бы ожидать на первый взгляд. * Парсер ожидал, что ключевое слово `not` будет частью оператора `not in` (потому что оба оператора `==` и `not in` имеют одинаковый приоритет), но после того, как он не смог найти ключевое слово `in`, следующее за `not`, он выдает `SyntaxError`. --- ### ▶ Строки наполовину в тройных кавычках **Вывод:** ```py >>> print('wtfpython''') wtfpython >>> print("wtfpython""") wtfpython >>> # Выражения ниже приводят к `SyntaxError` >>> # print('''wtfpython') >>> # print("""wtfpython") File "", line 3 print("""wtfpython") ^ SyntaxError: EOF while scanning triple-quoted string literal ``` #### 💡 Объяснение: + Python поддерживает неявную [конкатенацию строковых литералов](https://docs.python.org/3/reference/lexical_analysis.html#string-literal-concatenation), Пример, ``` >>> print("wtf" "python") wtfpython >>> print("wtf" "") # or "wtf""" wtf ``` + `'''` и `"""` также являются разделителями строк в Python, что вызывает SyntaxError, поскольку интерпретатор Python ожидал завершающую тройную кавычку в качестве разделителя при сканировании текущего встреченного строкового литерала с тройной кавычкой. --- ### ▶ Что не так с логическими значениями? 1\. ```py # Простой пример счетчика логических переменных и целых чисел # в итерируемом объекте со значениями разных типов данных mixed_list = [False, 1.0, "some_string", 3, True, [], False] integers_found_so_far = 0 booleans_found_so_far = 0 for item in mixed_list: if isinstance(item, int): integers_found_so_far += 1 elif isinstance(item, bool): booleans_found_so_far += 1 ``` **Результат:** ```py >>> integers_found_so_far 4 >>> booleans_found_so_far 0 ``` 2\. ```py >>> some_bool = True >>> "wtf" * some_bool 'wtf' >>> some_bool = False >>> "wtf" * some_bool '' ``` 3\. ```py def tell_truth(): True = False if True == False: print("I have lost faith in truth!") ``` **Результат (< 3.x):** ```py >>> tell_truth() I have lost faith in truth! ``` #### 💡 Объяснение: * `bool` это подкласс класса `int` в Python ```py >>> issubclass(bool, int) True >>> issubclass(int, bool) False ``` * `True` и `False` - экземпляры класса `int` ```py >>> isinstance(True, int) True >>> isinstance(False, int) True ``` * Целочисленное значение `True` равно `1`, а `False` равно `0`. ```py >>> int(True) 1 >>> int(False) 0 ``` * Объяснение на [StackOverflow](https://stackoverflow.com/a/8169049/4354153). * Изначально в Python не было типа `bool` (использовали 0 для false и ненулевое значение 1 для true). В версиях 2.x были добавлены `True`, `False` и тип `bool`, но для обратной совместимости `True` и `False` нельзя было сделать константами. Они просто были встроенными переменными, и их можно было переназначить. * Python 3 был несовместим с предыдущими версиями, эту проблему наконец-то исправили, и поэтому последний фрагмент не будет работать с Python 3.x! --- ### ▶ Атрибуты класса и экземпляра 1\. ```py class A: x = 1 class B(A): pass class C(A): pass ``` **Результат:** ```py >>> A.x, B.x, C.x (1, 1, 1) >>> B.x = 2 >>> A.x, B.x, C.x (1, 2, 1) >>> A.x = 3 >>> A.x, B.x, C.x # Значение C.x изменилось , но B.x - нет (3, 2, 3) >>> a = A() >>> a.x, A.x (3, 3) >>> a.x += 1 >>> a.x, A.x (4, 3) ``` 2\. ```py class SomeClass: some_var = 15 some_list = [5] another_list = [5] def __init__(self, x): self.some_var = x + 1 self.some_list = self.some_list + [x] self.another_list += [x] ``` **Результат:** ```py >>> some_obj = SomeClass(420) >>> some_obj.some_list [5, 420] >>> some_obj.another_list [5, 420] >>> another_obj = SomeClass(111) >>> another_obj.some_list [5, 111] >>> another_obj.another_list [5, 420, 111] >>> another_obj.another_list is SomeClass.another_list True >>> another_obj.another_list is some_obj.another_list True ``` #### 💡 Объяснение: * Переменные класса и переменные экземпляров класса внутренне обрабатываются как словари объекта класса. Если имя переменной не найдено в словаре текущего класса, оно ищется в родительских классах. * Оператор += изменяет объект на месте, не создавая новый объект. Таким образом, изменение атрибута одного экземпляра влияет на другие экземпляры и атрибут класса также. --- ### ▶ Возврат None из генератора ```py some_iterable = ('a', 'b') def some_func(val): return "something" ``` **Результат (<= 3.7.x):** ```py >>> [x for x in some_iterable] ['a', 'b'] >>> [(yield x) for x in some_iterable] at 0x7f70b0a4ad58> >>> list([(yield x) for x in some_iterable]) ['a', 'b'] >>> list((yield x) for x in some_iterable) ['a', None, 'b', None] >>> list(some_func((yield x)) for x in some_iterable) ['a', 'something', 'b', 'something'] ``` #### 💡 Объяснение: - Это баг в обработке yield в генераторах и списочных выражениях CPython. - Исходный код и объяснение можно найти [здесь](https://stackoverflow.com/questions/32139885/yield-in-list-comprehensions-and-generator-expressions) - Связанный [отчет об ошибке](https://bugs.python.org/issue10544) - В Python 3.8+ yield внутри списочных выражений больше не допускается и выдает `SyntaxError`. --- ### ▶ Yield from возвращает... * 1\. ```py def some_func(x): if x == 3: return ["wtf"] else: yield from range(x) ``` **Результат (> 3.3):** ```py >>> list(some_func(3)) [] ``` Куда исчезло `"wtf"`? Это связано с каким-то особым эффектом `yield from`? Проверим это. 2\. ```py def some_func(x): if x == 3: return ["wtf"] else: for i in range(x): yield i ``` **Результат:** ```py >>> list(some_func(3)) [] ``` То же самое, это тоже не сработало. Что происходит? #### 💡 Объяснение: + С Python 3.3 стало возможным использовать оператор `return` в генераторах с возвращением значения (см. [PEP380](https://www.python.org/dev/peps/pep-0380/)). В [официальной документации](https://www.python.org/dev/peps/pep-0380/#enhancements-to-stopiteration) говорится, что > "... `return expr` в генераторе вызывает исключение `StopIteration(expr)` при выходе из генератора." + В случае `some_func(3)` `StopIteration` возникает в начале из-за оператора `return`. Исключение `StopIteration` автоматически перехватывается внутри обертки `list(...)` и цикла `for`. Поэтому два вышеприведенных фрагмента приводят к пустому списку. + Чтобы получить `["wtf"]` из генератора `some_func`, нужно перехватить исключение `StopIteration`. ```py try: next(some_func(3)) except StopIteration as e: some_string = e.value ``` ```py >>> some_string ["wtf"] ``` --- ### ▶ Nan-рефлексивность * 1\. ```py a = float('inf') b = float('nan') c = float('-iNf') # Эти строки не чувствительны к регистру d = float('nan') ``` **Результат:** ```py >>> a inf >>> b nan >>> c -inf >>> float('some_other_string') ValueError: could not convert string to float: some_other_string >>> a == -c # inf==inf True >>> None == None # None == None True >>> b == d # но nan!=nan False >>> 50 / a 0.0 >>> a / a nan >>> 23 + b nan ``` 2\. ```py >>> x = float('nan') >>> y = x / x >>> y is y # идендичность сохраняется True >>> y == y # сравнение ложно для y False >>> [y] == [y] # но сравнение истинно для списка, содержащего y True ``` #### 💡 Объяснение: - `'inf'` и `'nan'` - это специальные строки (без учета регистра), которые при явном приведении к типу `float` используются для представления математической "бесконечности" и "не число" соответственно. - Согласно стандартам IEEE `NaN != NaN`, но соблюдение этого правила нарушает предположение о рефлексивности элемента коллекции в Python, то есть если `x` является частью коллекции, такой как `list`, реализации, методы сравнения предполагают, что `x == x`. Поэтому при сравнении элементов сначала сравниваются их идентификаторы (так как это быстрее), а значения сравниваются только при несовпадении идентификаторов. Следующий фрагмент сделает вещи более ясными: ```py >>> x = float('nan') >>> x == x, [x] == [x] (False, True) >>> y = float('nan') >>> y == y, [y] == [y] (False, True) >>> x == y, [x] == [y] (False, False) ``` Поскольку идентификаторы `x` и `y` разные, рассматриваются значения, которые также различаются; следовательно, на этот раз сравнение возвращает `False`. - Интересное чтение: [Рефлексивность и другие основы цивилизации](https://bertrandmeyer.com/2010/02/06/reflexivity-and-other-pillars-of-civilization/) --- ### ▶ Мутируем немутируемое! Это может показаться тривиальным, если вы знаете, как работают ссылки в Python. ```py some_tuple = ("A", "tuple", "with", "values") another_tuple = ([1, 2], [3, 4], [5, 6]) ``` **Результат:** ```py >>> some_tuple[2] = "change this" TypeError: 'tuple' object does not support item assignment >>> another_tuple[2].append(1000) # Не приводит к исключениям >>> another_tuple ([1, 2], [3, 4], [5, 6, 1000]) >>> another_tuple[2] += [99, 999] TypeError: 'tuple' object does not support item assignment >>> another_tuple ([1, 2], [3, 4], [5, 6, 1000, 99, 999]) ``` Но кортежи неизменяемы... Что происходит? #### 💡 Объяснение: * Перевод цитаты из [документации](https://docs.python.org/3/reference/datamodel.html) > Неизменяемые последовательности Объект неизменяемого типа последовательности не может измениться после создания. (Если объект содержит ссылки на другие объекты, эти объекты могут быть изменяемыми и могут быть изменены; однако набор объектов, на которые непосредственно ссылается неизменяемый объект, не может изменяться.) * Оператор `+=` изменяет список на месте. Присваивание элемента не работает, но когда возникает исключение, элемент уже был изменен на месте. * Также есть объяснение в официальном [Python FAQ](https://docs.python.org/3/faq/programming.html#why-does-a-tuple-i-item-raise-an-exception-when-the-addition-works). --- ### ▶ Исчезающая переменная из внешней области видимости ```py e = 7 try: raise Exception() except Exception as e: pass ``` **Результат (Python 2.x):** ```py >>> print(e) # Ничего не выводит ``` **Результат (Python 3.x):** ```py >>> print(e) NameError: name 'e' is not defined ``` #### 💡 Объяснение: * [Источник](https://docs.python.org/3/reference/compound_stmts.html#except) Когда исключение было назначено с помощью ключевого слова `as`, оно очищается в конце блока `except`. Это происходит так, как если бы ```py except E as N: foo ``` разворачивалось до ```py except E as N: try: foo finally: del N ``` Это означает, что исключению должно быть присвоено другое имя, чтобы на него можно было ссылаться после завершения блока `except`. Исключения очищаются, потому что с прикрепленным к ним трейсбэком они образуют цикл ссылок со стеком вызовов, сохраняя все локальные объекты в этой стэке до следующей сборки мусора. * В Python clauses не имеют области видимости. В примере все объекты в одной области видимости, а переменная `e` была удалена из-за выполнения блока `except`. Этого нельзя сказать о функциях, которые имеют отдельные внутренние области видимости. Пример ниже иллюстрирует это: ```py def f(x): del(x) print(x) x = 5 y = [5, 4, 3] ``` **Результат:** ```py >>> f(x) UnboundLocalError: local variable 'x' referenced before assignment >>> f(y) UnboundLocalError: local variable 'x' referenced before assignment >>> x 5 >>> y [5, 4, 3] ``` * В Python 2.x, имя переменной `e` назначается на экземпляр `Exception()`, и при попытки вывести значение `e` ничего не выводится. **Результат (Python 2.x):** ```py >>> e Exception() >>> print e # Ничего не выводится! ``` --- ### ▶ Загадочное преобразование типов ключей ```py class SomeClass(str): pass some_dict = {'s': 42} ``` **Результат:** ```py >>> type(list(some_dict.keys())[0]) str >>> s = SomeClass('s') >>> some_dict[s] = 40 >>> some_dict # Ожидается 2 разные пары ключ-значение {'s': 40} >>> type(list(some_dict.keys())[0]) str ``` #### 💡 Объяснение: * И объект `s`, и строка `"s"` хэшируются до одного и того же значения, потому что `SomeClass` наследует метод `__hash__` класса `str`. * Выражение `SomeClass("s") == "s"` эквивалентно `True`, потому что `SomeClass` также наследует метод `__eq__` класса `str`. * Поскольку оба объекта хэшируются на одно и то же значение и равны, они представлены одним и тем же ключом в словаре. * Чтобы добиться желаемого поведения, мы можем переопределить метод `__eq__` в `SomeClass`. ```py class SomeClass(str): def __eq__(self, other): return ( type(self) is SomeClass and type(other) is SomeClass and super().__eq__(other) ) # При переопределении метода __eq__, Python прекращает автоматическое наследование метода # __hash__, поэтому его нужно вручную определить __hash__ = str.__hash__ some_dict = {'s':42} ``` **Результат:** ```py >>> s = SomeClass('s') >>> some_dict[s] = 40 >>> some_dict {'s': 40, 's': 42} >>> keys = list(some_dict.keys()) >>> type(keys[0]), type(keys[1]) (__main__.SomeClass, str) ``` --- ### ▶ Посмотрим, сможете ли вы угадать что здесь? ```py a, b = a[b] = {}, 5 ``` **Результат:** ```py >>> a {5: ({...}, 5)} ``` #### 💡 Объяснение: * Согласно [документации](https://docs.python.org/3/reference/simple_stmts.html#assignment-statements), выражения присваивания имеют вид ``` (target_list "=")+ (expression_list | yield_expression) ``` и > Оператор присваивания исполняет список выражений (помните, что это может быть одно выражение или список, разделенный запятыми, в последнем случае получается кортеж) и присваивает единственный результирующий объект каждому из целевых списков, слева направо. * `+` в `(target_list "=")+` означает, что может быть **один или более** целевых списков. В данном случае целевыми списками являются `a, b` и `a[b]` (обратите внимание, что список выражений ровно один, в нашем случае это `{}, 5`). * После исполнения списка выражений его значение распаковывается в целевые списки **слева направо**. Так, в нашем случае сначала кортеж `{}, 5` распаковывается в `a, b`, и теперь у нас есть `a = {}` и `b = 5`. * Теперь `a` имеет значение `{}`, которое является изменяемым объектом. * Вторым целевым списком является `a[b]` (вы можете ожидать, что это вызовет ошибку, поскольку `a` и `b` не были определены в предыдущих утверждениях. Но помните, мы только что присвоили `a` значение `{}` и `b` - `5`). * Теперь мы устанавливаем ключ `5` в словаре в кортеж `({}, 5)`, создавая круговую ссылку (`{...}` в выводе ссылается на тот же объект, на который уже ссылается `a`). Другим более простым примером круговой ссылки может быть ```py >>> some_list [[...]] >>> some_list[0] [[...]] >>> some_list is some_list[0] True >>> some_list[0][0][0][0][0][0] == some_list True ``` Аналогичный случай в примере выше (`a[b][0]` - это тот же объект, что и `a`) * Подводя итог, можно разбить пример на следующие пункты ```py a, b = {}, 5 a[b] = a, b ``` А циклическая ссылка может быть оправдана тем, что `a[b][0]` - тот же объект, что и `a` ```py >>> a[b][0] is a True ``` --- ### ▶ Превышение предела целочисленного преобразования строк ```py >>> # Python 3.10.6 >>> int("2" * 5432) >>> # Python 3.10.8 >>> int("2" * 5432) ``` **Вывод:** ```py >>> # Python 3.10.6 222222222222222222222222222222222222222222222222222222222222222... >>> # Python 3.10.8 Traceback (most recent call last): ... ValueError: Exceeds the limit (4300) for integer string conversion: value has 5432 digits; use sys.set_int_max_str_digits() to increase the limit. ``` #### 💡 Объяснение: Этот вызов `int()` прекрасно работает в Python 3.10.6 и вызывает ошибку `ValueError` в Python 3.10.8, 3.11. Обратите внимание, что Python все еще может работать с большими целыми числами. Ошибка возникает только при преобразовании между целыми числами и строками. К счастью, вы можете увеличить предел допустимого количества цифр. Для этого можно воспользоваться одним из следующих способов: - `-X int_max_str_digits` - флаг командной строкиcommand-line flag - `set_int_max_str_digits()` - функция из модуля `sys` - `PYTHONINTMAXSTRDIGITS` - переменная окружения [Смотри документацию](https://docs.python.org/3/library/stdtypes.html#int-max-str-digits) для получения более подробной информации об изменении лимита по умолчанию, если вы ожидаете, что ваш код превысит это значение. --- ## Секция: Скользкие склоны ### ▶ Изменение словаря во время прохода по нему ```py x = {0: None} for i in x: del x[i] x[i+1] = None print(i) ``` **Результат (Python 2.7- Python 3.5):** ``` 0 1 2 3 4 5 6 7 ``` Да, цикл выполняет ровно **восемь** итераций и завершается. #### 💡 Объяснение: * Проход по словарю и его одновременное редактирование не поддерживается. * Выполняется восемь проходов, потому что именно в этот момент словарь изменяет размер, чтобы вместить больше ключей (у нас есть восемь записей об удалении, поэтому необходимо изменить размер). На самом деле это деталь реализации. * То, как обрабатываются удаленные ключи и когда происходит изменение размера, может отличаться в разных реализациях Python. * Так что для версий Python, отличных от Python 2.7 - Python 3.5, количество записей может отличаться от 8 (но каким бы ни было количество записей, оно будет одинаковым при каждом запуске). Обсуждения по этому поводу имеются в [issue](https://github.com/satwikkansal/wtfpython/issues/53) и на [StackOverflow](https://stackoverflow.com/questions/44763802/bug-in-python-dict). * В Python 3.7.6 и выше при попытке запустить пример вызывается исключение `RuntimeError: dictionary keys changed during iteration`. --- ### ▶ Упрямая операция `del` ```py class SomeClass: def __del__(self): print("Deleted!") ``` **Результат:** 1\. ```py >>> x = SomeClass() >>> y = x >>> del x # должно быть выведено "Deleted!" >>> del y Deleted! ``` Фух, наконец-то удалили. Вы, наверное, догадались, что спасло `__del__` от вызова в нашей первой попытке удалить `x`. Давайте добавим в пример еще больше изюминок. 2\. ```py >>> x = SomeClass() >>> y = x >>> del x >>> y # проверяем, существует ли y <__main__.SomeClass instance at 0x7f98a1a67fc8> >>> del y # Как и в прошлом примере, вывод должен содержать "Deleted!" >>> globals() # но вывод пуст. Проверим все глобальные переменные Deleted! {'__builtins__': , 'SomeClass': , '__package__': None, '__name__': '__main__', '__doc__': None} ``` Вот сейчас переменная `y` удалена :confused: #### 💡 Объяснение: + `del x` не вызывает напрямую `x.__del__()`. + Когда встречается `del x`, Python удаляет имя `x` из текущей области видимости и уменьшает на 1 количество ссылок на объект, на который ссылается `x`. `__del__()` вызывается только тогда, когда счетчик ссылок объекта достигает нуля. + Во втором фрагменте вывода `__del__()` не была вызвана, потому что предыдущий оператор (`>>> y`) в интерактивном интерпретаторе создал еще одну ссылку на тот же объект (в частности, магическую переменную `_`, которая ссылается на значение результата последнего не `None` выражения в REPL), тем самым не позволив счетчику ссылок достичь нуля, когда было встречено `del y`. + Вызов `globals` (или вообще выполнение чего-либо, что будет иметь результат, отличный от `None`) заставил `_` сослаться на новый результат, отбросив существующую ссылку. Теперь количество ссылок достигло 0, и мы можем видеть, как выводится "Deleted!" (наконец-то!). --- ### ▶ Переменная за пределами видимости 1\. ```py a = 1 def some_func(): return a def another_func(): a += 1 return a ``` 2\. ```py def some_closure_func(): a = 1 def some_inner_func(): return a return some_inner_func() def another_closure_func(): a = 1 def another_inner_func(): a += 1 return a return another_inner_func() ``` **Результат:** ```py >>> some_func() 1 >>> another_func() UnboundLocalError: local variable 'a' referenced before assignment >>> some_closure_func() 1 >>> another_closure_func() UnboundLocalError: local variable 'a' referenced before assignment ``` #### 💡 Объяснение: * Когда вы делаете присваивание переменной в области видимости, она становится локальной для этой области. Так `a` становится локальной для области видимости `another_func`, но она не была инициализирована ранее в той же области видимости, что приводит к ошибке. * Для изменения переменной `a` из внешней области видимости внутри функции `another_func`, необходимо использовать ключевое слово `global`. ```py def another_func() global a a += 1 return a ``` **Результат:** ```py >>> another_func() 2 ``` * В `another_closure_func` переменная `a` становится локальной для области видимости `another_inner_func`, но она не была инициализирована ранее в той же области видимости, поэтому выдает ошибку. * Чтобы изменить переменную внешней области видимости `a` в `another_inner_func`, используйте ключевое слово `nonlocal`. Утверждение nonlocal используется для обращения к переменным, определенным в ближайшей внешней (за исключением глобальной) области видимости. ```py def another_func(): a = 1 def another_inner_func(): nonlocal a a += 1 return a return another_inner_func() ``` **Результат:** ```py >>> another_func() 2 ``` * Ключевые слова `global` и `nonlocal` указывают интерпретатору python не объявлять новые переменные и искать их в соответствующих внешних областях видимости. * Прочитайте [это](https://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html) короткое, но потрясающее руководство, чтобы узнать больше о том, как работают пространства имен и разрешение областей видимости в Python. --- ### ▶ Удаление элемента списка во время прохода по списку ```py list_1 = [1, 2, 3, 4] list_2 = [1, 2, 3, 4] list_3 = [1, 2, 3, 4] list_4 = [1, 2, 3, 4] for idx, item in enumerate(list_1): del item for idx, item in enumerate(list_2): list_2.remove(item) for idx, item in enumerate(list_3[:]): list_3.remove(item) for idx, item in enumerate(list_4): list_4.pop(idx) ``` **Результат:** ```py >>> list_1 [1, 2, 3, 4] >>> list_2 [2, 4] >>> list_3 [] >>> list_4 [2, 4] ``` Есть предположения, почему вывод `[2, 4]`? #### 💡 Объяснение: * Никогда не стоит изменять объект, над которым выполняется итерация. Правильным способом будет итерация по копии объекта, и `list_3[:]` делает именно это. ```py >>> some_list = [1, 2, 3, 4] >>> id(some_list) 139798789457608 >>> id(some_list[:]) # Notice that python creates new object for sliced list. 139798779601192 ``` **Разница между `del`, `remove` и `pop`:** * `del var_name` просто удаляет привязку `var_name` из локального или глобального пространства имен (поэтому `list_1` не затрагивается). * `remove` удаляет первое подходящее значение, а не конкретный индекс, вызывает `ValueError`, если значение не найдено. * `pop` удаляет элемент по определенному индексу и возвращает его, вызывает `IndexError`, если указан неверный индекс. **Почему на выходе получается `[2, 4]`? - Проход по списку выполняется индекс за индексом, и когда мы удаляем `1` из `list_2` или `list_4`, содержимое списков становится `[2, 3, 4]`. Оставшиеся элементы сдвинуты вниз, то есть `2` находится на индексе 0, а `3` - на индексе 1. Поскольку на следующей итерации будет просматриваться индекс 1 (который и есть `3`), `2` будет пропущен полностью. Аналогичное произойдет с каждым альтернативным элементом в последовательности списка. * Объяснение примера можно найти на [StackOverflow](https://stackoverflow.com/questions/45946228/what-happens-when-you-try-to-delete-a-list-element-while-iterating-over-it). * Также посмотрите на похожий пример на [StackOverflow](https://stackoverflow.com/questions/45877614/how-to-change-all-the-dictionary-keys-in-a-for-loop-with-d-items), связанный со словарями. --- ### ▶ Сжатие итераторов с потерями * ```py >>> numbers = list(range(7)) >>> numbers [0, 1, 2, 3, 4, 5, 6] >>> first_three, remaining = numbers[:3], numbers[3:] >>> first_three, remaining ([0, 1, 2], [3, 4, 5, 6]) >>> numbers_iter = iter(numbers) >>> list(zip(numbers_iter, first_three)) [(0, 0), (1, 1), (2, 2)] # пока все хорошо, сожмем оставшуюся часть итератора >>> list(zip(numbers_iter, remaining)) [(4, 3), (5, 4), (6, 5)] ``` Куда пропал элемент `3` из списка `numbers`? #### 💡 Объяснение: - Согласно [документации](https://docs.python.org/3.12/library/functions.html#zip), примерная реализация функции `zip` выглядит так, ```py def zip(*iterables): sentinel = object() iterators = [iter(it) for it in iterables] while iterators: result = [] for it in iterators: elem = next(it, sentinel) if elem is sentinel: return result.append(elem) yield tuple(result) ``` - Таким образом, функция принимает произвольное количество итерируемых объектов, добавляет каждый из их элементов в список `result`, вызывая для них функцию `next`, и останавливается всякий раз, когда любой из итерируемых объектов исчерпывается. - Нюанс заключается в том, что при исчерпании любого итерируемого объекта существующие элементы в списке `result` отбрасываются. Именно это произошло с `3` в `numbers_iter`. - Правильный способ выполнения вышеописанных действий с помощью `zip` будет следующим, ```py >>> numbers = list(range(7)) >>> numbers_iter = iter(numbers) >>> list(zip(first_three, numbers_iter)) [(0, 0), (1, 1), (2, 2)] >>> list(zip(remaining, numbers_iter)) [(3, 3), (4, 4), (5, 5), (6, 6)] ``` Первый аргумент сжатия должен иметь наименьшее число элементов --- ### ▶ Утечка переменных внутри цикла 1\. ```py for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global') ``` **Вывод:** ```py 6 : for x inside loop 6 : x in global ``` Но `x` не была определена за пределами цикла `for`... 2\. ```py # В этот раз определим x до цикла x = -1 for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global') ``` **Вывод:** ```py 6 : for x inside loop 6 : x in global ``` 3\. **Вывод (Python 2.x):** ```py >>> x = 1 >>> print([x for x in range(5)]) [0, 1, 2, 3, 4] >>> print(x) 4 ``` **Вывод (Python 3.x):** ```py >>> x = 1 >>> print([x for x in range(5)]) [0, 1, 2, 3, 4] >>> print(x) 1 ``` #### 💡 Объяснение: - В Python циклы for используют область видимости, в которой они существуют, и оставляют свою определенную переменную цикла после завершения. Это также относится к случаям, когда мы явно определили переменную цикла for в глобальном пространстве имен. В этом случае будет произведена перепривязка существующей переменной. - Различия в выводе интерпретаторов Python 2.x и Python 3.x для примера с пониманием списков можно объяснить следующим изменением, задокументированным в журнале изменений [What's New In Python 3.0](https://docs.python.org/3/whatsnew/3.0.html): > "Генераторы списков ("list comprehensions") больше не поддерживает синтаксическую форму `[... for var in item1, item2, ...]`. Вместо этого используйте `[... for var in (item1, item2, ...)]`. Кроме того, обратите внимание, что генераторы списков имеют другую семантику: они ближе к синтаксическому сахару для генераторного выражения внутри конструктора `list()`, и, в частности, управляющие переменные цикла больше не просачиваются в окружающую область видимости." --- ### ▶ Остерегайтесь изменяемых аргументов по умолчанию! ```py def some_func(default_arg=[]): default_arg.append("some_string") return default_arg ``` **Результат:** ```py >>> some_func() ['some_string'] >>> some_func() ['some_string', 'some_string'] >>> some_func([]) ['some_string'] >>> some_func() ['some_string', 'some_string', 'some_string'] ``` #### 💡 Объяснение: - Изменяемые аргументы функций по умолчанию в Python на самом деле не инициализируются каждый раз, когда вы вызываете функцию. Вместо этого в качестве значения по умолчанию используется недавно присвоенное им значение. Когда мы явно передали `[]` в `some_func в качестве аргумента, значение по умолчанию переменной `default_arg` не было использовано, поэтому функция вернулась, как и ожидалось. ```py def some_func(default_arg=[]): default_arg.append("some_string") return default_arg ``` **Результат:** ```py >>> some_func.__defaults__ # Выражение выведет значения стандартных аргументов фукнции ([],) >>> some_func() >>> some_func.__defaults__ (['some_string'],) >>> some_func() >>> some_func.__defaults__ (['some_string', 'some_string'],) >>> some_func([]) >>> some_func.__defaults__ (['some_string', 'some_string'],) ``` - Чтобы избежать ошибок, связанных с изменяемыми аргументами, принято использовать `None` в качестве значения по умолчанию, а затем проверять, передано ли какое-либо значение в функцию, соответствующую этому аргументу. Пример: ```py def some_func(default_arg=None): if default_arg is None: default_arg = [] default_arg.append("some_string") return default_arg ``` --- ### ▶ Ловля исключений ```py some_list = [1, 2, 3] try: # Должно вернуться ``IndexError`` print(some_list[4]) except IndexError, ValueError: print("Caught!") try: # Должно вернуться ``ValueError`` some_list.remove(4) except IndexError, ValueError: print("Caught again!") ``` **Результат (Python 2.x):** ```py Caught! ValueError: list.remove(x): x not in list ``` **Результат (Python 3.x):** ```py File "", line 3 except IndexError, ValueError: ^ SyntaxError: invalid syntax ``` #### 💡 Объяснение * Чтобы добавить несколько Исключений в блок `except`, необходимо передать их в виде кортежа с круглыми скобками в качестве первого аргумента. Второй аргумент - это необязательное имя, которое при передаче свяжет экземпляр исключения, который был пойман. Пример, ```py some_list = [1, 2, 3] try: # Должно возникнуть ``ValueError`` some_list.remove(4) except (IndexError, ValueError), e: print("Caught again!") print(e) ``` **Результат (Python 2.x):** ``` Caught again! list.remove(x): x not in list ``` **Результат (Python 3.x):** ```py File "", line 4 except (IndexError, ValueError), e: ^ IndentationError: unindent does not match any outer indentation level ``` * Отделение исключения от переменной запятой является устаревшим и не работает в Python 3; правильнее использовать `as`. Пример, ```py some_list = [1, 2, 3] try: some_list.remove(4) except (IndexError, ValueError) as e: print("Caught again!") print(e) ``` **Результат:** ``` Caught again! list.remove(x): x not in list ``` --- ### ▶ Одни и те же операнды, разная история! 1\. ```py a = [1, 2, 3, 4] b = a a = a + [5, 6, 7, 8] ``` **Результат:** ```py >>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4] ``` 2\. ```py a = [1, 2, 3, 4] b = a a += [5, 6, 7, 8] ``` **Результат:** ```py >>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4, 5, 6, 7, 8] ``` #### 💡 Объяснение: * Выражение `a += b` не всегда ведет себя так же, как и `a = a + b`. Классы *могут* по-разному реализовывать операторы *`op=`*, а списки ведут себя так. * Выражение `a = a + [5,6,7,8]` создает новый список и устанавливает ссылку `a` на этот новый список, оставляя `b` неизменным. * Выражение `a += [5,6,7,8]` фактически отображается на функцию "extend", которая работает со списком так, что `a` и `b` по-прежнему указывают на тот же самый список, который был изменен на месте. --- ### ▶ Разрешение имен игнорирует область видимости класса 1\. ```py x = 5 class SomeClass: x = 17 y = (x for i in range(10)) ``` **Результат:** ```py >>> list(SomeClass.y)[0] 5 ``` 2\. ```py x = 5 class SomeClass: x = 17 y = [x for i in range(10)] ``` **Результат (Python 2.x):** ```py >>> SomeClass.y[0] 17 ``` **Результат (Python 3.x):** ```py >>> SomeClass.y[0] 5 ``` #### 💡 Объяснение - Области видимости, вложенные внутрь определения класса, игнорируют имена, связанные на уровне класса. - Выражение-генератор имеет свою собственную область видимости. - Начиная с версии Python 3.X, списковые вычисления также имеют свою собственную область видимости. --- ### ▶ Округляясь как банкир * Реализуем простейшую функцию по получению среднего элемента списка: ```py def get_middle(some_list): mid_index = round(len(some_list) / 2) return some_list[mid_index - 1] ``` **Python 3.x:** ```py >>> get_middle([1]) # вроде неплохо 1 >>> get_middle([1,2,3]) # все еще хорошо 2 >>> get_middle([1,2,3,4,5]) # что-то не то? 2 >>> len([1,2,3,4,5]) / 2 # хорошо 2.5 >>> round(len([1,2,3,4,5]) / 2) # почему снова так? 2 ``` Кажется, Python округлил 2.5 до 2. #### 💡 Объяснение: - Это не ошибка округления float, на самом деле такое поведение намеренно. Начиная с Python 3.0, `round()` использует [округление банкира](https://en.wikipedia.org/wiki/Rounding#Round_half_to_even), где дроби .5 округляются до ближайшего **четного** числа. ```py >>> round(0.5) 0 >>> round(1.5) 2 >>> round(2.5) 2 >>> import numpy # поведение numpy аналогично >>> numpy.round(0.5) 0.0 >>> numpy.round(1.5) 2.0 >>> numpy.round(2.5) 2.0 ``` - Это рекомендуемый способ округления дробей до .5, описанный в [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules). Однако в школах чаще всего преподают другой способ (округление от нуля), поэтому округление банкира, скорее всего, не так хорошо известно. Более того, некоторые из самых популярных языков программирования (например, JavaScript, Java, C/C++, Ruby, Rust) также не используют округление банкира. Таким образом, для Python это все еще довольно специфично и может привести к путанице при округлении дробей. - Дополнительную информацию можно найти в [документации](https://docs.python.org/3/library/functions.html#round) функции `round` или на [StackOverflow](https://stackoverflow.com/questions/10825926/python-3-x-rounding-behavior). --- ### ▶ Иголки в стоге сена * Я не встречал ни одного питониста на данный момент, который не встречался с одним из следующих сценариев, 1\. ```py x, y = (0, 1) if True else None, None ``` **Результат:** ```py >>> x, y # ожидается (0, 1) ((0, 1), None) ``` 2\. ```py t = ('one', 'two') for i in t: print(i) t = ('one') for i in t: print(i) t = () print(t) ``` **Результат:** ```py one two o n e tuple() ``` 3\. ```py ten_words_list = [ "some", "very", "big", "list", "that" "consists", "of", "exactly", "ten", "words" ] ``` **Результат** ```py >>> len(ten_words_list) 9 ``` 4\. Недостаточно твердое утверждение ```py a = "python" b = "javascript" ``` **Результат:** ```py # assert выражение с сообщением об ошиб >>> assert(a == b, "Both languages are different") # Исключение AssertionError не возникло ``` 5\. ```py some_list = [1, 2, 3] some_dict = { "key_1": 1, "key_2": 2, "key_3": 3 } some_list = some_list.append(4) some_dict = some_dict.update({"key_4": 4}) ``` **Результат:** ```py >>> print(some_list) None >>> print(some_dict) None ``` 6\. ```py def some_recursive_func(a): if a[0] == 0: return a[0] -= 1 some_recursive_func(a) return a def similar_recursive_func(a): if a == 0: return a a -= 1 similar_recursive_func(a) return a ``` **Результат:** ```py >>> some_recursive_func([5, 0]) [0, 0] >>> similar_recursive_func(5) 4 ``` #### 💡 Объяснение: * Для 1 примера правильным выражением для ожидаемого поведения является `x, y = (0, 1) if True else (None, None)`. * Для 2 примера правильным выражением для ожидаемого поведения будет `t = ('one',)` или `t = 'one',` (пропущена запятая), иначе интерпретатор рассматривает `t` как `str` и перебирает его символ за символом. * `()` - специальное выражение, обозначающая пустой `tuple`. * В 3 примере, как вы, возможно, уже поняли, пропущена запятая после 5-го элемента (`"that"`) в списке. Таким образом, неявная конкатенация строковых литералов, ```py >>> ten_words_list ['some', 'very', 'big', 'list', 'thatconsists', 'of', 'exactly', 'ten', 'words'] ``` * В 4-ом фрагменте не возникло `AssertionError`, потому что вместо "проверки" отдельного выражения `a == b`, мы "проверяем" весь кортеж. Следующий фрагмент прояснит ситуацию, ```py >>> a = "python" >>> b = "javascript" >>> assert a == b Traceback (most recent call last): File "", line 1, in AssertionError >>> assert (a == b, "Values are not equal") :1: SyntaxWarning: assertion is always true, perhaps remove parentheses? >>> assert a == b, "Values are not equal" Traceback (most recent call last): File "", line 1, in AssertionError: Values are not equal ``` * Что касается пятого фрагмента, то большинство методов, изменяющих элементы последовательности/маппингов, такие как `list.append`, `dict.update`, `list.sort` и т. д., изменяют объекты на месте и возвращают `None`. Это делается для того, чтобы повысить производительность, избегая создания копии объекта, если операция может быть выполнена на месте (подробнее в [документации](https://docs.python.org/3/faq/design.html#why-doesn-t-list-sort-return-the-sorted-list)). * Последнее должно быть достаточно очевидным, изменяемый объект (например, `list`) может быть изменен в функции, а переназначение неизменяемого (`a -= 1`) не является изменением значения. * Знание этих тонкостей может сэкономить вам часы отладки в долгосрочной перспективе. --- ### ▶ Сплиты (splitsies) * ```py >>> 'a'.split() ['a'] # эквивалентно >>> 'a'.split(' ') ['a'] # но >>> len(''.split()) 0 # не эквивалентно >>> len(''.split(' ')) 1 ``` #### 💡 Объяснение - Может показаться, что разделителем по умолчанию для split является одиночный пробел `' '`, но согласно [документации](https://docs.python.org/3/library/stdtypes.html#str.split) > если sep не указан или равен `none`, применяется другой алгоритм разбиения: последовательные пробельные символы рассматриваются как один разделитель, и результат не будет содержать пустых строк в начале или конце, если в строке есть ведущие или завершающие пробелы. Следовательно, разбиение пустой строки или строки, состоящей только из пробельных символов, с разделителем none возвращает `[]`. > если задан sep, то последовательные разделители не группируются вместе и считаются разделителями пустых строк (например, `'1,,2'.split(',')` возвращает `['1', '', '2']`). Разделение пустой строки с указанным разделителем возвращает `['']`. - Обратите внимание, как обрабатываются ведущие и завершающие пробелы в следующем фрагменте, ```py >>> ' a '.split(' ') ['', 'a', ''] >>> ' a '.split() ['a'] >>> ''.split(' ') [''] ``` --- ### ▶ Подстановочное импортирование (wild imports) * ```py # File: module.py def some_weird_name_func_(): print("works!") def _another_weird_name_func(): print("works!") ``` **Результат** ```py >>> from module import * >>> some_weird_name_func_() "works!" >>> _another_weird_name_func() Traceback (most recent call last): File "", line 1, in NameError: name '_another_weird_name_func' is not defined ``` #### 💡 Объяснение: - Часто рекомендуется не использовать импорт с подстановочными знаками (wildcard import). Первая очевидная причина заключается в том, что при импорте с подстановочным знаком имена с ведущим подчеркиванием не импортируются. Это может привести к ошибкам во время выполнения. - Если бы мы использовали синтаксис `from ... import a, b, c`, приведенная выше `NameError` не возникла бы. ```py >>> from module import some_weird_name_func_, _another_weird_name_func >>> _another_weird_name_func() works! ``` - Если вы действительно хотите использовать импорт с подстановочными знаками, то нужно определить список `__all__` в вашем модуле, который будет содержать публичные объекты, доступные при wildcard импортировании. ```py __all__ = ['_another_weird_name_func'] def some_weird_name_func_(): print("works!") def _another_weird_name_func(): print("works!") ``` **Результат** ```py >>> _another_weird_name_func() "works!" >>> some_weird_name_func_() Traceback (most recent call last): File "", line 1, in NameError: name 'some_weird_name_func_' is not defined ``` ---