Python 2.x получил ошибки и мины
Цель моего вопроса - укрепить свою базу знаний с помощью Python и получить более полное представление о ней, которая включает в себя знание ее недостатков и неожиданностей. Если говорить конкретно, меня интересует только интерпретатор CPython.
Я ищу что-то похожее на то, что узнал из моего вопроса о наземных минах PHP, где некоторые ответы были мне хорошо известны, но пара была ужасающей.
Обновление: Видимо, один, может быть, два человека расстроены тем, что я задал вопрос, на который уже частично ответили за пределами переполнения стека. В качестве своего рода компромисса вот URL http://www.ferg.org/projects/python_gotchas.html
Обратите внимание, что один или два ответа здесь уже являются оригинальными из того, что было написано на сайте, указанном выше.
22 ответа
Выражения в аргументах по умолчанию вычисляются при определении функции, а не при ее вызове.
Пример: рассмотрим значение по умолчанию для аргумента текущего времени:
>>>import time
>>> def report(when=time.time()):
... print when
...
>>> report()
1210294387.19
>>> time.sleep(5)
>>> report()
1210294387.19
when
аргумент не меняется. Он оценивается при определении функции. Он не изменится, пока приложение не будет перезапущено.
Стратегия: вы не споткнетесь об этом, если вы по умолчанию None
а затем сделайте что-нибудь полезное, когда увидите это:
>>> def report(when=None):
... if when is None:
... when = time.time()
... print when
...
>>> report()
1210294762.29
>>> time.sleep(5)
>>> report()
1210294772.23
Упражнение: чтобы убедиться, что вы поняли: почему это происходит?
>>> def spam(eggs=[]):
... eggs.append("spam")
... return eggs
...
>>> spam()
['spam']
>>> spam()
['spam', 'spam']
>>> spam()
['spam', 'spam', 'spam']
>>> spam()
['spam', 'spam', 'spam', 'spam']
Вы должны знать, как переменные класса обрабатываются в Python. Рассмотрим следующую иерархию классов:
class AAA(object):
x = 1
class BBB(AAA):
pass
class CCC(AAA):
pass
Теперь проверьте вывод следующего кода:
>>> print AAA.x, BBB.x, CCC.x
1 1 1
>>> BBB.x = 2
>>> print AAA.x, BBB.x, CCC.x
1 2 1
>>> AAA.x = 3
>>> print AAA.x, BBB.x, CCC.x
3 2 3
Удивлены? Вы не будете, если будете помнить, что переменные класса внутренне обрабатываются как словари объекта класса. Для операций чтения, если имя переменной не найдено в словаре текущего класса, его ищут в родительских классах. Итак, следующий код снова, но с пояснениями:
# AAA: {'x': 1}, BBB: {}, CCC: {}
>>> print AAA.x, BBB.x, CCC.x
1 1 1
>>> BBB.x = 2
# AAA: {'x': 1}, BBB: {'x': 2}, CCC: {}
>>> print AAA.x, BBB.x, CCC.x
1 2 1
>>> AAA.x = 3
# AAA: {'x': 3}, BBB: {'x': 2}, CCC: {}
>>> print AAA.x, BBB.x, CCC.x
3 2 3
То же самое касается обработки переменных класса в экземплярах классов (этот пример рассматривается как продолжение предыдущего):
>>> a = AAA()
# a: {}, AAA: {'x': 3}
>>> print a.x, AAA.x
3 3
>>> a.x = 4
# a: {'x': 4}, AAA: {'x': 3}
>>> print a.x, AAA.x
4 3
Циклы и лямбды (или любое замыкание, действительно): переменные связаны по имени
funcs = []
for x in range(5):
funcs.append(lambda: x)
[f() for f in funcs]
# output:
# 4 4 4 4 4
Обходной путь - создание отдельной функции или передача аргументов по имени:
funcs = []
for x in range(5):
funcs.append(lambda x=x: x)
[f() for f in funcs]
# output:
# 0 1 2 3 4
Динамическое связывание делает нахождение в ваших именах переменных на удивление трудным для поиска. Легко потратить полчаса на исправление тривиальной ошибки.
РЕДАКТИРОВАТЬ: пример...
for item in some_list:
... # lots of code
... # more code
for tiem in some_other_list:
process(item) # oops!
Один из самых больших сюрпризов, которые у меня когда-либо были с Python:
a = ([42],)
a[0] += [43, 44]
Это работает, как и следовало ожидать, за исключением вызова ошибки TypeError после обновления первой записи кортежа! Так a
будет ([42, 43, 44],)
после выполнения +=
заявление, но все равно будет исключение. Если вы попробуете это с другой стороны
a = ([42],)
b = a[0]
b += [43, 44]
вы не получите ошибку.
try:
int("z")
except IndexError, ValueError:
pass
причина, по которой это не работает, заключается в том, что IndexError - это тип исключения, которое вы перехватываете, а ValueError - это имя переменной, которой вы назначаете исключение.
Правильный код для перехвата нескольких исключений:
try:
int("z")
except (IndexError, ValueError):
pass
Некоторое время назад было много дискуссий о скрытых языковых возможностях: hidden-features-of-python. Где были упомянуты некоторые подводные камни (и некоторые хорошие вещи тоже).
Также вы можете проверить Python Warts.
Но для меня целочисленное деление - это гоча:
>>> 5/2
2
Вы, вероятно, хотели:
>>> 5*1.0/2
2.5
Если вы действительно хотите это (C-like) поведение, вы должны написать:
>>> 5//2
2
Как это будет работать и с плавающими (и это будет работать, когда вы в конечном итоге перейдете на Python 3):
>>> 5*1.0//2
2.0
GvR объясняет, как целочисленное деление стало работать так же, как и в истории Python.
Нарезка списков доставила мне много горя. Я на самом деле считаю следующее поведение ошибкой.
Определить список х
>>> x = [10, 20, 30, 40, 50]
Индекс доступа 2:
>>> x[2]
30
Как вы ожидаете.
Разрезать список от индекса 2 до конца списка:
>>> x[2:]
[30, 40, 50]
Как вы ожидаете.
Индекс доступа 7:
>>> x[7]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
Опять же, как вы ожидаете.
Тем не менее, попробуйте нарезать список от индекса 7 до конца списка:
>>> x[7:]
[]
???
Средство состоит в том, чтобы поставить много тестов при использовании нарезки списка. Я хотел бы просто получить ошибку вместо этого. Намного легче отлаживать.
Не включая __init__
.py в ваших пакетах. Этот до сих пор иногда получает меня.
Единственное замечание / сюрприз, с которым я имел дело, - это GIL от CPython. Если по какой-либо причине вы ожидаете, что потоки Python в CPython будут работать одновременно... ну, это не так, и это довольно хорошо задокументировано толпой Python и даже самим Гвидо.
Длинное, но подробное объяснение многопоточности CPython и некоторых вещей, происходящих под капотом, и почему настоящий параллелизм с CPython невозможен. http://jessenoller.com/2009/02/01/python-threads-and-the-global-interpreter-lock/
Джеймс Дюмей красноречиво напомнил мне еще одну ошибку Питона:
Не все "включенные батареи" Python замечательны.
Конкретным примером Джеймса были библиотеки HTTP: httplib
, urllib
, urllib2
, urlparse
, mimetools
, а также ftplib
, Некоторые функции дублируются, а некоторые ожидаемые функции полностью отсутствуют, например, обработка перенаправления. Честно говоря, это ужасно.
Если мне когда-нибудь придется что-то захватывать через HTTP, я использую модуль urlgrabber, разветвленный в проекте Yum.
def f():
x += 1
x = 42
f()
приводит к UnboundLocalError
потому что локальные имена обнаруживаются статически. Другой пример будет
def f():
print x
x = 43
x = 42
f()
Плавания не печатаются с полной точностью по умолчанию (без repr
):
x = 1.0 / 3
y = 0.333333333333
print x #: 0.333333333333
print y #: 0.333333333333
print x == y #: False
repr
печатает слишком много цифр:
print repr(x) #: 0.33333333333333331
print repr(y) #: 0.33333333333300003
print x == 0.3333333333333333 #: True
Непреднамеренное смешивание классов oldstyle и newstyle может привести к загадочным ошибкам.
Скажем, у вас есть простая иерархия классов, состоящая из суперкласса A и подкласса B. Когда создается экземпляр B, конструктор A должен вызываться первым. Код ниже правильно делает это:
class A(object):
def __init__(self):
self.a = 1
class B(A):
def __init__(self):
super(B, self).__init__()
self.b = 1
b = B()
Но если вы забыли сделать класс newstyle и определить его следующим образом:
class A:
def __init__(self):
self.a = 1
Вы получаете эту трассировку:
Traceback (most recent call last):
File "AB.py", line 11, in <module>
b = B()
File "AB.py", line 7, in __init__
super(B, self).__init__()
TypeError: super() argument 1 must be type, not classobj
Два других вопроса, касающихся этой проблемы, являются 489269 и 770134
x += [...]
это не то же самое, что x = x + [...]
когда x
это список`
>>> x = y = [1,2,3]
>>> x = x + [4]
>>> x == y
False
>>> x = y = [1,2,3]
>>> x += [4]
>>> x == y
True
Один создает новый список, а другой изменяет на месте
Вы не можете использовать locals()['x'] = что угодно, чтобы изменить значения локальной переменной, как вы могли ожидать.
This works:
>>> x = 1
>>> x
1
>>> locals()['x'] = 2
>>> x
2
BUT:
>>> def test():
... x = 1
... print x
... locals()['x'] = 2
... print x # *** prints 1, not 2 ***
...
>>> test()
1
1
Это фактически сожгло меня в ответе здесь на SO, так как я проверил это вне функции и получил изменение, которое я хотел. Впоследствии я обнаружил, что это упоминается и противопоставляется случаю globals() в "Dive Into Python". Смотрите пример 8.12. (Хотя это не замечает, что изменение через localals () будет работать на верхнем уровне, как я покажу выше.)
Повторение списка с вложенными списками
Это застало меня сегодня врасплох и потратило час на отладку:
>>> x = [[]]*5
>>> x[0].append(0)
# Expect x equals [[0], [], [], [], []]
>>> x
[[0], [0], [0], [0], [0]] # Oh dear
Объяснение: проблема со списком Python
Python 2 имеет несколько удивительное поведение при сравнении:
>>> print x
0
>>> print y
1
>>> x < y
False
В чем дело? repr()
в помощь:
>>> print "x: %r, y: %r" % (x, y)
x: '0', y: 1
Использование переменных класса, когда вы хотите переменные экземпляра. В большинстве случаев это не вызывает проблем, но если это изменчивое значение, это вызывает сюрпризы.
class Foo(object):
x = {}
Но:
>>> f1 = Foo()
>>> f2 = Foo()
>>> f1.x['a'] = 'b'
>>> f2.x
{'a': 'b'}
Вы почти всегда хотите переменные экземпляра, которые требуют назначения внутри __init__
:
class Foo(object):
def __init__(self):
self.x = {}
Если вы присваиваете переменную внутри функции, Python предполагает, что переменная определена внутри этой функции:
>>> x = 1
>>> def increase_x():
... x += 1
...
>>> increase_x()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in increase_x
UnboundLocalError: local variable 'x' referenced before assignment
использование global x
(или же nonlocal x
в Python 3) чтобы объявить, что вы хотите установить переменную, определенную вне вашей функции.
Из-за "правдивости" это имеет смысл:
>>>bool(1)
True
но вы не можете ожидать, что это пойдет другим путем:
>>>float(True)
1.0
Это может быть уловкой, если вы преобразуете строки в числовые, и ваши данные имеют значения True/False.
Значения range(end_val)
не только строго меньше, чем end_val
, но строго меньше, чем int(end_val)
, Для float
аргумент range
это может быть неожиданным результатом:
from future.builtins import range
list(range(2.89))
[0, 1]
Если вы создаете список таким образом:
arr = [[2]] * 5
print arr
[[2], [2], [2], [2], [2]]
Затем это создает массив со всеми элементами, указывающими на один и тот же объект! Это может создать настоящую путаницу. Учти это:
arr[0][0] = 5
тогда если вы печатаете обр
print arr
[[5], [5], [5], [5], [5]]
Правильный способ инициализации массива, например, со списком:
arr = [[2] for _ in range(5)]
arr[0][0] = 5
print arr
[[5], [2], [2], [2], [2]]