После того как будет создана коллекция элементов данных, вполне естественно возникает желание обойти все элементы, содержащиеся в ней. Еще одна часто выполняемая операция – копирование коллекций. Из-за того, что в языке Python повсеместно используются ссылки на объекты (ради повышения эффективности), существуют некоторые особенности, связанные с копированием.
В этом разделе сначала мы рассмотрим итераторы языка Python, а затем принципы копирования коллекций.
Итерируемый тип данных – это такой тип, который может возвращать
свои элементы по одному. Любой объект, имеющий метод __iter__()
, или
любая последовательность (то есть объект, имеющий метод
__getitem__()
, принимающий целочисленный аргумент со значением от
0
и выше), является итерируемым и может предоставлять
итератор. Итератор – это объект, имеющий метод __next__()
, который
при каждом вызове возвращает очередной элемент и возбуждает исключение
StopIteration
после исчерпания всех элементов. В
табл. collections:iterandcopy:iterator:tbl:1 перечислены
операторы и функции, которые могут применяться к итерируемым
объектам.
Таблица 4. Общие функции и операторы для работы с итерируемыми объектами
Синтаксис | Описание |
s + t | Возвращает конкатенацию последовательностей s и t |
s * n | Возвращает конкатенацию из int n последовательностей s |
x in i | Возвращает True , если элемент x присутствует в итерируемом объекте i , обратная проверка выполняется с помощью оператора not in |
all(i) | Возвращает True , если все элементы итерируемого объекта i в логическом контексте оцениваются как значение True |
any(i) | Возвращает True , если хотя бы один элемент итерируемого объекта i в логическом контексте оценивается как значение True |
enumerate (i,start) | Обычно используется в циклах for ... in , чтобы получить последовательность кортежей (index, item) , где значения индексов начинают отсчитывать от 0 или от значения start |
len(x) | Возвращает «длину» объекта x . Если x – коллекция, то возвращаемое число представляет количество элементов. Если x – строка, то возвращаемое число представляет количество символов |
max(i, key) | Возвращает наибольший элемент в итерируемом объекте i или элемент с наибольшим значением key(item) , если функция key определена |
min(i, key) | Возвращает наименьший элемент в итерируемом объекте i или элемент с наименьшим значением key(item) , если функция key определена |
range(start, stop, step) | Возвращает целочисленный итератор. С одним аргументом (stop ) итератор представляет последовательность целых чисел от 0 до stop-1 , с двумя аргументами (start , stop ) – последовательность целых чисел от start до stop-1 , с тремя аргументами – последовательность целых чисел от start до stop-1 c шагом step |
reversed(i) | Возвращает итератор, который будет возвращать элементы итератора i в обратном порядке |
sorted(i, key, reverse) | Возвращает список элементов итератора i в отсортированном порядке. Аргумент key используется для выполнения сортировки DSU (Decorate, Sort, Undecorate – декорирование, сортировка, обратное декорирование). Если аргумент reverse имеет значение True , сортировка выполняется в обратном порядке |
sum(i, start) | Возвращает сумму элементов итерируемого объекта i , плюс ар гумент start (значение которого по умолчанию равно 0 ). Объект i не должен содержать строк |
zip(i1, ..., iN) | Возвращает итератор кортежей, используя итераторы от i1 до iN |
Порядок, в котором возвращаются элементы, зависит от итерируемого
объекта. В случае списков и кортежей элементы обычно возвращаются
в предопределенном порядке, начиная с первого элемента (находящегося в
позиции с индексом 0
), но другие итераторы возвращают элементы в
произвольном порядке – например, итераторы словарей и множеств.
Встроенная функция iter()
используется двумя совершенно различными
способами. Применяемая к коллекции или к последовательности, она
возвращает итератор для заданного объекта или возбуждает исключение
TypeError
, если объект не является итерируемым. Такой способ часто
используется при работе с нестандартными типами коллекций
и крайне редко – в других контекстах. Во втором варианте использования
функции iter()
ей передается вызываемый объект (функция или метод) и
специальное значение. В этом случае полученная функция или метод
вызывается на каждой итерации, а значение этой функции, если оно не
равно специальному значению, возвращается вызывающей программе; в
противном случае возбуждается исключение StopIteration
.
Когда в программе используется цикл for item in iterable
,
интерпретатор Python вызывает функцию iter(iterable)
, чтобы получить
итератор. После этого на каждой итерации вызывается метод __next__()
итератора, чтобы получить очередной элемент, а когда возбуждается
исключение StopIteration
, оно перехватывается и цикл завершается.
Другой способ получить очередной элемент итератора состоит в том,
чтобы вызвать встроенную функцию next()
. Ниже приводятся два
эквивалентных фрагмента программного кода (оба они вычисляют
произведение элементов списка), в одном из них используется цикл
for ... in
, а во втором явно используется итератор:
product = 1
for i in [1, 2, 4, 8]:
product *= i
print(product)
product = 1
i = iter([1, 2, 4, 8])
while True:
try:
product *= next(i)
except StopIteration:
break
print(product)
Любой (конечный) итерируемый объект i
может быть преобразован
в кортеж вызовом функции tuple(i)
или в список – вызовом функции
list(i)
.
Поскольку в языке Python повсюду используются ссылки на объекты, когда
выполняется оператор присваивания (=
), никакого копирования данных
на самом деле не происходит. Если справа от оператора находится
литерал, например, строка или число, в операнд слева записывается
ссылка, которая указывает на объект в памяти, хранящий значение
литерала. Если справа находится ссылка на объект, в левый операнд
записывается ссылка, указывающая на тот же самый объект, на который
ссылается правый операнд. Вследствие этого операция присваивания
обладает чрезвычайно высокой скоростью выполнения.
Когда выполняется присваивание крупной коллекции, такой как длинный список, экономия времени становится более чем очевидной. Например:
>>> songs = ["Because", "Boys", "Carol"]
>>> beatles = songs
>>> beatles, songs
(['Because', 'Boys', 'Carol'], ['Because', 'Boys', 'Carol'])
Здесь была создана новая ссылка на объект (beatles
), и обе ссылки
указывают на один и тот же список – никакого копирования данных не
производилось.
Поскольку списки относятся к категории изменяемых объектов, мы можем вносить в них изменения. Например:
>>> beatles[2] = "Cayenne"
>>> beatles, songs
(['Because', 'Boys', 'Cayenne'], ['Because', 'Boys', 'Cayenne'])
Изменения были внесены с использованием переменной beatles
, но это
всего лишь ссылка, указывающая на тот же самый объект, что и ссылка
songs. Поэтому любые изменения, произведенные с использованием одной
ссылки, можно наблюдать с использованием другой ссылки.
Часто это именно то, что нам требуется, поскольку копирование крупных
коллекций может оказаться дорогостоящей операцией. Кроме того, это
также означает, что имеется возможность передавать списки или другие
изменяемые коллекции в виде аргументов функций, изменять эти коллекции
в функциях и пребывать в уверенности, что изменения будут доступны
после того, как функция вернет управление вызывающей программе.
Однако в некоторых ситуациях действительно бывает необходимо создать
отдельную копию коллекции (то есть создать другой изменяемый
объект). В случае последовательностей, когда выполняется оператор
извлечения среза, например, songs[:2]
, полученный срез – это всегда
независимая копия элементов. Поэтому скопировать последовательность
целиком можно следующим способом:
>>> songs = ["Because", "Boys", "Carol"]
>>> beatles = songs[:]
>>> beatles[2] = "Cayenne"
>>> beatles, songs
(['Because', 'Boys', 'Cayenne'], ['Because', 'Boys', 'Carol'])
В случае словарей и множеств копирование можно выполнить с помощью
методов dict.copy()
и set.copy()
. Кроме того, в модуле copy
имеется функция copy.copy(), которая возвращает копию заданного
объекта. Другой способ копирования встроенных типов коллекций
заключается в использовании имени типа как функции, которой в качестве
аргумента передается копируемая коллекция. Например:
copy_of_dict_d = dict(d)
copy_of_list_L = list(L)
copy_of_set_s = set(s)
Обратите внимание, что все эти приемы копирования создают поверхностные копии, то есть копируются только ссылки на объекты, но не сами объекты. Для неизменяемых типов данных, таких как числа и строки, это равносильно копированию (за исключением более высокой эффективности), но для изменяемых типов данных, таких как вложенные коллекции, это означает, что ссылки в оригинальной коллекции и в копии будут указывать на одни и те же объекты. Эту особенность иллюстрирует следующий пример:
>>> x = [53, 68, ["A", "B", "C"]]
>>> y = x[:] # поверхностное копирование
>>> x, y
([53, 68, ['A', 'B', 'C']], [53, 68, ['A', 'B', 'C']])
>>> y[1] = 40
>>> x[2][0] = 'Q'
>>> x, y
([53, 68, ['Q', 'B', 'C']], [53, 40, ['Q', 'B', 'C']])
Когда выполняется поверхностное копирование списка x
, копируется
ссылка на вложенный список ["A", "B", "C"]
. Это означает, что третий
элемент в обоих списках, x
и y
, ссылается на один и тот же список,
поэтому любые изменения, произведенные во вложенном списке, можно
наблюдать с помощью любой из ссылок, x
или y
. Если действительно
необходимо создать абсолютно независимую копию коллекции с
произвольной глубиной вложенности, необходимо выполнить глубокое
копирование:
>>> import copy
>>> x = [53, 68, ["A", "B", "C"]]
>>> y = copy.deepcopy(x)
>>> y[1] = 40
>>> x[2][0] = 'Q'
>>> x, y
([53, 68, ['Q', 'B', 'C']], [53, 40, ['A', 'B', 'C']])
Здесь списки x
и y
, а также элементы, которые они содержат,
полностью независимы.