Чистыйкод дляпродолжающи х
Скачать 7.85 Mb.
|
clothes = ['skirt', 'red sock', 'blue sock'] >>> newClothes = [] >>> for clothing in clothes: ... if 'sock' in clothing: ... print('Appending:', clothing) 166 Глава 8.Часто встречающиеся ловушки Python ... newClothes.append(clothing) # Изменяется список newClothes, # а не clothes. Appending: red sock Appending: blue sock >>> print(newClothes) ['red sock', 'blue sock'] >>> clothes.extend(newClothes) # Присоединяем элементы newClothes к clothes. >>> print(clothes) ['skirt', 'red sock', 'blue sock', 'red sock', 'blue sock'] Рис. 8.1. При каждой итерации цикла for к списку присоединяется новый объект 'red sock', к которому clothing переходит при следующей итерации. Цикл повторяется бесконечно Процесс выполнения этого кода наглядно представлен на https://autbor.com/ addingloopfixed/. Цикл for перебирает элементы списка clothes , но clothes не изменяется в цикле. Вместо этого изменяется отдельный список newClothes . Затем после завершения цикла список clothes дополняется содержимым newClothes . В итоге вы получаете список clothes с парными носками. Не добавляйте и не удаляйте элементы из списка в процессе перебора 167 По тем же причинам элементы не должны удаляться из списка в процессе перебора. Рассмотрим пример, в котором из списка нужно удалить любую строку, отличную от 'hello' . Наивное решение перебирает список, удаляя из него элементы, отлич- ные от 'hello' : >>> greetings = ['hello', 'hello', 'mello', 'yello', 'hello'] >>> for i, word in enumerate(greetings): ... if word != 'hello': # Удалить все элементы, отличные от 'hello'. ... del greetings[i] >>> print(greetings) ['hello', 'hello', 'yello', 'hello'] Процесс выполнения этого кода наглядно показан на https://autbor.com/deletingloop/. Похоже, в списке остался лишний элемент 'yello' . Дело в том, что, когда цикл for проверял индекс 2, он удалил 'mello' из списка. Но затем все оставшиеся элементы сдвинулись на один индекс вниз и элемент 'yello' перешел с индекса 3 на индекс 2. Следующая итерация цикла проверяет индекс 3, который теперь соответствует по- следнему элементу 'hello' (рис. 8.2). Строка 'yello' осталась непроверенной! Не удаляйте элементы из списка в процессе перебора этого списка. Другие элементы сдвигаются вниз. Эта строка удаляется. Рис. 8.2. Когда цикл удаляет 'mello', элементы списка сдвигаются на одну позицию вниз, в результате чего i пропускает элемент 'yello' Вместо этого создайте новый список и скопируйте в него все элементы, кроме тех, которые должны быть удалены, после чего замените исходный список. Чтобы 168 Глава 8.Часто встречающиеся ловушки Python увидеть исправленную версию предыдущего примера, введите следующий код в интерактивной оболочке: >>> greetings = ['hello', 'hello', 'mello', 'yello', 'hello'] >>> newGreetings = [] >>> for word in greetings: ... if word == 'hello': # Копирование всех элементов, равных 'hello'. ... newGreetings.append(word) >>> greetings = newGreetings # Заменить исходный список. >>> print(greetings) ['hello', 'hello', 'hello'] Процесс выполнения этого кода наглядно представлен на https://autbor.com/ deletingloopfixed/. Так как этот код представляет собой простой цикл, который генерирует список, его можно заменить списковым включением. Списковое включение не выполняется быстрее и не расходует меньше памяти, но оно быстрее вводится без ущерба для удобочитаемости. Введите в интерактивной оболочке следующий фрагмент, экви- валентный коду из предыдущего примера: >>> greetings = ['hello', 'hello', 'mello', 'yello', 'hello'] >>> greetings = [word for word in greetings if word == 'hello'] >>> print(greetings) ['hello', 'hello', 'hello'] Списковое включение не только записывается более компактно, но и избегает ошибок, возникающих из-за изменения списка в процессе его перебора. И хотя элементы не должны добавляться или удаляться из списка (или любого ите- рируемого объекта) в процессе перебора, ничто не мешает вам изменять содержимое списка. Допустим, у вас есть список чисел в виде строк: ['1', '2', '3', '4', '5'] Его можно преобразовать в список целых чисел [1, 2, 3, 4, 5] в процессе перебора: >>> numbers = ['1', '2', '3', '4', '5'] >>> for i, number in enumerate(numbers): ... numbers[i] = int(number) >>> numbers [1, 2, 3, 4, 5] Процесс выполнения этого кода наглядно показан на https://autbor.com/ covertstringnumbers/. Изменять элементы в списке можно; ошибки возникают при изменении количества элементов в списке. Также существует другой безопасный способ добавления или удаления элементов из списка: перебор от конца списка к началу. При таком подходе можно удалять элементы из списка в процессе перебора или добавлять элементы — при условии, Не добавляйте и не удаляйте элементы из списка в процессе перебора 169 что новый элемент добавляется в конец списка. Например, введите следующий код, который удаляет четные целые числа из списка someInts : >>> someInts = [1, 7, 4, 5] >>> for i in range(len(someInts)): ... if someInts[i] % 2 == 0: ... del someInts[i] Traceback (most recent call last): File " IndexError: list index out of range >>> someInts = [1, 7, 4, 5] >>> for i in range(len(someInts) - 1, -1, -1): ... if someInts[i] % 2 == 0: ... del someInts[i] >>> someInts [1, 7, 5] Этот код работает, потому что ни у одного из элементов, которые будут пере- бираться в будущем, индекс не изменяется. Однако из-за многократного сдвига значений после удаляемого значения такое решение становится неэффективным для длинных списков. Процесс выполнения этого кода наглядно представлен на https://autbor.com/iteratebackwards1/. Различия между прямым и обратным пере- бором показаны на рис. 8.3. Рис. 8.3. Удаление четных чисел из списка при прямом (слева) и обратном (справа) переборе 170 Глава 8.Часто встречающиеся ловушки Python ССЫЛКИ, ИСПОЛЬЗОВАНИЕ ПАМЯТИ И SYS.GETSIZEOF() Может показаться, что создание нового списка вместо изменения исходного приводит к лишним затратам памяти. Но вспомните, что с технической точки зрения все переменные содержат ссылки на значения вместо самих значений; так же и списки содержат ссылки на значения. Приведенная выше строка newGreetings.append(word) не создает копию строки из пере- менной word , а только копирует ссылку на строку, которая занимает куда меньше памяти. Чтобы убедиться в этом, можно воспользоваться функцией sys.getsizeof() , которая возвращает размер памяти в байтах, занимаемой переданным функ- ции объектом. В этом примере интерактивной оболочки мы видим, что короткая строка 'cat' занимает 52 байта, тогда как более длинная строка занимает 85 байт: >>> import sys >>> sys.getsizeof('cat') 52 >>> sys.getsizeof('a much longer string than just "cat"') 85 (В моей версии Python служебная информация объекта строки занимает 49 байт, тогда как каждый символ в строке занимает 1 байт.) Однако список, содержащий любую из этих строк, занимает 72 байта независимо от длины строки: >>> sys.getsizeof(['cat']) 72 >>> sys.getsizeof(['a much longer string than just "cat"']) 72 Дело в том, что с технической точки зрения список содержит не строки, а ссылки на строки, а ссылки всегда имеют одинаковые размеры незави- симо от размера данных, на которые они ссылаются. Вызов newGreetings. append(word) копирует не строку, а ссылку на строку. Если вы хотите узнать, сколько памяти занимает объект и все объекты, на которые он содержит ссылки, разработчик ядра Python Рэймонд Хеттингер (Raymond Hettinger) написал специальную функцию, доступную по адресу https://code.activestate. com/recipes/577504-compute-memory-footprint-of-an-object-and-its-cont/. Итак, не стоит думать, что создание нового списка вместо изменения исход- ного списка в процессе перебора приводит к лишним затратам памяти. Даже если ваш код с изменением списка вроде бы работает, он может стать источ- ником коварных ошибок, а их поиск и исправление займут много времени. Время программиста стоит намного дороже, чем затраты памяти компьютера. Не копируйте изменяемые значения без copy.copy() и copy.deepcopy() 171 Аналогичным образом можно добавлять элементы в конец списка при обратном переборе. Введите в интерактивной оболочке следующий фрагмент, который при- соединяет копию всех четных чисел из списка someInts в конец списка: >>> someInts = [1, 7, 4, 5] >>> for i in range(len(someInts) - 1, -1, -1): ... if someInts[i] % 2 == 0: ... someInts.append(someInts[i]) >>> someInts [1, 7, 4, 5, 4] Процесс выполнения этого кода наглядно показан на https://autbor.com/ iteratebackwards2/. Перебирая список в обратном направлении, можно как присо- единять к нему элементы, так и удалять элементы из списка. Тем не менее сделать это иногда непросто, потому что незначительные изменения в этом решении при- ведут к возникновению ошибок. Гораздо проще создать новый список, вместо того чтобы изменять исходный. Разработчик ядра Python Рэймонд Хеттингер (Raymond Hettinger) сформулировал это так: «В. Какие практики следует применять при изменении списка во время его перебора? О. Не делайте этого». Не копируйте изменяемые значения без copy.copy() и copy.deepcopy() Переменные лучше представлять как наклейки или ярлыки, которые прикрепля- ются к объектам, а не как коробки, в которых лежат объекты. Эта модель особенно полезна в ситуациях, связанных с модификацией изменяемых объектов (списков, словарей и множеств, значение которых может изменяться). Одна из распространен- ных ошибок: разработчик копирует одну переменную, ссылающуюся на изменяемый объект, в другую переменную и думает, что копируется реальный объект. В Python команды копирования никогда не копируют объекты; они копируют только ссылки на объект. (Разработчик Python Нед Бэтчелдер (Ned Batchelder) выступил с пре- восходным докладом на эту тему «Facts and Myths about Python Names and Values» на конференции PyCon 2015 (https://youtu.be/_AEJHKGk9ns).) Например, введите следующий фрагмент в интерактивной оболочке; обратите внимание: хотя мы изменяем только переменную spam , переменная cheese тоже изменяется: >>> spam = ['cat', 'dog', 'eel'] >>> cheese = spam 172 Глава 8.Часто встречающиеся ловушки Python >>> spam ['cat', 'dog', 'eel'] >>> cheese ['cat', 'dog', 'eel'] >>> spam[2] = 'MOOSE' >>> spam ['cat', 'dog', 'MOOSE'] >>> cheese ['cat', 'dog', 'MOOSE'] >>> id(cheese), id(spam) 2356896337288, 2356896337288 Процесс выполнения этого кода наглядно показан на https://autbor.com/ listcopygotcha1/. Если вы думаете, что команда cheese = spam копирует объект списка, то вас может удивить, что переменная cheese изменилась, хотя в про- грамме изменялась только переменная spam . Но команды присваивания никогда не копируют объекты, а только ссылки на них. Команда присваивания cheese = spam заставляет cheese ссылаться на тот же объект списка в памяти, на который ссылается переменная spam . Она не создает дополнительной копии объекта списка. Вот почему изменение spam также изменяет cheese : обе переменные ссылаются на один и тот же объект списка. Тот же принцип действует для изменяемых объектов, передаваемых при вызове функции. Введите в интерактивной оболочке следующий фрагмент; обратите внимание на то, что глобальная переменная spam и локальный параметр (напом- ню: параметрами называются переменные, определяемые в команде def функции) theList ссылаются на один объект: >>> def printIdOfParam(theList): ... print(id(theList)) >>> eggs = ['cat', 'dog', 'eel'] >>> print(id(eggs)) 2356893256136 >>> printIdOfParam(eggs) 2356893256136 Процесс выполнения этого кода демонстрируется на https://autbor.com/ listcopygotcha2/. Обратите внимание: вызовы id() для eggs и theList возвращают одинаковые идентичности; это означает, что переменные ссылаются на один и тот же объект списка. Объект списка из переменной eggs не был скопирован в theList ; вместо этого была скопирована ссылка, поэтому обе переменные ссылаются на один список. Размер ссылки составляет лишь несколько байтов, но представьте, что Python вместо ссылки скопировал бы весь список. Если бы в списке eggs были миллиарды эле- ментов вместо всего трех, при передаче его функции printIdOfParam() пришлось Не копируйте изменяемые значения без copy.copy() и copy.deepcopy() 173 бы скопировать этот огромный список. Простой вызов функции потребовал бы многих гигабайтов памяти! Вот почему команда присваивания в Python копиру- ет только ссылки и никогда не копирует объекты. Одно из возможных решений этой проблемы заключается в копировании объекта списка (а не только ссылки) функцией copy.copy() . Введите следующий фрагмент в интерактивной оболочке: >>> import copy >>> bacon = [2, 4, 8, 16] >>> ham = copy.copy(bacon) >>> id(bacon), id(ham) (2356896337352, 2356896337480) >>> bacon[0] = 'CHANGED' >>> bacon ['CHANGED', 4, 8, 16] >>> ham [2, 4, 8, 16] >>> id(bacon), id(ham) (2356896337352, 2356896337480) Процесс выполнения этого кода показан на https://autbor.com/copycopy1/. Пере- менная ham ссылается на скопированный объект списка вместо исходного объекта списка, на который ссылалась переменная bacon , поэтому она не страдает от этой проблемы. Но подобно тому, как переменные правильнее сравнивать с наклейками, а не с коробками, в которых хранятся объекты, в списках также хранятся наклейки (ссылки на объекты) вместо самих объектов. Если ваш список содержит другие списки, copy.copy() копирует только ссылки на эти внутренние списки. Чтобы увидеть эту проблему, введите следующий фрагмент в интерактивной оболочке: >>> import copy >>> bacon = [[1, 2], [3, 4]] >>> ham = copy.copy(bacon) >>> id(bacon), id(ham) (2356896466248, 2356896375368) >>> bacon.append('APPENDED') >>> bacon [[1, 2], [3, 4], 'APPENDED'] >>> ham [[1, 2], [3, 4]] >>> bacon[0][0] = 'CHANGED' >>> bacon [['CHANGED', 2], [3, 4], 'APPENDED'] >>> ham [['CHANGED', 2], [3, 4]] >>> id(bacon[0]), id(ham[0]) (2356896337480, 2356896337480) Процесс выполнения этого кода показан на https://autbor.com/copycopy2/. Хотя bacon и ham — два разных объекта списков, они ссылаются на одни и те же внутренние 174 Глава 8.Часто встречающиеся ловушки Python списки [1, 2] и [3, 4] , так что изменения в этих внутренних списках будут отражены в обеих переменных, несмотря на использование copy.copy() . Проблема решается использованием функции copy.deepcopy() , которая копирует все объекты списков внутри копируемых объектов списков (а также все объекты списков в этих объектах списков, и т. д.). Введите следующий фрагмент в интерактивной оболочке: >>> import copy >>> bacon = [[1, 2], [3, 4]] >>> ham = copy.deepcopy(bacon) >>> id(bacon[0]), id(ham[0]) (2356896337352, 2356896466184) >>> bacon[0][0] = 'CHANGED' >>> bacon [['CHANGED', 2], [3, 4]] >>> ham [[1, 2], [3, 4]] Процесс выполнения этого кода показан на https://autbor.com/copydeepcopy/. И хотя функция copy.deepcopy() работает чуть медленнее copy.copy() , она без- опаснее, если вы не знаете, содержит ли копируемый список другие списки (или другие изменяемые объекты, такие как словари или множества). В общем слу- чае я рекомендую всегда использовать функцию copy.deepcopy() : она способна предотвратить коварные ошибки, а замедление кода вряд ли будет сколько-нибудь заметным. Не используйте изменяемые значения для аргументов по умолчанию Python позволяет назначить аргументы по умолчанию для параметров функций, которые вы определяете. Если пользователь не задает значение параметра явно, то функция выполняется с аргументом по умолчанию. Это может быть удобно, если большинство вызовов функции использует одно и то же значение аргумента, потому что с аргументами по умолчанию параметр становится необязательным. Например, при передаче None методу split() разбиение производится по пробельным симво- лам, но None также является аргументом по умолчанию: вызов 'cat dog'.split() делает то же самое, что 'cat dog'.split(None) . Функция использует аргумент по умолчанию для соответствующего параметра, если вызывающая сторона не пере- даст значение явно. Однако в качестве аргумента по умолчанию никогда не следует назначать изменя- емый объект, такой как список или словарь. Чтобы понять, к каким ошибкам это может привести, рассмотрим следующий пример. В нем определяется функция addIngredient() , которая добавляет строку с ингредиентом в список, представля- ющий собой рецепт сэндвича. Так как первым и последним элементами рецепта Не используйте изменяемые значения для аргументов по умолчанию 175 обычно является хлеб (bread), изменяемый список ['bread', 'bread'] используется в качестве аргумента по умолчанию: >>> def addIngredient(ingredient, sandwich=['bread', 'bread']): ... sandwich.insert(1, ingredient) ... return sandwich >>> mySandwich = addIngredient('avocado') >>> mySandwich ['bread', 'avocado', 'bread'] Но при использовании в качестве аргумента по умолчанию изменяемого объекта, такого как список ['bread', 'bread'] , — возникает неочевидная проблема: список создается при выполнении команды def этой функции, а не при каждом вызове функции. Это означает, что создается только один объект списка ['bread', 'bread'] , потому что функция addIngredient() определяется только один раз. При каждом вызове функции addIngredient() этот список будет использоваться повторно. Это приводит к неожиданному поведению: >>> |