mirror of
https://github.com/satwikkansal/wtfpython
synced 2024-11-10 05:28:51 +01:00
2846 lines
118 KiB
Markdown
Vendored
2846 lines
118 KiB
Markdown
Vendored
<p align="center"><img src="/images/logo.png#gh-light-mode-only" alt=""><img src="/images/logo-dark.png#gh-dark-mode-only" alt=""></p>
|
||
<h1 align="center">What the f*ck Python! 😱</h1>
|
||
<p align="center">Изучение и понимание Python с помощью нестандартного поведения и "магического" поведения.</p>
|
||
|
||
Переводы: [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
|
||
```
|
||
|
||
# 👀 Примеры
|
||
|
||
## Секция: Напряги мозги!
|
||
|
||
### ▶ Первым делом!
|
||
|
||
<!-- 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 элементов.
|
||
|
||
---
|
||
|
||
### ▶ Строки иногда ведут себя непредсказуемо
|
||
|
||
<!-- Example ID: 30f1d3fc-e267-4b30-84ef-4d9e7091ac1a --->
|
||
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).
|
||
|
||
---
|
||
|
||
|
||
### ▶ Осторожнее с цепочкой операций
|
||
<!-- Example ID: 07974979-9c86-4720-80bd-467aa19470d9 --->
|
||
```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`
|
||
<!-- Example ID: 230fa2ac-ab36-4ad1-b675-5f5a1c1a6217 --->
|
||
Ниже приведен очень известный пример.
|
||
|
||
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) для получения обновлений.
|
||
|
||
---
|
||
|
||
|
||
### ▶ Мистическое хэширование
|
||
<!-- Example ID: eb17db53-49fd-4b61-85d6-345c5ca213ff --->
|
||
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)) и ухудшает производительность постоянного времени, которую обычно обеспечивает хэширование).
|
||
|
||
---
|
||
|
||
|
||
### ▶ В глубине души мы все одинаковы.
|
||
<!-- Example ID: 8f99a35f-1736-43e2-920d-3b78ec35da9b --->
|
||
```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
|
||
```
|
||
Как вы можете заметить, все дело в порядке уничтожения объектов.
|
||
|
||
---
|
||
|
||
|
||
### ▶ Беспорядок внутри порядка *
|
||
<!-- Example ID: 91bff1f8-541d-455a-9de4-6cd8ff00ea66 --->
|
||
```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 "<stdin>", line 1, in <module>
|
||
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`.
|
||
|
||
---
|
||
|
||
|
||
### ▶ Продолжай пытаться... *
|
||
<!-- Example ID: b4349443-e89f-4d25-a109-82616be9d41a --->
|
||
```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 "<stdin>", line 1, in <module>
|
||
ZeroDivisionError: division by zero
|
||
|
||
>>> one_more_func()
|
||
Iteration 0
|
||
|
||
```
|
||
|
||
#### 💡 Объяснение:
|
||
|
||
- Когда один из операторов `return`, `break` или `continue` выполняется в блоке `try` оператора "try...finally", на выходе также выполняется пункт `finally`.
|
||
- Возвращаемое значение функции определяется последним выполненным оператором `return`. Поскольку блок `finally` выполняется всегда, оператор `return`, выполненный в блоке `finally`, всегда будет последним.
|
||
- Предостережение - если в блоке `finally` выполняется оператор `return` или `break`, то временно сохраненное исключение отбрасывается.
|
||
|
||
---
|
||
|
||
|
||
### ▶ Для чего?
|
||
<!-- Example ID: 64a9dccf-5083-4bc9-98aa-8aeecde4f210 --->
|
||
```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
|
||
```
|
||
|
||
---
|
||
|
||
|
||
### ▶ Расхождение во времени исполнения
|
||
<!-- Example ID: 6aa11a4b-4cf1-467a-b43a-810731517e98 --->
|
||
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 ...)`
|
||
<!-- Example ID: b26fb1ed-0c7d-4b9c-8c6d-94a58a055c0d --->
|
||
```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 побеждает с первой попытки!
|
||
<!-- Example ID: 69329249-bdcb-424f-bd09-cca2e6705a7a --->
|
||
|
||
```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', '', ''], ['', '', ''], ['', '', '']]
|
||
```
|
||
|
||
---
|
||
|
||
|
||
### ▶ Переменная Шредингера *
|
||
<!-- Example ID: 4dc42f77-94cb-4eb5-a120-8203d3ed7604 --->
|
||
|
||
|
||
```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())
|
||
```
|
||
|
||
---
|
||
|
||
|
||
### ▶ Проблема курицы и яйца *
|
||
<!-- Example ID: 60730dc2-0d79-4416-8568-2a63323b3ce8 --->
|
||
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 из-за "обмана" на уровне реализации.
|
||
|
||
---
|
||
|
||
|
||
### ▶ Отношения между подклассами
|
||
<!-- Example ID: 9f6d8cf0-e1b5-42d0-84a0-4cfab25a0bc0 --->
|
||
**Вывод:**
|
||
```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/).
|
||
|
||
---
|
||
|
||
|
||
### ▶ Равенство и тождество методов
|
||
<!-- Example ID: 94802911-48fe-4242-defa-728ae893fa32 --->
|
||
|
||
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
|
||
<bound method SomeClass.method of <__main__.SomeClass object at ...>>
|
||
```
|
||
* При многократном обращении к атрибуту каждый раз создается объект метода! Поэтому `o1.method is o1.method` всегда ложно. Однако доступ к функциям как к атрибутам класса (в отличие от экземпляра) не создает методов; поэтому
|
||
`SomeClass.method is SomeClass.method` является истинным.
|
||
```py
|
||
>>> SomeClass.method
|
||
<function SomeClass.method at ...>
|
||
```
|
||
* `classmethod` преобразует функции в методы класса. Методы класса - это дескрипторы, которые при обращении к ним создают
|
||
объект метода, который связывает *класс* (тип) объекта, а не сам объект.
|
||
```py
|
||
>>> o1.classm
|
||
<bound method SomeClass.classm of <class '__main__.SomeClass'>>
|
||
```
|
||
* В отличие от функций, `classmethod` будет создавать метод и при обращении к нему как к атрибуту класса (в этом случае они
|
||
привязываются к классу, а не к его типу). Поэтому `SomeClass.classm is SomeClass.classm` является ошибочным.
|
||
```py
|
||
>>> SomeClass.classm
|
||
<bound method SomeClass.classm of <class '__main__.SomeClass'>>
|
||
```
|
||
* Объект-метод равен, если обе функции равны, а связанные объекты одинаковы. Поэтому
|
||
`o1.method == o1.method` является истинным, хотя и не является одним и тем же объектом в памяти.
|
||
* `staticmethod` преобразует функции в дескриптор "no-op", который возвращает функцию как есть. Методы-объекты
|
||
никогда не создается, поэтому сравнение с `is` является истинным.
|
||
```py
|
||
>>> o1.staticm
|
||
<function SomeClass.staticm at ...>
|
||
>>> SomeClass.staticm
|
||
<function SomeClass.staticm at ...>
|
||
```
|
||
* Необходимость создавать новые объекты "метод" каждый раз, когда Python вызывает методы экземпляра, и необходимость изменять аргументы
|
||
каждый раз, чтобы вставить `self`, сильно сказывается на производительности.
|
||
CPython 3.7 [решил эту проблему](https://bugs.python.org/issue26110), введя новые опкоды, которые работают с вызовом методов
|
||
без создания временных объектов методов. Это используется только при фактическом вызове функции доступа, так что
|
||
приведенные здесь фрагменты не затронуты и по-прежнему генерируют методы :)
|
||
|
||
---
|
||
|
||
|
||
### ▶ All-true-ation (непереводимая игра слов) *
|
||
<!-- Example ID: dfe6d845-e452-48fe-a2da-0ed3869a8042 -->
|
||
|
||
```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`. Это происходит потому, что единственный элемент переданного массива (`[[...]]`) уже не пуст, а списки со значениями являются истинными.
|
||
|
||
---
|
||
|
||
|
||
### ▶ Неожиданная запятая
|
||
<!-- Example ID: 31a819c8-ed73-4dcc-84eb-91bedbb51e58 --->
|
||
**Вывод (< 3.6):**
|
||
|
||
```py
|
||
>>> def f(x, y,):
|
||
... print(x, y)
|
||
...
|
||
>>> def g(x=4, y=5,):
|
||
... print(x, y)
|
||
...
|
||
>>> def h(x, **kwargs,):
|
||
File "<stdin>", line 1
|
||
def h(x, **kwargs,):
|
||
^
|
||
SyntaxError: invalid syntax
|
||
|
||
>>> def h(*args,):
|
||
File "<stdin>", line 1
|
||
def h(*args,):
|
||
^
|
||
SyntaxError: invalid syntax
|
||
```
|
||
|
||
#### 💡 Объяснение:
|
||
|
||
- Запятая в конце списка аргументов функции Python не всегда законна.
|
||
- В Python список аргументов определяется частично с помощью ведущих запятых, а частично с помощью запятых в конце списка. Этот конфликт приводит к ситуациям, когда запятая оказывается в середине, и ни одно из правил не выполняется.
|
||
- **Примечание:** Проблема с запятыми в конце списка аргументов [исправлена в Python 3.6](https://bugs.python.org/issue9232). Варианты использования запятых в конце выражения приведены в [обсуждении](https://bugs.python.org/issue9232#msg248399).
|
||
|
||
---
|
||
|
||
|
||
### ▶ Строки и обратные слэши
|
||
<!-- Example ID: 6ae622c3-6d99-4041-9b33-507bd1a4407b --->
|
||
**Вывод:**
|
||
```py
|
||
>>> print("\"")
|
||
"
|
||
|
||
>>> print(r"\"")
|
||
\"
|
||
|
||
>>> print(r"\")
|
||
File "<stdin>", 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!)
|
||
<!-- Example ID: 7034deb1-7443-417d-94ee-29a800524de8 --->
|
||
```py
|
||
x = True
|
||
y = False
|
||
```
|
||
|
||
**Результат:**
|
||
```py
|
||
>>> not x == y
|
||
True
|
||
>>> x == not y
|
||
File "<input>", 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`.
|
||
|
||
---
|
||
|
||
|
||
### ▶ Строки наполовину в тройных кавычках
|
||
<!-- Example ID: c55da3e2-1034-43b9-abeb-a7a970a2ad9e --->
|
||
**Вывод:**
|
||
```py
|
||
>>> print('wtfpython''')
|
||
wtfpython
|
||
>>> print("wtfpython""")
|
||
wtfpython
|
||
>>> # Выражения ниже приводят к `SyntaxError`
|
||
>>> # print('''wtfpython')
|
||
>>> # print("""wtfpython")
|
||
File "<input>", 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 ожидал завершающую тройную кавычку в качестве разделителя при сканировании текущего встреченного строкового литерала с тройной кавычкой.
|
||
|
||
---
|
||
|
||
### ▶ Что не так с логическими значениями?
|
||
<!-- Example ID: 0bba5fa7-9e6d-4cd2-8b94-952d061af5dd --->
|
||
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!
|
||
|
||
---
|
||
|
||
|
||
### ▶ Атрибуты класса и экземпляра
|
||
<!-- Example ID: 6f332208-33bd-482d-8106-42863b739ed9 --->
|
||
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 из генератора
|
||
<!-- Example ID: 5a40c241-2c30-40d0-8ba9-cf7e097b3b53 --->
|
||
```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]
|
||
<generator object <listcomp> 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 возвращает... *
|
||
<!-- Example ID: 5626d8ef-8802-49c2-adbc-7cda5c550816 --->
|
||
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-рефлексивность *
|
||
|
||
<!-- Example ID: 59bee91a-36e0-47a4-8c7d-aa89bf1d3976 --->
|
||
|
||
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/)
|
||
|
||
---
|
||
|
||
|
||
### ▶ Мутируем немутируемое!
|
||
|
||
<!-- Example ID: 15a9e782-1695-43ea-817a-a9208f6bb33d --->
|
||
|
||
Это может показаться тривиальным, если вы знаете, как работают ссылки в 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).
|
||
|
||
---
|
||
|
||
|
||
### ▶ Исчезающая переменная из внешней области видимости
|
||
<!-- Example ID: 7f1e71b6-cb3e-44fb-aa47-87ef1b7decc8 --->
|
||
|
||
```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
|
||
# Ничего не выводится!
|
||
```
|
||
|
||
---
|
||
|
||
|
||
### ▶ Загадочное преобразование типов ключей
|
||
<!-- Example ID: 00f42dd0-b9ef-408d-9e39-1bc209ce3f36 --->
|
||
```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)
|
||
```
|
||
|
||
---
|
||
|
||
|
||
### ▶ Посмотрим, сможете ли вы угадать что здесь?
|
||
<!-- Example ID: 81aa9fbe-bd63-4283-b56d-6fdd14c9105e --->
|
||
```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) для получения более подробной информации об изменении лимита по умолчанию, если вы ожидаете, что ваш код превысит это значение.
|
||
|
||
---
|
||
|
||
|
||
## Секция: Скользкие склоны
|
||
|
||
### ▶ Изменение словаря во время прохода по нему
|
||
<!-- Example ID: b4e5cdfb-c3a8-4112-bd38-e2356d801c41 --->
|
||
```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`
|
||
<!-- Example ID: 777ed4fd-3a2d-466f-95e7-c4058e61d78e --->
|
||
<!-- read-only -->
|
||
|
||
```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__': <module '__builtin__' (built-in)>, 'SomeClass': <class __main__.SomeClass at 0x7f98a1a5f668>, '__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!" (наконец-то!).
|
||
|
||
---
|
||
|
||
|
||
### ▶ Переменная за пределами видимости
|
||
<!-- Example ID: 75c03015-7be9-4289-9e22-4f5fdda056f7 --->
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
|
||
### ▶ Удаление элемента списка во время прохода по списку
|
||
<!-- Example ID: 4cc52d4e-d42b-4e09-b25f-fbf5699b7d4e --->
|
||
```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), связанный со словарями.
|
||
|
||
---
|
||
|
||
|
||
### ▶ Сжатие итераторов с потерями *
|
||
<!-- Example ID: c28ed154-e59f-4070-8eb6-8967a4acac6d --->
|
||
|
||
```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)]
|
||
```
|
||
Первый аргумент сжатия должен иметь наименьшее число элементов
|
||
|
||
---
|
||
|
||
|
||
### ▶ Утечка переменных внутри цикла
|
||
<!-- Example ID: ccec7bf6-7679-4963-907a-1cd8587be9ea --->
|
||
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()`, и, в частности, управляющие переменные цикла больше не просачиваются в окружающую область видимости."
|
||
|
||
---
|
||
|
||
|
||
### ▶ Остерегайтесь изменяемых аргументов по умолчанию!
|
||
<!-- Example ID: 7d42dade-e20d-4a7b-9ed7-16fb58505fe9 --->
|
||
|
||
```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
|
||
```
|
||
|
||
---
|
||
|
||
|
||
### ▶ Ловля исключений
|
||
<!-- Example ID: b5ca5e6a-47b9-4f69-9375-cda0f8c6755d --->
|
||
```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 "<input>", 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 "<input>", 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
|
||
```
|
||
|
||
---
|
||
|
||
|
||
### ▶ Одни и те же операнды, разная история!
|
||
<!-- Example ID: ca052cdf-dd2d-4105-b936-65c28adc18a0 --->
|
||
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` по-прежнему указывают на тот же самый список, который был изменен на месте.
|
||
|
||
---
|
||
|
||
|
||
### ▶ Разрешение имен игнорирует область видимости класса
|
||
<!-- Example ID: 03f73d96-151c-4929-b0a8-f74430788324 --->
|
||
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).
|
||
|
||
---
|
||
|
||
|
||
### ▶ Иголки в стоге сена *
|
||
|
||
<!-- Example ID: 52a199b1-989a-4b28-8910-dff562cebba9 --->
|
||
|
||
Я не встречал ни одного питониста на данный момент, который не встречался с одним из следующих сценариев,
|
||
|
||
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 "<stdin>", line 1, in <module>
|
||
AssertionError
|
||
|
||
>>> assert (a == b, "Values are not equal")
|
||
<stdin>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?
|
||
|
||
>>> assert a == b, "Values are not equal"
|
||
Traceback (most recent call last):
|
||
File "<stdin>", line 1, in <module>
|
||
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) *
|
||
<!-- example id: ec3168ba-a81a-4482-afb0-691f1cc8d65a --->
|
||
```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) *
|
||
<!-- Example ID: 83deb561-bd55-4461-bb5e-77dd7f411e1c --->
|
||
<!-- read-only -->
|
||
|
||
```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 "<stdin>", line 1, in <module>
|
||
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 "<stdin>", line 1, in <module>
|
||
NameError: name 'some_weird_name_func_' is not defined
|
||
```
|
||
|
||
---
|