Чистыйкод дляпродолжающи х
Скачать 7.85 Mb.
|
mySandwich = addIngredient('avocado') >>> mySandwich ['bread', 'avocado', 'bread'] >>> anotherSandwich = addIngredient('lettuce') >>> anotherSandwich ['bread', 'lettuce', 'avocado', 'bread'] Так как addIngredient('lettuce') использует тот же список аргументов по умол- чанию, что и предыдущие вызовы, в который уже добавлен элемент 'avocado' , вместо ['bread', 'lettuce', 'bread'] функция возвращает ['bread', 'lettuce', 'avocado', 'bread'] . Строка 'avocado' появляется снова, потому что список для параметра sandwich будет тот же, что и при последнем вызове функции. Создается только один список ['bread', 'bread'] , потому что команда def функции выпол- няется только один раз, а не при каждом вызове функции. Процесс выполнения этого кода наглядно показан на https://autbor.com/sandwich/. Если вам когда-либо потребуется использовать список или словарь в качестве аргумента по умолчанию, то питоническим решением будет назначить аргумент по умолчанию None . Затем в функцию включается код, который проверяет эту ситуацию и создает новый список или словарь при каждом вызове функции. Тем самым гарантируется, что функция будет создавать новый изменяемый объект при каждом вызове функции, вместо того чтобы делать это только один раз при определении функции, как в следующем примере: >>> def addIngredient(ingredient, sandwich=None): ... if sandwich is None: ... sandwich = ['bread', 'bread'] ... sandwich.insert(1, ingredient) 176 Глава 8.Часто встречающиеся ловушки Python ... return sandwich >>> firstSandwich = addIngredient('cranberries') >>> firstSandwich ['bread', 'cranberries', 'bread'] >>> secondSandwich = addIngredient('lettuce') >>> secondSandwich ['bread', 'lettuce', 'bread'] >>> id(firstSandwich) == id(secondSandwich) False ❶ Обратите внимание: firstSandwich и secondSandwich не используют одну и ту же ссылку на список ❶ , потому что sandwich = ['bread', 'bread'] создает новый объ- ект списка при каждом вызове addIngredient() , а не однократно при определении addIngredient() К числу изменяемых типов данных относятся списки, словари, множества и объек- ты, создаваемые командой class . Не задавайте объекты этих типов как аргументы по умолчанию в командах def Не создавайте строки посредством конкатенации В Python строки являются неизменяемыми (immutable) объектами. Это означает, что строковые значения не могут изменяться, и любой код, который на первый взгляд изменяет строку, в действительности создает новый объект строки. Напри- мер, каждая из следующих операций изменяет содержимое переменной spam — не изменением строкового значения, а заменой его новым строковым значением с новой идентичностью: >>> spam = 'Hello' >>> id(spam), spam (38330864, 'Hello') >>> spam = spam + ' world!' >>> id(spam), spam (38329712, 'Hello world!') >>> spam = spam.upper() >>> id(spam), spam (38329648, 'HELLO WORLD!') >>> spam = 'Hi' >>> id(spam), spam (38395568, 'Hi') >>> spam = f'{spam} world!' >>> id(spam), spam (38330864, 'Hi world!') Обратите внимание: при каждом вызове id(spam) возвращается новая идентич- ность, потому что объект строки в spam не изменяется: он заменяется совершенно Не создавайте строки посредством конкатенации 177 новым объектом строки с новой идентичностью. При создании новых строк с ис- пользованием f-строк, строкового метода format() или спецификаторов формата %s также создаются новые объекты строк, как и при конкатенации строк. Обычно эта техническая подробность ни на что не влияет. Python — высокоуровневый язык, который берет на себя многие технические решения, чтобы вы могли сосредото- читься на создании программы. Каждая итерация цикла создает новый объект строки, а старый объект строки при этом пропадает; в коде происходящее выглядит как выполнение конкатенаций в цикле for или while , как в следующем примере: >>> finalString = '' >>> for i in range(100000): ... finalString += 'spam ' >>> finalString spam spam spam spam spam spam spam spam spam spam spam spam --snip-- Так как операция finalString += 'spam ' выполняется 100 000 раз внутри цикла, Python выполняет 100 000 конкатенаций. Процессору приходится создавать все промежуточные строковые значения, объединяя текущее значение finalString со строкой 'spam ', помещать их в память, а затем почти немедленно уничтожать результат при следующей итерации. Все это крайне неэффективно, потому что интересует нас только конечная строка. Питонический способ построения строк основан на присоединении меньших строк к списку и на последующем слиянии элементов списка в одну строку. Этот способ также создает 100 000 строковых объектов, но с выполнением только одной конкате- нации строк при вызове join() . Например, следующий код создает эквивалентный объект finalString , но без промежуточных конкатенаций: >>> finalString = [] >>> for i in range(100000): ... finalString.append('spam ') >>> finalString = ''.join(finalString) >>> finalString spam spam spam spam spam spam spam spam spam spam spam spam --snip-- Когда я замерил время выполнения двух фрагментов кода на моей машине, вер- сия с присоединением к списку работала в 10 раз быстрее версии с конкатенаци- ей. (В главе 13 описано, как измерить скорость выполнения ваших программ.) Чем больше итераций содержит цикл, тем заметнее различия. Но если заменить range(100000) на range(100) , хотя конкатенация все равно работает медленнее присоединения, различия в скорости становятся пренебрежимо малыми. Не обя- зательно фанатично избегать конкатенации строк, f-строк, метода строк format() или спецификатора формата %s . Скорость значительно повышается только при большом количестве конкатенаций. 178 Глава 8.Часто встречающиеся ловушки Python Python избавляет вас от необходимости думать о многих технических подробно- стях. Это позволяет программисту быстро создавать программы, а, как я упоминал ранее, время программиста ценнее процессорного времени. Но в некоторых случаях желательно понимать суть происходящего (например, различия между неизменяе- мыми строками и изменяемыми списками), чтобы не попасть в скрытую ловушку, как при построении строк конкатенацией. Не рассчитывайте, что sort() выполнит сортировку по алфавиту Работа алгоритмов сортировки — алгоритмов, которые систематически упорядо- чивают значения в некотором установленном порядке, — стала одной из важных тем компьютерной теории. Но эта книга не посвящена компьютерной теории; знать такие алгоритмы не обязательно, потому что вы всегда можете вызвать метод Python sort() . Однако неожиданно вы замечаете, что sort() начинает странно себя вести при сортировке: буква Z в верхнем регистре предшествует букве a в нижнем регистре: >>> letters = ['z', 'A', 'a', 'Z'] >>> letters.sort() >>> letters ['A', 'Z', 'a', 'z'] Стандарт ASCII (American Standard Code for Information Interchange) определяет соответствие между числовыми кодами (которые называются кодовыми пунктами, или кодами, — code points, или ordinals) и символами текста. Метод sort() исполь- зует ASCII-алфавитную сортировку (обобщенный термин, означающий сортировку по кодовым пунктам) вместо алфавитной сортировки. В системе ASCII буква A представляется кодовым пунктом 65, B — кодовым пунктом 66 и так далее до бук- вы Z с кодовым пунктом 90. Буква a в нижнем регистре представляется кодовым пунктом 97, b — кодовым пунктом 98 и так далее до буквы z с кодовым пунктом 122. При сортировке в порядке ASCII буква Z в верхнем регистре (кодовый пункт 90) предшествует букве a в нижнем регистре (кодовый пункт 97). И хотя кодировка ASCII почти повсеместно применялась в компьютерной обла- сти в 1990-е годы и ранее, это чисто американский стандарт: в ASCII существует кодовый пункт для знака доллара $ (кодовый пункт 36), но нет кодового пункта для знака британского фунта £. В наши дни кодировку ASCII в основном заме- стил Юникод, потому что он содержит все кодовые пункты ASCII, а также более 100 000 других кодовых пунктов. Чтобы получить кодовый пункт символа, передайте этот символ функции ord() Также можно выполнить обратную операцию: передать кодовый пункт функции Не рассчитывайте на идеальную точность чисел с плавающей точкой 179 chr() , которая возвращает строковое представление символа. Например, введите следующий фрагмент в интерактивной оболочке: >>> ord('a') 97 >>> chr(97) 'a' Если вы хотите выполнить сортировку по алфавиту, передайте метод str.lower в параметре key . Список сортируется так, как если бы для значений вызывался метод строк lower() : >>> letters = ['z', 'A', 'a', 'Z'] >>> letters.sort(key=str.lower) >>> letters ['A', 'a', 'z', 'Z'] Обратите внимание: реальные строки в списке не преобразуются к нижнему реги- стру; они только сортируются так, как если бы они к нему были преобразованы. Нед Бэтчелдер (Ned Batchelder) предоставляет дополнительную информацию о Юникоде и кодовых пунктах в своем докладе «Pragmatic Unicode, or, How Do I Stop the Pain?» (https://nedbatchelder.com/text/unipain.html). Кстати, метод Python sort() использует алгоритм сортировки Timsort, который был спроектирован разработчиком ядра Python и автором тезисов «Дзен Python» Тимом Петерсом. Алгоритм представляет собой гибрид алгоритмов сортировки методом слияния и сортировки методом вставки, его описание доступно на https:// ru.wikipedia.org/wiki/Timsort. Не рассчитывайте на идеальную точность чисел с плавающей точкой Компьютеры позволяют хранить только цифры двоичной системы счисления, то есть 1 и 0. Для представления знакомых десятичных чисел необходимо преоб- разовать такое число, как 3,14, в серию двоичных единиц и нулей. Компьютеры делают это в соответствии со стандартом IEEE 754, опубликованным Институтом инженеров по электротехнике и радиоэлектронике (IEEE, Institute of Electrical and Electronics Engineers). Для простоты эти подробности остаются скрытыми от про- граммиста, чтобы он мог просто вводить числа с точкой, не отвлекаясь на процесс преобразования десятичных значений в двоичные: >>> 0.3 0.3 180 Глава 8.Часто встречающиеся ловушки Python Хотя подробности конкретных случаев выходят за рамки книги, представление чисел с плавающей точкой в стандарте IEEE 754 не всегда точно соответствует исходному десятичному числу. Классический пример — число 0.1 : >>> 0.1 + 0.1 + 0.1 0.30000000000000004 >>> 0.3 == (0.1 + 0.1 + 0.1) False Эта странная сумма с небольшой погрешностью является результатом ошибки округления, возникающей из-за особенностей компьютерного представления и обработки чисел с плавающей точкой. Происходящее не является особенностью Python; стандарт IEEE 754 является аппаратным стандартом, реализованным на уровне электронных компонентов процессора, обеспечивающих вычисления с пла- вающей точкой. Те же результаты будут получены при выполнении программы на C++, JavaScript и любом другом языке — на процессоре, использующем стандарт IEEE 754 (то есть практически на любом процессоре в мире). Стандарт IEEE 754 (по техническим причинам объяснение которых выходит за рамки книги) также не позволяет представлять числовые значения, превышающие 2 53 . Например, числа 2 53 и 2 53 +1 в представлении с плавающей точкой округляются до 9007199254740992.0 : >>> float(2**53) == float(2**53) + 1 True Если вы используете тип данных с плавающей точкой, у этой проблемы нет обход- ного решения. Не беспокойтесь — если только вы не пишете программы для банков или пункта управления ядерным реактором (или управления ядерным реактором в банке), ошибки округления достаточно незначительны, чтобы не создавать су- щественных проблем для вашей программы. Часто проблему можно решить использованием целых чисел с меньшими едини- цами: например, 133 цента вместо 1.33 доллара, или 200 миллисекунд вместо 0.2 секунды. Таким образом, 10 + 10 + 10 в сумме дают 30 центов или миллисекунд, тогда как суммирование 0.1 + 0.1 + 0.1 дает 0.30000000000000004 доллара или секунды. Но если вам требуется абсолютная точность (допустим, для научных или финан- совых вычислений), используйте встроенный модуль Python decimal , опублико- ванный по адресу https://docs.python.org/3/library/decimal.html. И хотя объекты Decimal работают медленнее, они обеспечивают более точную замену для значений с плавающей точкой. Например, вызов decimal.Decimal('0.1') создает объект, представляющий точное число 0.1 без погрешности, которая бы неизбежно при- сутствовала в значении с плавающей точкой 0.1 Не рассчитывайте на идеальную точность чисел с плавающей точкой 181 Если передать значение с плавающей точкой 0.1 при вызове decimal.Decimal() , будет создан объект Decimal с такой же погрешностью, как у значения с плава- ющей точкой; вот почему полученный объект Decimal не будет в точности равен Decimal('0.1') . Вместо этого decimal.Decimal() следует передавать строковое пред- ставление числа с плавающей точкой. Чтобы убедиться в этом, введите следующий фрагмент в интерактивной оболочке: >>> import decimal >>> d = decimal.Decimal(0.1) >>> d Decimal('0.1000000000000000055511151231257827021181583404541015625') >>> d = decimal.Decimal('0.1') >>> d Decimal('0.1') >>> d + d + d Decimal('0.3') У целых чисел ошибки округления отсутствуют, поэтому передавать их decimal. Decimal() всегда безопасно. Введите следующий фрагмент в интерактивной обо- лочке: >>> 10 + d Decimal('10.1') >>> d * 3 Decimal('0.3') >>> 1 - d Decimal('0.9') >>> d + 0.1 Traceback (most recent call last): File " TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float' Однако объекты Decimal не обладают неограниченной точностью; просто они имеют прогнозируемый, четко установленный уровень точности. Для примера рассмотрим следующие операции: >>> import decimal >>> d = decimal.Decimal(1) / 3 >>> d Decimal('0.3333333333333333333333333333') >>> d * 3 Decimal('0.9999999999999999999999999999') >>> (d * 3) == 1 # d is not exactly 1/3 False Выражение decimal.Decimal(1) / 3 в результате дает значение, которое не равно точно 1/3. Но по умолчанию оно будет точным до 28 значащих цифр. Чтобы уз- нать, сколько значащих цифр использует модуль decimal , обратитесь к атрибуту 182 Глава 8.Часто встречающиеся ловушки Python decimal.getcontext().prec . (С технической точки зрения prec является атри- бутом объекта Context , возвращаемого вызовом getcontext() , но его удобно разместить в той же строке.) Этот атрибут можно изменить, чтобы все объекты Decimal , создаваемые в дальнейшем, имели новый уровень точности. Следующий пример, введенный в интерактивной оболочке, понижает точность с исходных 28 значащих цифр до 2: >>> import decimal >>> decimal.getcontext().prec 28 >>> decimal.getcontext().prec = 2 >>> decimal.Decimal(1) / 3 Decimal('0.33') Модуль decimal позволяет точно управлять взаимодействием между числами. Полная документация этого модуля доступна по адресу https://docs.python.org/3/ library/decimal.html. Не объединяйте операторы != в цепочку Сцепленные операторы сравнения (например, 18 < age < 35 ) или сцепленные при- сваивания (например, six = halfDozen = 6 ) являются удобными сокращениями для (18 < age) and (age < 35) и six = 6; halfDozen = 6 соответственно. Однако оператор проверки неравенства != в цепочках использовать не стоит. На первый взгляд, следующий код проверяет, что все три переменные имеют разные значения, потому что следующее выражение дает результат True : >>> a = 'cat' >>> b = 'dog' >>> c = 'moose' >>> a != b != c True Но на самом деле эта цепочка эквивалентна (a != b) and (b != c) . А это означает, что a и c могут быть равны и выражение a != b != c все равно будет равно True : >>> a = 'cat' >>> b = 'dog' >>> c = 'cat' >>> a != b != c True Это весьма коварная ошибка, а код на первый взгляд выглядит нормально, поэтому лучше полностью избегать сцепленных операторов != Итоги 183 Не забудьте запятую в кортежах из одного элемента Когда вы записываете значения кортежей в своем коде, помните о том, что за- вершающая запятая необходима даже в том случае, если кортеж содержит только один элемент. Хотя значение (42, ) представляет собой кортеж, содержащий целое число 42, запись (42) обозначает всего лишь целое число 42 . Скобки в (42) сходны с используемыми в выражении (20 + 1) * 2 , результатом которого является целое число 42. Если вы забудете поставить запятую, это может иметь непредвиденные последствия: >>> spam = ('cat', 'dog', 'moose') >>> spam[0] 'cat' >>> spam = ('cat') >>> spam[0] ❶ 'c' >>> spam = ('cat', ) ❷ >>> spam[0] 'cat' Без запятой при вычислении ('cat') будет получен строковый результат, поэтому spam[0] дает первый символ строки, 'c' ❶ . Завершающая запятая необходима для того, чтобы выражение в круглых скобках распознавалось как значение-кортеж ❷ В Python кортеж определяется запятыми в большей степени, чем круглыми скоб- ками. |