Python, будучи прекрасно спроектированным высокоуровневым языком программирования, предоставляет множество возможностей для удобства программиста. Но иногда результаты работы Python кода могут показаться неочевидными на первый взгляд.
**wtfpython** задуман как проект, пытающийся объяснить, что именно происходит под капотом некоторых неочевидных фрагментов кода и менее известных возможностей Python.
Если вы опытный программист на Python, вы можете принять это как вызов и правильно объяснить WTF ситуации с первой попытки. Возможно, вы уже сталкивались с некоторыми из них раньше, и я смогу оживить ваши старые добрые воспоминания! 😅
PS: Если вы уже читали **wtfpython** раньше, с изменениями можно ознакомиться [здесь](https://github.com/satwikkansal/wtfpython/releases/) (примеры, отмеченные звездочкой - это примеры, добавленные в последней основной редакции).
> (Опционально): Краткое описание неожиданного результата
>
>
> #### 💡 Объяснение
>
> * Краткое объяснение того, что происходит и почему это происходит.
>
> ```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),
<!-- Example ID: d3d73936-3cf1-4632-b5ab-817981338863 -->
<!-- read-only -->
По какой-то причине "моржовый оператор" (англ. walrus) `:=` в Python 3.8 стал довольно популярным. Давайте проверим его,
1\.
```py
# Python version 3.8+
>>> a = "wtf_walrus"
>>> a
'wtf_walrus'
>>> a := "wtf_walrus"
File "<stdin>", 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 "<stdin>", 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 элементов.
>>> 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).