MyTetra Share
Делитесь знаниями!
11 ловушек и странностей в Python
Время создания: 19.06.2023 13:50
Текстовые метки: python, особенности, странности, неоднозначности, неожиданность, ловушки, объекты, классы, исключения, списки, строки, кортежи, присвоение, присваивание, изменение
Раздел: Компьютер - Программирование - Язык Python
Запись: xintrea/mytetra_syncro/master/base/1687171830beqxu9xxdv/text.html на raw.github.com

Описанное делее - не обязательно дефекты; скорее, это - особенности языка (точнее, их побочные эффекты), которые часто ведут себя как ловушки для новичков, и иногда - для опытных программистов. Причиной попадания в эти ловушки является недостаточное понимание некоторых основных принципов, присущих языку Python.

Цель написания этого документа - предоставить некоторую помощь для тех, кто новичок в Python. Лучше изучить возможные ловушки заранее, чем столкнуться с ними в коде, готовом к вводу в эксплуатацию, как раз перед наступлением сроков сдачи. :-} Эта статься - не критика языка как такового; как уже было сказано, эти ловушки возникают не из-за дефектов языка.


1. Непоследовательная расстановка отступов в коде

Ага, это — как раз то что нужно для начала. Многие новички приходят к Python из языков, где пробельные символы “ничего не меняют”, и очень удивлены, когда выясняют на своей шкуре, что непоследовательная расстановка отступов в коде Python наказуема.

Что делать: быть последовательными. Используйте только пробелы или только символы табуляции, но ни в коем случае и то и другое вместе. Нормальный редактор в этом поможет.


2. Присваивание, или имена и объекты

Те, кто приходит из языков со статической типизацией, таких как Pascal и C, часто ожидают, что переменные и присваивание в Python работают так же как в их любимом языке программирования. На первый взгляд, действительно, никакой разницы:


a = b = 3

a = 4

print a, b  # 4, 3


Однако, вскоре они попадутся, когда будут присваивать изменяемые объекты. Часто в связи с этим Python обвиняют в том, что он обходится с изменяемыми и неизменяемыми объектами по-разному.


a = [1, 2, 3]

b = a

a.append(4)

print b

# b теперь тоже [1, 2, 3, 4]


Что на самом деле тут происходит, так это то, что в результате такого выражения как a = [1, 2, 3] происходят две вещи:

  • создается объект, в данном случае это список со значением [1, 2, 3];
  • этот объект связывается с именем a в локальном пространстве имён.

b = a затем связывает имя b с тем же объектом списка (на который уже ссылается a). Как только вы поймете это, вам нетрудно будет понять, что происходит, когда выполняется a.append(4) … это выражение изменяет список, на который ссылаются и a, и b.

Предположение о том, что изменяемые и неизменяемые объекты в Python обрабатываются по-разному — неверно. В случае присваивания списка происходит абсолютно то же самое, то и в случае a = 3 и b = a. a и b после этого также ссылаются на один и тот же объект - целое число со значением 3. При этом, из-за того, что целые числа - неизменяемые объекты, вы не столкнётесь с побочным эффектом.

Что делать: читать это. Чтобы избавиться от нежелательных побочных эффектов, копируйте объекты (используя функцию copy, оператор среза (slice) и т.д.). Python никогда не копирует объекты неявно.


3. Оператор +=

В языках, имеющих в предках язык C, расширенные операторы присваивания, такие как +=, служат для замены более длинных выражений. Например, x += 42; — это “синтаксический сахар” для x = x + 42;

Так что вы могли бы подумать, что в Python вас ждет то же самое. Действительно, на первый взгляд так оно и есть:


a = 1

a = a + 42

# a is 43


a = 1

a += 42

# a теперь 43


Однако, для изменяемых объектов x += y не обязательно значит то же самое, что и x = x + y. Возьмем списки:


>>> z = [1, 2, 3]

>>> id(z)

24213240

>>> z += [4]

>>> id(z)

24213240

>>> z = z + [5]

>>> id(z)

24226184


x += y изменяет список по месту, аналогично результату метода extend. x = x + y создает новый объект класса list и связывает его с именем x, что есть нечто другое. Тонкое, невидимое различие, которое может привести к невидимым и трудно отлавливаемым багам.

Но более того, это также приводит к неожиданным последствиям, когда мы смешиваем изменяемые и неизменяемые объекты-контейнеры:


>>> t = ([],)

>>> t[0] += [2, 3]

Traceback (most recent call last):

 File "<input />", line 1, in ?

TypeError: object doesn't support item assignment

>>> t

([2, 3],)


В самом деле, кортежи не поддерживают присваивание по ключу. Однако после применения +=, список внутри кортежа на самом деле изменился!! Причина опять в том же: += изменяет объект по месту. Присваивание по ключу не работает, однако на момент возникновения исключения, элемент кортежа уже был изменен по месту.

Что делать: в зависимости от вашего личного отношения к этой проблеме, вы можете: не использовать += вообще; использовать только для целых чисел; жить с этим. :-)


4. Атрибуты классов и атрибуты экземпляров классов

Здесь возможны по крайней мере две ситуации, когда что-то пойдет не так. Во-первых, новички постоянно присваивают атрибуты в классе (вместо того, чтобы делать это в экземпляре класса), и потом удивляются, что значения этих атрибутов разделяемы между всеми экземплярами:


>>> class Foo:

...     bar = []

...     def __init__(self, x):

...         self.bar.append(x)

...

>>> f = Foo(42)

>>> g = Foo(100)

>>> f.bar, g.bar

([42, 100], [42, 100])


Это - не дефект, наоборот - удобная особенность, которая может быть очень полезна во многих ситуациях. Непонимание возникает от того, что использованы атрибуты класса, а не атрибуты экземпляра, возможно от того, что в Python создание атрибутов экземпляра происходит не так как в других языках. В C++, Object Pascal и т.д., атрибуты должны декларироваться в определении класса.

Другая (небольшая) ловушка состоит в том, что self.foo может ссылаться на два рода вещей: атрибут foo экземпляра класса, или, в его отсутствие, атрибут foo самого класса. Сравните:


>>> class Foo:

...     a = 42

...     def __init__(self):

...         self.a = 43

...

>>> f = Foo()

>>> f.a

43


и


>>> class Foo:

...     a = 42

...

>>> f = Foo()

>>> f.a

42


В первом примере f.a ссылается на атрибут экземпляра класса, со значением 43. Он переопределяет атрибут самого класса со значением 42. Во втором примере атрибута a экземпляра класса не существует, так что f.a ссылается на атрибут класса.

Такой код объединяет оба случая:


>>> class Foo:

...

...     bar = []

...     def __init__(self, x):

...         self.bar = self.bar + [x]

...

>>> f = Foo(42)

>>> g = Foo(100)

>>> f.bar

[42]

>>> g.bar

[100]


В выражении self.bar = self.bar + [x] ссылки self.bar — не одно и то же… вторая ссылается на атрибут bar класса, а результат выражения связывается с атрибутом экземпляра.

Что делать: это различие может по началу путать, но эта путаница не является непреодолимой. Используйте атрибуты класса когда вы хотите разделить что-то между множеством экземпляров этого класса. Чтобы избежать двусмысленности, ссылайтесь на них, используя self.__class__.name, а не self.name, даже тогда, когда соответствующий атрибут экземпляра не существует. Используйте атрибуты экземпляра для хранения данных, уникальных для данного экземпляра, и ссылайтесь на него, используя self.name.

Как было замечено, случаи из параграфов #3 и #4 можно объединить так, чтобы повеселиться пуще прежнего:


>>> class Foo:

...     bar = []

...     def __init__(self, x):

...             self.bar += [x]

...

>>> f = Foo(42)

>>> g = Foo(100)

>>> f.bar

[42, 100]

>>> g.bar

[42, 100]


Опять, причина в том, что self.bar += something — это нечто другое, чем self.bar = self.bar + something. self.bar здесь ссылается на Foo.bar, так что f и g изменяют один и тот же список.


5. Изменяемые объекты в аргументах функций по умолчанию

Этот зверь кусает новичков снова и снова. На самом деле, это - вариант случая #2, в комбинации с поведением аргументов по умолчанию, о котором программист знает не всё. Возьмем такую функцию:


>>>  def popo(x=[]):

...     x.append(777)

...     print x

...

>>> popo([1, 2, 3])

[1, 2, 3, 777]

>>> x = [1, 2]

>>> popo(x)

[1, 2, 777]

>>> x

[1, 2, 777]


Что и следовало ожидать. Но:


>>> popo()

[777]

>>> popo()

[777, 777]

>>> popo()

[777, 777, 777]


Возможно, вы ждали, что результат будет [777] во всех случаях… в конце концов, когда popo() вызвана без аргументов, она берет [] как аргумент по умолчанию для x, так? Не так. Аргумент по умолчанию связывается с переменной только один раз, когда функция была определена, а не тогда, когда она была вызвана. Другими словами, для функции f(x=[]), значение связано с переменной x не тогда, когда функция вызвана. x связана со значением [] когда мы определили f, вот так. Ну и, так как это — изменяемый объект, и он был изменен, следующий вызов получит тот же самый список (содержимое которого уже изменилось) в качестве аргумента по умолчанию.

Что делать: иногда это поведение может быть полезным. В общем случае, имейте в виду возможность нежелательных побочных эффектов.


6. UnboundLocalError

Согласно справочному руководству, эта ошибка возникает когда имя “ссылается на локальную переменную, которая пока не была связана с этим именем”. Звучит загадочно. Лучше всего проиллюстрировать на простом примере:


>>> def p():

...     x = x + 2

...

>>> p()

Traceback (most recent call last):

 File "<input />", line 1, in ?

 File "<input />", line 2, in p

UnboundLocalError: local variable 'x' referenced before assignment


Внутри p, утверждение x = x + 2 не может быть выполнено, т.к. x в выражении x + 2 еще не содержит значения. Это звучит разумно; нельзя ссылаться на имя, с которым пока еще не связано значение. Но теперь посмотрим на это:


>>> x = 2

>>> def q():

...     print x

...     x = 3

...     print x

...

>>> q()

Traceback (most recent call last):

 File "<input />", line 1, in ?

 File "<input />", line 2, in q

UnboundLocalError: local variable 'x' referenced before assignment


Вы можете предполагать, что этот кусок кода правильный — сначала он напечатает 2 (взяв глобальную x), а затем присвоит локальной переменной x значение 3, и выведет его (3). Тем не менее это не работает. Из-за правил области видимости, что описано в руководстве:

“Если имя связывается со значением в блоке — это локальная переменная этого блока. Если имя связывается со значением в модуле — это глобальная переменная. (Переменные в пространстве блока модуля — и локальные, и глобальные.) Если переменная используется в блоке, но не определена в нем, то это - “свободная” переменная.

Если имя вообще не найдено, возникает исключение NameError. Если имя ссылается на локальную переменную, которая пока что не была связана со значением, возникает исключение UnboundLocalError.”

Другими словами: переменная в функции может быть либо локальной, либо глобальной, но не то и другое вместе. (Неважно, что вы связываете ее со значением в локальном пространстве имен позже.) В примере выше, Python определил, что переменная x — локальная (в соответствие с правилами). Но в процессе исполнения он встретился с print x, а x пока не имеет значения… отсюда и ошибка.

Заметьте, что в том случае, если функция была бы определена как print x или x = 3; print x, это было бы вполне правильно.

Что делать: не путать локальные переменные с глобальными.


7. Ошибки при округлении чисел с плавающей запятой

Когда вы используете числа с плавающей запятой, вывод их значений может вас удивить. Дела становятся еще интересней, когда вы заметите, что представления, возвращаемые функциями str() и repr() могут различаться. Показателен пример:


>>> c = 0.1

>>> c

0.10000000000000001

>>> repr(c)

'0.10000000000000001'

>>> str(c)

'0.1'


Из-за того, что многие числа не могут быть представлены в двоичной системе (в которой работает аппаратное обеспечение компьютера), их значение в десятичной системе может быть выведено лишь приблизительно.

Что делать: читать руководство.


8. Объединение строк

Эта западня — нечто необычное. Во многих языках программирования, объединение строк с использованием оператора + или чего-либо аналогичного может быть весьма эффективной процедурой. Например, в Pascal:


var S : String;

for I := 1 to 10000 do begin

   S := S + Something(I);

end;


(Этот кусок кода подразумевает, что тип строки поддерживает значение длиннее, чем 255 символов, что, между прочим, было максимумом в Turbo Pascal… ;-)

Подобный код в Python скорее всебудет весьма неэффективен. Т.к. строки в Python — неизменяемые объекты (в чем они отличаются от строк в Pascal), с каждой итерацией создается новая строка (а старая — отбрасывается). Это может вылиться в неожиданные потери в производительности. Использование + или += для объединения небольших строк или небольших изменений вполне допустимо, но обычно внутри циклов так делать не рекомендуется.

Что делать: если только есть возможность, создавайте список значений, а затем — string.join (или строковый метод join()) для того, чтобы объединить все эти значения в одну строку. Иногда это может привести к значительному ускорению работы кода.

Для иллюстрации приведу простой тест производительности. (timeit — простая функция, которая вызывает другую функцию и возвращает время, затраченное на выполнение этой функции, в секундах.)


>>> def f():

...     s = ""

...     for i in range(100000):

...         s = s + "abcdefg"[i % 7]

...

>>> timeit(f)

23.7819999456

>>> def g():

...     z = []

...     for i in range(100000):

...         z.append("abcdefg"[i % 7])

...     return ''.join(z)

...

>>> timeit(g)

0.343000054359


Дополнение: это было исправлено в CPython 2.4. Согласно тексту What’s New in Python 2.4: “Объединение строк в выражениях вида s = s + "abc" и s += "abc" теперь происходит более эффективно в определенных обстоятельствах. Эта оптимизация не присутствует в других реализациях Python, таких как Jython, так что не нужно на нее полагаться; использование метода строк join() все еще рекомендовано в тех случаях когда нужно объединить большое количество строк.”


9. Двоичный режим для файлов

Или, скорее, неиспользование двоичного режима, которое может служить источником путаницы. Некоторые операционные системы, например Windows, видят различия между двоичными файлами и текчтовыми файлами. Для иллюстрации, файлы в Python могут быть открыты в двоичном режиме и текстовом режиме:


f1 = open(filename, "r") # текст

f2 = open(filename, "rb") # двоичный


В текстовом режиме стромогут быть разделены любым из символов новой строки/возврата каретки (\n, \r, or \r\n). В двоичном режиме этого не происходит. Также, под Windows, когда происходит чтение файла в текстовом режиме, символы новой строки представлены в Python как \n (универсальное решение); в двоичном режиме их значение — \r\n. Следовательно, чтение данных может приводить к различным результатам в разных режимах.

Также есть системы, которые не видят разницы между двоичным и текстовым режимом. Например, под Unix файлы всегда открываются в двоичном режиме. Из-за этого код, написанный под Unix может открывать файлы в режиме ‘r’, но под Windows он будет работать по-другому. Или же, некто, привыкший к Unix может использовать флаг ‘r’ под Windows, и будет озадачен результатами этого.

Что делать: использовать корректные флаги — ‘r’ для текстового режима (даже под Unix), ‘rb’ для двоичного режима.


10. Перехват нескольких исключений

Время от времени вам надо перехватить несколько исключений в одном except. Очевидное решение выглядит так:


try:

   ...нечто, что вызывает ошибку...

except IndexError, ValueError:

   # ожидаем перехват IndexError и ValueError

   # и зря!


Так не работает… причина становится ясна, когда мы сравним это с таким кодом:


>>> try:

...     1/0

... except ZeroDivisionError, e:

...     print e

...


integer division or modulo by zero


Первый “аргумент” для except — это класс исключения, второй — необязательное имя, которое будет использовано для связывания с конкретным экземпляром исключения, который и был вызван. Так что, в коде с ошибкой, except перехватывает IndexError, а затем связывает имя ValueError с экземпляром исключения. Скорее всего, это не то, что нам нужно. ;-)

Так лучше:


try:

   ...нечто, что вызывает ошибку...

except (IndexError, ValueError):

   # перехватывает IndexError и ValueError


Что делать: когда хотите перехватить несколько исключений в одном except, пишите скобки, чтобы определить кортеж исключений.


11. Отсутствие методов size() или count() для строк и списков

Да, при достаточно развитых методах работы со строками и списками, в Python для объектов строк и списков элементарно нет метода, возвращающего размер. Так исторически сложилось.


Вместо этого предлагается пользоваться глобальной функцией len(), в которую скармливается строка/список, а она в свою очередь возвращает размер.



myString = "Hello world"

print("Количество символов:", len(myString))

# Результат - 11


myList = ['one', 'two', 3, 4.8]

print("Количество элементов списка:", len(myList))

# Результат - 4



Вообще, функция len() работает с любыми перечислимыми объектами. Именно поэтому ей можно скармливать строку (она будет рассмотрена как список символов) и словарь (он будет рассмотрен как список пар ключ-значение)



letters = {"A": 1, "B": 20, "C": 300, "D": 4000, "E": 50000,}

print("количество элементов словаря:", len(letters))

# Результат - 5



Таким образом, функция len() работает еще и с кортежами, множествами, байтовыми объектами и вообще с любыми коллекциями и объектами, реализующими метод __len__().


Так же в этом разделе:
 
MyTetra Share v.0.60
Яндекс индекс цитирования