Недавно мы писали о забавных, хитрых и странных примерах на JavaScript. Теперь пришла очередь Python. У Python, высокоуровневого и интерпретируемого языка, много удобных свойств. Но иногда результат работы некоторых кусков кода на первый взгляд выглядит неочевидным.
Ниже — забавный проект, в котором собраны примеры неожиданного поведения в Python с обсуждением того, что происходит под капотом. Часть примеров не относятся к категории настоящих WTF?!, но зато они демонстрируют интересные особенности языка, которых вы можете захотеть избегать. Я думаю, это хороший способ изучить внутреннюю работу Python, и надеюсь, вам будет интересно.
Если вы уже опытный программист на Python, то многие примеры могут быть вам знакомы и даже вызовут ностальгию по тем случаям, когда вы ломали над ними голову :)
Содержание
- Структура примеров
- Использование
- Примеры
- Пропуск строк?
- Ну, как-то сомнительно...
- Время для хеш-пирожных!
- Несоответствие времени обработки
- Преобразование словаря во время его итерирования
- Удаление элемента списка во время его итерирования
- Обратные слеши в конце строки
- Давайте сделаем гигантскую строку!
- Оптимизации интерпретатора конкатенации строк
- Да, оно существует!
is
не то, что оно естьis not ...
отличается отis (not ...)
- Функция внутри цикла выдаёт один и тот же результат
- Утечка переменных цикла из локальной области видимости
- Крестики-нолики, где Х побеждает с первой попытки
- Опасайтесь изменяемых аргументов по умолчанию
- Те же операнды, но другая история
- Изменение неизменяемого
- Использование переменной, не определённой в области видимости
- Исчезновение переменной из внешней области видимости
Return
возвращает везде- Когда True на самом деле False
- Будьте осторожны с цепочками операций
- Разрешение имён (name resolution) игнорирует область видимости класса
- От заполненности до None в одной инструкции
- Явное приведение типов в строковых значениях
- Атрибуты классов и экземпляров
- Ловля исключений
- Полночь не существует?
- Что не так с булевыми значениями?
- Игла в стоге сена
- For что?
- Узел not
- А вы могли такое предположить?
- Мелкие примеры
- Внести свой вклад
- Полезные ссылки
Структура примеров
Примечание: Все приведённые примеры протестированы на интерактивном интерпретаторе Python 3.5.2 и должны работать во всех версиях языка, если иное явно не указано в описании.
Структура примеров:
Какой-нибудь дурацкий заголовок
# Код.
# Подготовка к магии...
Результат (версия Python):
>>> инициирующее_выражение
Вероятно, неожиданный результат
(Опционально): Однострочное описание неожиданного результата.
Объяснение:
Краткое объяснение того, что произошло и почему.
Поясняющие примеры (если необходимо)
Результат:
>>> инициирование # какого-то примера, срывающего покровы с магии # объясняющий результат
Использование
Мне кажется, что лучший способ извлечь максимальную пользу из этих примеров — это читать их в хронологическом порядке:
- Внимательно изучать начальный код. Если вы опытный Python-программист, то чаще всего будете успешно предсказывать, что произойдёт.
- Изучать результаты и:
- Проверять, совпали ли они с вашими ожиданиями.
- Убеждаться, что вы понимаете, почему получен именно такой результат.
- Если не понимаете, то читайте объяснение (если всё равно не понимаете, то заорите и напишите здесь).
- Если понимаете, то погладьте себя по головке и переходите к следующему примеру.
P. S. Также можете читать эти примеры в командной строке. Только сначала установите npm-пакет wtfpython
,
$ npm install -g wtfpython
Теперь запустите wtfpython
в командной строке, и в результате эта коллекция откроется в вашем $PAGER
.
#TODO: Добавьте пакет pypi для чтения в командной строке.
Примеры
Пропуск строк?
Результат:
>>> value = 11
>>> valuе = 32
>>> value
11
Wat?
Примечание: проще всего воспроизвести этот пример с помощью копирования и вставки в ваш файл/оболочку.
Объяснение
Некоторые Unicode-символы выглядят так же, как и ASCII, но различаются интерпретатором.
>>> value = 42 #ascii e
>>> valuе = 23 #cyrillic e, Python 2.x interpreter would raise a `SyntaxError` here
>>> value
42
Ну, как-то сомнительно...
def square(x):
"""
Простая функция для вычисления квадрата числа путём сложения.
"""
sum_so_far = 0
for counter in range(x):
sum_so_far = sum_so_far + x
return sum_so_far
Результат (Python 2.x):
>>> square(10)
10
Разве должно было получиться не 100?
Примечание: если не можете воспроизвести результат, попробуйте запустить в оболочке файл mixed_tabs_and_spaces.py.
Объяснение
- Не смешивайте табуляцию и пробелы! Символ, предшествующий return, это табуляция, он распознаётся как четыре пробела.
- Вот как Python обрабатывает табуляции:
Сначала они заменяются (слева направо) пробелами, от одного до восьми, так что общее количество заменяемых символов может быть в восемь раз больше... - Поэтому табуляция в последней строке функции
square
заменяется восемью пробелами и попадает в цикл. - Python 3 в таких случаях умеет автоматически кидать ошибку.
Результат (Python 3.x):
TabError: inconsistent use of tabs and spaces in indentation
Время для хеш-пирожных!
1.
some_dict = {}
some_dict[5.5] = "Ruby"
some_dict[5.0] = "JavaScript"
some_dict[5] = "Python"
Результат:
>>> some_dict[5.5]
"Ruby"
>>> some_dict[5.0]
"Python"
>>> some_dict[5]
"Python"
Python уничтожил существование JavaScript?
Объяснение
- Словари в Python проверяют эквивалентность и сравнивают значение хешей, чтобы определить, одинаковы ли два ключа.
Неизменяемые объекты с одинаковыми значениями в Python всегда получают одинаковые хеши.
>>> 5 == 5.0 True >>> hash(5) == hash(5.0) True
Примечание: объекты с разными значениями тоже могут получить одинаковые хеши (такая ситуация называется хеш-коллизией).
- При выполнении выражения
some_dict[5] = "Python"
существующее выражение «JavaScript» переписывается на «Python», потому что Python распознаёт5
и5.0
как одинаковые ключи словаряsome_dict
. - На StackOverflow прекрасно объясняется причина такого поведения.
Несоответствие времени обработки
array = [1, 8, 15]
g = (x for x in array if array.count(x) > 0)
array = [2, 8, 22]
Результат:
>>> print(list(g))
[8]
Объяснение
- В выражении генератора клауза
in
обрабатывается во время объявления, а условная клауза — во время run time. - Поэтому перед run time выполняется переприсваивание
array
к списку[2, 8, 22]
, а поскольку из1
,8
и15
только значение счётчика8
больше0
, то генератор выдаёт только8
.
Преобразование словаря во время его итерирования
x = {0: None}
for i in x:
del x[i]
x[i+1] = None
print(i)
Результат:
0
1
2
3
4
5
6
7
Да, выполняется ровно восемь раз и останавливается.
Объяснение:
- В языке не поддерживается возможность итерирования словаря, который вы редактируете.
- Выполняется восемь раз потому, что в этом месте словарь увеличивается, чтобы вмещать больше ключей (у нас восемь записей удаления, так что нужно менять размер словаря). Это особенности реализации.
- Аналогичный пример разбирается на StackOverflow.
Удаление элемента списка во время его итерирования
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)
Результат:
>>> list_1
[1, 2, 3, 4]
>>> list_2
[2, 4]
>>> list_3
[]
>>> list_4
[2, 4]
Знаете, почему получился результат [2, 4]
?
Объяснение:
Менять объект во время его итерирования — всегда плохая идея. Лучше тогда итерировать копию объекта, что и делает
list_3[:]
.
>>> 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
окажется пропущена. То же самое произойдёт с каждым вторым элементом в списке. Похожий пример, связанный со словарями в Python, прекрасно объяснён на StackOverflow.
Обратные слеши в конце строки
Результат:
>>> print("\\ some string \\")
>>> print(r"\ some string")
>>> print(r"\ some string \")
File "<stdin>", line 1
print(r"\ some string \")
^
SyntaxError: EOL while scanning string literal
Объяснение
- В необработанном строковом литерале (raw string literal), на что указывает префикс
r
, обратный слеш не имеет особого значения. - Но интерпретатор меняет поведение обратных слешей, в результате они и последующий символ просто пропускаются. Поэтому обратные слеши в конце необработанных строк не действуют.
Давайте сделаем гигантскую строку!
Это вовсе не WTF, а лишь некоторые прикольные вещи, и их нужно опасаться :)
def add_string_with_plus(iters):
s = ""
for i in range(iters):
s += "xyz"
assert len(s) == 3*iters
def add_string_with_format(iters):
fs = "{}"*iters
s = fs.format(*(["xyz"]*iters))
assert len(s) == 3*iters
def add_string_with_join(iters):
l = []
for i in range(iters):
l.append("xyz")
s = "".join(l)
assert len(s) == 3*iters
def convert_list_to_string(l, iters):
s = "".join(l)
assert len(s) == 3*iters
Результат:
>>> timeit(add_string_with_plus(10000))
100 loops, best of 3: 9.73 ms per loop
>>> timeit(add_string_with_format(10000))
100 loops, best of 3: 5.47 ms per loop
>>> timeit(add_string_with_join(10000))
100 loops, best of 3: 10.1 ms per loop
>>> l = ["xyz"]*10000
>>> timeit(convert_list_to_string(l, 10000))
10000 loops, best of 3: 75.3 µs per loop
Объяснение
- Можете подробнее почитать про timeit. Обычно с её помощью измеряют, как долго выполняются фрагменты кода.
- Не используйте
+
для генерирования длинных строк: в Pythonstr
— неизменяемая, поэтому для каждой пары конкатенаций левая и правая строки должны быть скопированы в новую строку. Если вы конкатенируете четыре строки длиной по 10 символов, то копируйте (10 + 10) + ((10 + 10) + 10) + (((10 + 10) +10) +10) = 90 символов вместо 40. По мере увеличения количества и размера строк ситуация вчетверо ухудшается. - Поэтому рекомендуется использовать синтаксис
.format.
или%
(но на коротких строках это работает чуть медленнее, чем +). - А если ваш контент уже доступен в виде итерируемого объекта, то лучше выбирать гораздо более быстрое
''.join(iterable_object)
.
Оптимизации интерпретатора конкатенации строк
>>> a = "some_string"
>>> id(a)
140420665652016
>>> id("some" + "_" + "string") # Notice that both the ids are same.
140420665652016
# using "+", three strings:
>>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.25748300552368164
# using "+=", three strings:
>>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.012188911437988281
Объяснение:
+=
быстрее+
более чем двух строк, потому что первая строка (например,s1
дляs1 += s2 + s3
) не уничтожается, пока строка не будет обработана целиком.- Обе строки ссылаются на один объект, потому что оптимизация CPython в некоторых случаях старается использовать существующие неизменяемые объекты (особенность реализации), а не создавать каждый раз новые. Почитать подробнее.
Да, оно существует!
Клауза else
для циклов. Типичный пример:
def does_exists_num(l, to_find):
for num in l:
if num == to_find:
print("Exists!")
break
else:
print("Does not exist")
Результат:
>>> some_list = [1, 2, 3, 4, 5]
>>> does_exists_num(some_list, 4)
Существует!
>>> does_exists_num(some_list, -1)
Не существует.
Клауза else
в обработке исключений. Пример:
try:
pass
except:
print("Exception occurred!!!")
else:
print("Try block executed successfully...")
Результат:
Try block executed successfully...
Объяснение:
- Клауза
else
исполняется после цикла только тогда, когда после всех итераций нет явногоbreak
. - Клауза
else
после блокаtry
также называется клаузой завершения (completion clause), поскольку доступностьelse
в выраженииtry
означает, что блокtry
успешно завершён.
is
не то, что оно есть
Этот пример очень широко известен.
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
>>> a = 257; b = 257
>>> a is b
True
Объяснение:
Разница между is
и ==
- Оператор
is
проверяет, чтобы оба операнда ссылались на один объект (т. е. проверяет, идентичны ли они друг другу). - Оператор
==
сравнивает значения операндов и проверяет на идентичность. - Так что
is
используется для эквивалентности ссылок, а==
— для эквивалентности значений. Поясняющий пример:
>>> [] == []
True
>>> [] is [] # These are two empty lists at two different memory locations.
False
256
— существующий объект, а 257
— нет
При запуске Python в памяти размещаются числа от -5
до 256
. Они используются часто, так что целесообразно держать их наготове.
Цитата из https://docs.python.org/3/c-api/long.html
В текущей реализации поддерживается массив целочисленных объектов для всех чисел с –5 по 256, так что когда вы создаёте int из этого диапазона, то получаете ссылку на существующий объект. Поэтому должна быть возможность изменить значение на 1. Но подозреваю, что в этом случае поведение Python будет непредсказуемым. :-)
>>> 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
, поэтому создаёт в памяти другой объект.
a
и b
ссылаются на один объект при инициализации с одинаковым значением в одной строке.
>>> 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
, то не увидите такого поведения, потому что файл компилируется за раз.
is not ...
отличается от is (not ...)
>>> 'something' is not None
True
>>> 'something' is (not None)
False
Объяснение
is not
— это одиночный бинарный оператор, поведение которого отличается от ситуации, когда по отдельности используютсяis
иnot
.is not
выдаётFalse
, если переменные с обеих сторон оператора указывают на один объект. В противном случае выдаётсяTrue
.
Функция внутри цикла выдает один и тот же результат
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]
Результат:
>>> results
[0, 1, 2, 3, 4, 5, 6]
>>> funcs_results
[6, 6, 6, 6, 6, 6, 6]
Если до добавления some_func
в funcs
значения x
в каждой итерации были разными, все функции возвращали 6.
//OR
>>> 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]
Объяснение
- При определении функции в цикле, в теле которого используется переменная цикла, замыкание функции цикла привязано к переменной, а не к её значению. Так что все функции для вычисления используют последнее значение, присвоенное переменной.
- Чтобы получить желаемое поведение, вы можете передавать в функцию переменную цикла в качестве именованной переменной. Почему это работает? Потому что таким образом переменная снова будет определена в области видимости функции.
funcs = []
for x in range(7):
def some_func(x=x):
return x
funcs.append(some_func)
Результат:
>>> funcs_results = [func() for func in funcs]
>>> funcs_results
[0, 1, 2, 3, 4, 5, 6]
Утечка переменных цикла из локальной области видимости
1.
for x in range(7):
if x == 6:
print(x, ': for x inside loop')
print(x, ': x in global')
Результат:
6 : for x inside loop
6 : x in global
Но x
не был определён для цикла вне области видимости.
2.
# This time let's initialize x first
x = -1
for x in range(7):
if x == 6:
print(x, ': for x inside loop')
print(x, ': x in global')
Результат:
6 : for x inside loop
6 : x in global
3.
x = 1
print([x for x in range(5)])
print(x, ': x in global')
Результат (on Python 2.x):
[0, 1, 2, 3, 4]
(4, ': x in global')
Результат (on Python 3.x):
[0, 1, 2, 3, 4]
1 : x in global
Объяснение
- В Python циклы for используют то пространство видимости, в котором они существуют, не заботясь о своих определённых переменных цикла. Это относится и к ситуации, если мы до этого явно определили переменную цикла for в глобальном пространстве имён. Тогда она будет перепривязана к существующей переменной.
- Разница в результатах работы интерпретаторов Python 2.x и Python 3.x применительно к примеру с генерированием списков (list comprehension) может быть объяснена с помощью изменения, описанного в документации What’s New In Python 3.0:
«Для генерирования списков больше не поддерживается синтаксическая форма
[... for var in item1, item2, ...]
. Используйте вместо неё[... for var in (item1, item2, ...)]
. Также обратите внимание, что генерирования списков имеют разные семантики: они ближе к синтаксическому сахару применительно к генерирующему выражению внутри конструктораlist()
, и, в частности, переменные управления циклом больше не утекают в окружающую область видимости».
Крестики-нолики, где Х побеждает с первой попытки
# Let's initialize a row
row = [""]*3 #row i['', '', '']
# Let's make a board
board = [row]*3
Результат:
>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]
Но мы же не присваивали три X, верно?
Объяснение
Эта визуализация объясняет, что происходит в памяти при инициализации переменной row
:
А когда посредством умножения row
инициализируется board
, то в памяти происходит вот что (каждый из элементов board[0]
, board[1]
и board[2]
является ссылкой на один и тот же список, указанный в row
):
Опасайтесь изменяемых аргументов по умолчанию
def some_func(default_arg=[]):
default_arg.append("some_string")
return default_arg
Результат:
>>> 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
не было использовано значение по умолчанию, поэтому функция вернула то, что ожидалось.
def some_func(default_arg=[]):
default_arg.append("some_string")
return default_arg
Результат:
>>> some_func.__defaults__ #This will show the default argument values for the function
([],)
>>> 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
в качестве значения по умолчанию с последующей проверкой, передано ли какое-то значение в функцию, соответствующую этому аргументу. Пример:
def some_func(default_arg=None): if not default_arg: default_arg = [] default_arg.append("some_string") return default_arg
Те же операнды, но другая история
1.
a = [1, 2, 3, 4] b = a a = a + [5, 6, 7, 8]
Результат:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4]
2.
a = [1, 2, 3, 4] b = a a += [5, 6, 7, 8]
Результат:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4, 5, 6, 7, 8]
Объяснение
- Выражение
a += b
ведёт себя не так же, какa = a + b
- Выражение
a = a + [5,6,7,8]
генерирует новый объект и присваивает a ссылку на него, оставляяb
без изменений. - Выражение
a + =[5,6,7,8]
фактически преобразуется (mapped to) в функцию extend, которая работает с объектом таким образом, чтоa
иb
всё ещё указывают на один и тот же объект, который был изменён на месте.
Изменение неизменяемого
some_tuple = ("A", "tuple", "with", "values") another_tuple = ([1, 2], [3, 4], [5, 6])
Результат:
>>> some_tuple[2] = "change this" TypeError: 'tuple' object does not support item assignment >>> another_tuple[2].append(1000) #This throws no error >>> 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/2/reference/datamodel.html
Объект неизменяемого типа последовательности (immutable sequence type) не может измениться после своего создания. Если объект содержит ссылки на другие объекты, то эти объекты могут быть изменяемыми и могут быть изменены. Однако коллекцию объектов, на которую прямо ссылается неизменяемый объект, изменить нельзя.
- Оператор
+=
изменяет список на месте. Присвоение элемента (item assignment) не работает, но, когда возникает исключение, элемент уже был изменён на месте.
Использование переменной, не определeнной в области видимости
a = 1 def some_func(): return a def another_func(): a += 1 return a
Результат:
>>> some_func() 1 >>> another_func() UnboundLocalError: local variable 'a' referenced before assignment
Объяснение
- Когда вы присваиваете переменную в области видимости, она становится локальной для этой области. То есть a становится локальной переменной области видимости
another_func
, но она не была ранее инициализирована в той же области, которая кидает ошибку. - Почитайте это короткое замечательное руководство, чтобы больше узнать о том, как в Python работают пространства имён и разрешение области видимости (scope resolution).
- Используйте ключевое слово
global
для модифицирования переменной внешней области видимостиa
вanother_func
.
def another_func() global a a += 1 return a
Результат:
>>> another_func() 2
Исчезновение переменной из внешней области видимости
e = 7 try: raise Exception() except Exception as e: pass
Результат (Python 2.x):
>>> print(e) # prints nothing
Результат (Python 3.x):
>>> print(e) NameError: name 'e' is not defined
Объяснение
Источник.
Если назначает исключение с целевымas
, оно очищается в конце клаузыexcept
. Как если бы
except E as N: foo
было преобразовано в
except E as N: try: foo finally: del N
Это означает, что исключение нужно назначать на другое имя, чтобы можно было ссылаться на него после клаузы
except
. Исключения очищаются потому, что к ним прикрепляется обратная трассировка (traceback), в результате во фрейме стека формируется ссылочный цикл (reference cycle), поддерживающий все локалы в этом фрейме живыми, пока не пройдёт следующая итерация сборки мусора.
- В Python клаузы не входят в область видимости. В этом примере всё представлено в одной области видимости, и переменная e убирается из-за исполнения клаузы
except
. Но это не относится к функциям, имеющим отдельные внутренние области видимости. Иллюстрация:
def f(x): del(x) print(x)
x = 5
y = [5, 4, 3]
**Результат:**
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()`, так что при попытке вывода на экран вы ничего не увидите.
Результат (Python 2.x):
>>> e Exception() >>> print e # Nothing is printed!
Return возвращает везде
def some_func(): try: return 'from_try' finally: return 'from_finally'
Результат:
>>> some_func() 'from_finally'
Объяснение
- Когда в блоке
try
выраженияtry…finally
выполняетсяreturn
,break
илиcontinue
, то на выходе также исполняется клаузаfinally
. - Возвращаемое функцией значение определяется последним выполненным выражением
return
. Поскольку клаузаfinally
исполняется всегда, выражениеreturn
, исполненное в клаузеfinally
, всегда будет последним исполненным.
Когда True на самом деле False
True = False if True == False: print("I've lost faith in truth!")
Результат:
I've lost faith in truth!
Объяснение
- Изначально в Python не было типа
bool
(программисты использовали 0 для false и ненулевое значение вроде 1 для true). Затем в язык добавилиTrue
,False
и типbool
, но из-за обратной совместимости нельзя было сделатьTrue
иFalse
константами — они представляли собой просто встроенные переменные. - Python 3 стал обратно несовместимым, поэтому в нём наконец исправили ситуацию с булевыми значениями, так что этот пример не работает в Python 3.x!
Будьте осторожны с цепочками операций
>>> 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/2/reference/expressions.html#not-in
Формально, если a, b, c, ..., y, z — выражения, а op1, op2, ..., opN —операторы сравнения, тогда a op1 b op2 c… y opN z эквивалентно op1 b и b op2 c и… y opN z, за исключением того, что каждое выражение вычисляется однократно.
Хотя такое поведение могло показаться вам глупостью, оно очень удобно в ситуациях вроде
a == b == c и 0 <= x <= 100
.
False is False is False
эквивалентно(False is False) and (False is False)
True is False == False
эквивалентноTrue is False and False == False
, и поскольку первая часть выражения (True is False
) вычисляется какFalse
, то и всё выражение вычисляется какFalse
.1 > 0 < 1
эквивалентно1 > 0
and0 < 1
, что вычисляется какTrue
.- Выражение
(1 > 0) < 1
эквивалентноTrue < 1
и
>>> int(True) 1 >>> True + 1 #not relevant for this example, but just for fun 2
Так что
1 < 1
вычисляется какFalse
Разрешение имен игнорирует область видимости класса
1.
x = 5 class SomeClass: x = 17 y = (x for i in range(10))
Результат:
>>> list(SomeClass.y)[0] 5 2. x = 5 class SomeClass: x = 17 y = [x for i in range(10)]
Результат (Python 2.x):
>>> SomeClass.y[0] 17
Результат (Python 3.x):
>>> SomeClass.y[0] 5
Объяснение
- Области видимости, вложенные внутрь определения класса, игнорируют имена, привязанные к уровню класса.
- Генерирующее выражение имеет собственную область видимости.
- Начиная с Python 3.X генераторы списков (list comprehensions) тоже имеют свои области видимости.
От заполненности до None в одной инструкции
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})
Результат:
>>> print(some_list) None >>> print(some_dict) None
Объяснение
Большинство методов, изменяющих элементы объектов последовательности/преобразования (sequence/mapping objects) вроде
list.append
,dict.update
,list.sort
и т. д., изменяют объекты на месте и возвращаютNone
. Причина — в улучшении производительности благодаря избеганию созданий копии объекта, если операцию можно выполнить на месте (взято отсюда)
Явное приведение типов в строковых значениях
Это вовсе не WTF, но у меня ушла куча времени на осознание того, что в Python существуют такие вещи. Делюсь с начинающими.
a = float('inf') b = float('nan') c = float('-iNf') #These strings are case-insensitive d = float('nan')
Результат:
>>> 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 #but nan!=nan False >>> 50/a 0.0 >>> a/a nan >>> 23 + b nan
Объяснение
'inf'
и'nan'
— специальные строковые значения (чувствительные к регистру). Если явно привести их к типуfloat
, то можно использовать их для представления, соответственно, математических «бесконечности» и «не числа».
Атрибуты классов и экземпляров
1.
class A: x = 1 class B(A): pass class C(A): pass
Результат:
>>> 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 (3, 2, 3) >>> a = A() >>> a.x, A.x (3, 3) >>> a.x += 1 >>> a.x, A.x (4, 3)
2.
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]
Результат:
>>> 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
Объяснение
- Переменные классов и переменные в экземплярах классов внутренне обрабатываются как словари объектов классов. Если имя переменной не найдено в словаре текущего класса, то поиск выполняется в родительских классах.
- Оператор
+=
модифицирует изменяемый объект на месте без создания нового объекта. Так что изменение атрибута одного экземпляра влияет также на другие экземпляры и атрибут класса.
Ловля исключений
some_list = [1, 2, 3] try: # This should raise an ``IndexError`` print(some_list[4]) except IndexError, ValueError: print("Caught!") try: # This should raise a ``ValueError`` some_list.remove(4) except IndexError, ValueError: print("Caught again!")
Результат (Python 2.x):
Caught! ValueError: list.remove(x): x not in list
Результат (Python 3.x):
File "<input>", line 3 except IndexError, ValueError: ^ SyntaxError: invalid syntax
Объяснение
- Для добавления нескольких исключений в клаузу
except
вам нужно передавать их в первый аргумент в виде взятого в круглые скобки кортежа. Второй аргумент — опциональное имя, которое потом привязывается к экземпляру после кидаемого исключения. Пример:
some_list = [1, 2, 3] try: # This should raise a ``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):
File "<input>", line 4 except (IndexError, ValueError), e: ^ IndentationError: unindent does not match any outer indentation level
- Не рекомендуется отделять исключение от переменной с помощью запятой, это не работает в Python 3; нужно использовать
as
. Пример:
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
Полночь не существует?
from datetime import datetime midnight = datetime(2018, 1, 1, 0, 0) midnight_time = midnight.time() noon = datetime(2018, 1, 1, 12, 0) noon_time = noon.time() if midnight_time: print("Time at midnight is", midnight_time) if noon_time: print("Time at noon is", noon_time)
Результат:
('Time at noon is', datetime.time(12, 0)) The midnight time is not printed.
Объяснение
До Python 3.5 булевым значением для объекта
datetime.time
былоFalse
, если требовалось представить полночь в формате UTC. Из-за этого могут возникать ошибки при использовании синтаксисаif obj
: при проверке, имеет лиobj
значение null или другой эквивалент «пустоты».
Что не так с булевыми значениями?
1.
# A simple example to count the number of boolean and # integers in an iterable of mixed data types. 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
Результат:
>>> booleans_found_so_far 0 >>> integers_found_so_far 4
2.
another_dict = {} another_dict[True] = "JavaScript" another_dict[1] = "Ruby" another_dict[1.0] = "Python"
Результат:
>>> another_dict[True] "Python"
Объяснение
- Булевые значения — подкласс
int
>>> isinstance(True, int) True >>> isinstance(False, int) True
- Целое число
True
равно1
, аFalse
—0
.
>>> True == 1 == 1.0 and False == 0 == 0.0 True
- Причины описаны на StackOverflow.
Игла в стоге сена
Почти каждый Python-программист сталкивался с этой ситуацией.
t = ('one', 'two') for i in t: print(i) t = ('one') for i in t: print(i) t = () print(t)
Результат:
one two o n e tuple()
Объяснение
- Корректным выражением для ожидаемого поведения будет
t = ('one',)
илиt = 'one'
, (отсутствует запятая), иначе интерпретатор решит, чтоt
являетсяstr
и итерирует её символ за символом. - () — специальный токен, обозначающий пустой
tuple
.
For что?
Предложено @MittalAshok.
some_string = "wtf" some_dict = {} for i, some_dict[i] in enumerate(some_string): pass
Результат:
>>> some_dict # An indexed dict is created. {0: 'w', 1: 'f', 2: 'f'}
Объяснение
- Выражение
for
определено в учебнике Python как:
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
Здесь
exprlist
— цель присвоения. Это означает, что эквивалент{exprlist} = {next_value}
исполняется для каждого элемента из итерируемых. Интересный пример предложен @tukkek:
for i in range(4): print(i) i = 10
Результат:
0 1 2 3
Вы думали, что цикл будет прогнан только один раз?
Объяснение
- Выражение присваивания
i = 10
никогда не влияет на итерации цикла из-за особенностей работы цикла в Python. Перед началом каждой итерации следующий элемент, предоставленный итератором (range(4)
в данном случае), распаковывается и присваивается к переменной из целевого списка (i
в данном случае). - Функция
enumerate(some_string)
в каждой итерации извлекает новое значениеi
(счётчикA
увеличивается) и символ изsome_string
. Затем она задаёт (просто присваивает) этому символу ключi
из словаряsome_dict
. Развёртку цикла можно упростить до:
>>> i, some_dict[i] = (0, 'w') >>> i, some_dict[i] = (1, 't') >>> i, some_dict[i] = (2, 'f') >>> some_dict
Узел not
Предложено @MostAwesomeDude.
x = True y = False
Результат:
>>> not x == y True >>> x == not y File "<input>", line 1 x == not y ^ SyntaxError: invalid syntax
Объяснение
- Старшинство оператора влияет на вычисление выражения, и в Python оператор
==
выше по старшинству, чем операторnot
. - Поэтому
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
имеют одинаковое старшинство), но когда следом за токеномnot
он не находит токенin
, то выдаёт SyntaxError.
А вы могли такое предположить?
a, b = a[b] = {}, 5
Результат:
>>> a {5: ({...}, 5)}
Объяснение
- Согласно языковой справке, оператор присваивания имеет форму:
(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
). Более простой пример циклической ссылки:
>>> some_list = some_list[0] = [0] >>> some_list [[...]] >>> some_list[0] [[...]] >>> some_list is some_list[0] [[...]]
Аналогично вышеприведённому примеру (
a[b][0]
это тот же объект, что иa
)
- То есть вы можете разбить пример на:
a, b = {}, 5 a[b] = a, b
И циклическая ссылка может быть оправдана тем, что
a[b][0]
является тем же объектом, что и
a >>> a[b][0] is a True
Мелкие примеры
Join()
— это строковая операция (string operation), а не операция списка (list operation). В первое время это выглядит неочевидным.
Объяснение: еслиjoin()
— это метод для строки, тогда он может оперировать любыми итерируемыми (списками, кортежами, итераторами). Если бы это был метод для списка, то он реализовывался бы каждым типом отдельно. Кроме того, нет смысла помещать предназначенный для строковых значений метод в API обычного объектаlist
.- Несколько странно выглядящих, но семантически корректных выражений:
[] = ()
(распаковывает пустойtuple
в пустойlist
)'a'[0][0][0][0][0]
также семантически корректно, потому что строки в Python итерируемые.3 --0-- 5 == 8 и --5 == 5
семантически верны и вычисляются какTrue
.
- Python использует два байта для хранения локальной переменной в функции. В теории это означает, что в функции можно определить только 65 536 переменных. Но в Python есть удобное встроенное решение, которое позволяет хранить более 2^16 имён переменных. В этом коде показано, что произойдёт в стеке, когда определено более 65 536 локальных переменных (внимание: код выводит около 2^18 строк текста!):
import dis exec(""" def f():* """ + """ """.join(["X"+str(x)+"=" + str(x) for x in range(65539)])) f() print(dis.dis(f))
- Несколько потоков выполнения Python не будут работать параллельно (да, именно так!). Если вы создадите несколько потоков и попытаетесь запустить их параллельно, то из-за Global Interpreter Lock в Python все ваши потоки будут выполняться на одном ядре шаг за шагом. Для реального распараллеливания воспользуйтесь модулем многопроцессорной обработки.
- Создание срезов списка (list slicing) с выходящими за границы индексами не приведёт к ошибкам:
>>> some_list = [1, 2, 3, 4, 5] >>> some_list[111:] []
Внести свой вклад
Все патчи приветствуются! Только перед самой публикацией прошу сначала создавать тему:) Подробности описаны в CONTRIBUTING.md.
Полезные ссылки
• https://www.youtube.com/watch?v=sH4XF6pKKmk
• https://www.reddit.com/r/Python/comments/3cu6ej/what_are_some_wtf_things_about_python
• https://sopython.com/wiki/Common_Gotchas_In_Python
• https://stackoverflow.com/questions/530530/python-2-x-gotchas-and-landmines
• https://stackoverflow.com/questions/1011431/common-pitfalls-in-python (В этой теме на StackOverflow приведены полезные рекомендации, что можно и что нельзя делать в Python.) - Выражение
tmnhy
Начинал читать ещё оригинал, но и с переводом тоже самое, после первых двух примеров интерес пропадает.
acmnu
Да уж. Как-то ждешь больше жести от такой статьи, а там про то что можно букву не латинскую вставить.
zedroid
Пример с:
Меня в свое время очень удивил.
Dark_Daiver
Разве такая фигня не во всех языках где сложные типы создаются на куче?
equand
Чем? Всё ссылка — один из первых уроков Пайтона.
Sly_tom_cat
Именно так — про микс табуляций с пробелами — уже даже не смешно :(
acmnu
И вроде в Python 3 не актуально — не даст запуститься.
shpaker
Ещё менее смешно про вставку кириллического символа.
Что за дерьмо, Mail.Ru?
Ritorno
Специально докрутил эту монструозную стену текста до комментов, чтобы увидеть там этот, и не прогадал.
ZaEzzz
Сделал то же самое после первого примера.
Написали, что код должен выполняться на всех версиях, если иное не указано. Ок.
mayorovp
Так ведь там указано, что конкретно этот пример — только для третьего.
ZaEzzz
В объяснении? Я до него не дошел. В надежде на освещение действительно интересных нюансов и маленький челендж, мне было интересно проверить пример и подумать над результатом, а не читать почему так произошло. Но меня сразу постигло разочарование.
P.S. Если честно, я надеялся увидеть статью из разряда «ЯП: фрактал ...», но не сложилось (про нелюбимые мною PHP и JS написали, а про обожаемый питон нет).
gnomeby
Дальше более полезные примеры
tmnhy
Полезные в чём? Если кодить «методом тыка», то получишь непредсказуемый результат? Так это «капитанство».
Кмк, кто удосужился хотя бы пробежаться по принципам ЯП, не словит такие ситуации, а словит, просто поправит опечатки в коде.
PiaFraus
Я думал хабр сума сошёл. Кто меня на хабре упоминать будет, а оказывается у меня тут такой же ник как на гитхабе.
Deepwalker
Про клаузу и генератор, где несовпадение времени обработки — это ужас. Во-первых что такое «клауза»? Во-вторых объяснение поведения описано просто ужасно, in срабатывает, что??
ZyXI
Насколько я понял, имеется ввиду, что «
in array
вызоветiter(array)
и сохранит итератор ещё при создании генератора, а условия будут проверяться только во время собственно итерации, которая ленивая».frol
«Клауза» получилась из clause, в русском это обычно замыканием называют.
mayorovp
Замыкание — closure. А clause в данном случае — конструкция.
frol
Ваша правда, глупость выдал с переводом. Посылаю голову пеплом.
TyVik
Чтобы проверить примеры на python? <собака-подозревака>
kalininmr
непонимаю а в чем WTF (ну кроме парочки случаев)?
все так и задуманно.
странно удивлятся например что list — ссылочный тип и т.д.
DarkGenius
Вы все примеры можете вычислить в уме и получить ровно тот же результат, что и интерпретатор Python?
Deepwalker
Практически. Все эти штучки отлавливаются в первые полгода знакомства с языком. Дальше все логично.
То есть вообще не то же самое что с JS, где про сравнения я вот прям ну никак не скажу что же там получится в итоге, и в больших сомнениях что у меня есть ресурсы это держать в памяти.
equand
Большинство примеров этого списка упираются в первое правило Питона — все есть ссылка.
Проверяется легко id(var) и type(var)
kalininmr
почти все. о некоторых не задумывался.
со слешами меня просто расмешило.
а исключения через запятую… а почему не попробовали скажем три? удивились бы ошибке компиляции?
zaglyanuvshy
Что-то не удается воспроизвести пример из «a и b ссылаются на один объект при инициализации с одинаковым значением в одной строке»:
Результат, все переменные ссылаются на один объект:
Версия Python 3.5.3
ZyXI
В этом примере всё зависит от версии Python: PyPy, PyPy3 и CPython начиная с какой?то из третьих версий (но точно больше 3.4.5: у меня именно она и
a is not b
в данном случае) применяют какую?то оптимизацию, из?за чегоa is b
в вашем случае, тогда как другие версии её не применяют. В общем, просто учитывайте, что257 is 257
не гарантируется.devpony
Только в интерпретаторе.
Dim0v
Надо в интерпретаторе запускать. Там код построчно выполняется и оптимизируется, соответственно, только одна строчка за раз. Если в виде .py файла запускать, то оптимизирован будет весь файл и тогда уже все 4 переменные будут указывать в одно место.
zaglyanuvshy
ммм… Спасибо, не знал.
kalininmr
а там куча нюансов.
у меня в 3.5 работает а в 3.6 — нет.
tema_sun
Как скучно я живу.
gaf
Да уж. Примеры из разряда второй день пишу на Python. Вы бы еще привели пример изменения значения по умолчанию параметра функции
gnomeby
Он там был
gaf
оу, воу — не заметил. сори
kalininmr
это же классика :)
bodrilko
Метки: python wtf никто не читает теги
камон, ну конечно же мы читаем теги ^_^
helpik94
Такое ощущение, что все начали писать на питоне сразу без ошибок. «Ой-ой, как можно не знать таких вещей.» Все же ошибались и когда-то чего-то не знали.
monester
Ещё из неочевидного:
В документации python2/3 написано что если от пустой строки сделать
split
с разделителемNone
то вернется пустой массив.Если использовать разделитель отличный от None тогда вернется массив с одним элементом —
''
.https://docs.python.org/2/library/string.html#string.split
https://docs.python.org/3/library/stdtypes.html#str.split
mayorovp
Тут скорее неочевидным является само существование формы вызова
split()
без параметра.SolidMinus
Wtf будет жесткий если случайно в арифметике опечататься. Это не будет синтаксической ошибкой.
ArtyL
А меня в свое позабавил такой пример (строчка которая никогда не выполниться влияет на результат):
kalininmr
занятно…
вот тут не поспоришь вполне себе WTF