Python для поведения цикла и итератора
Я хотел узнать немного больше о iterators
, поэтому, пожалуйста, поправьте меня, если я ошибаюсь.
Итератор - это объект, который имеет указатель на следующий объект и считывается как буфер или поток (т.е. связанный список). Они особенно эффективны, потому что все, что они делают, это сказать вам, что будет дальше по ссылкам вместо использования индексации.
Однако я до сих пор не понимаю, почему происходит следующее поведение:
In [1]: iter = (i for i in range(5))
In [2]: for _ in iter:
....: print _
....:
0
1
2
3
4
In [3]: for _ in iter:
....: print _
....:
In [4]:
После первого цикла через итератор (In [2]
) он как бы был уничтожен и оставлен пустым, поэтому второй цикл (In [3]
) ничего не печатает.
Однако я никогда не назначал новое значение переменной iter
.
Что действительно происходит под капотом цикла for
?
Ответы
Ответ 1
Ваше подозрение верное: итератор был уничтожен.
В действительности ваш итератор generator, который является объектом, который может быть повторен только один раз.
type((i for i in range(5))) # says it type generator
def another_generator():
yield 1 # the yield expression makes it a generator, not a function
type(another_generator()) # also a generator
Причина, по которой они эффективны, не имеет никакого отношения к тому, чтобы сообщить вам, что будет дальше "по ссылке". Они эффективны, потому что они только генерируют следующий элемент по запросу; все элементы не генерируются сразу. Фактически, вы можете иметь бесконечный генератор:
def my_gen():
while True:
yield 1 # again: yield means it is a generator, not a function
for _ in my_gen(): print(_) # hit ctl+c to stop this infinite loop!
Некоторые другие исправления, которые помогут улучшить ваше понимание:
- Генератор не является указателем и не ведет себя как указатель, как вы могли бы узнать на других языках.
- Одно из отличий от других языков: как сказано выше, каждый результат генератора генерируется "на лету". Следующий результат не создается до тех пор, пока он не будет запрошен.
- Комбинация ключевых слов
for
in
принимает итерируемый объект в качестве второго аргумента.
- Итерируемый объект может быть генератором, как в случае вашего примера, но также может быть любым другим итерируемым объектом, таким как объект
list
или dict
, или str
(строка), или пользовательский тип, который обеспечивает требуемую функциональность.
-
iter
function применяется к объекту для получения итератора (кстати: не используйте iter
как имя переменной в Python, как вы это сделали - это одно из ключевых слов). На самом деле, если быть более точным, вызывается объект __iter__
method (который, по большей части, все функции iter
в любом случае, __iter__
является одним из Python так называемых "магических методов" ).
- Если вызов
__iter__
выполняется успешно, функция next()
применяется к итерируемому объекту снова и снова, в цикл, а первая переменная, передаваемая в for
in
, присваивается результату функции next()
. (Помните: итерируемый объект может быть генератором или итератором контейнера или любым другим итерируемым объектом.) На самом деле, если быть точнее: он вызывает объект итератора __next__
, что является еще одним "магическим методом".
- Цикл
for
заканчивается, когда next()
вызывает исключение StopIteration
(что обычно происходит, когда итерабельность не имеет другого объект, возвращаемый при вызове next()
).
Вы можете "вручную" реализовать цикл for
в python таким образом (вероятно, не идеально, но достаточно близко):
try:
temp = iterable.__iter__()
except AttributeError():
raise TypeError("'{}' object is not iterable".format(type(iterable).__name__))
else:
while True:
try:
_ = temp.__next__()
except StopIteration:
break
except AttributeError:
raise TypeError("iter() returned non-iterator of type '{}'".format(type(temp).__name__))
# this is the "body" of the for loop
continue
Взаимосвязь между приведенным выше и вашим примером кода практически не существует.
На самом деле, более интересной частью цикла for
является не for
, а in
. Использование in
само по себе создает другой эффект, чем for
in
, но очень полезно понять, что делает in
с его аргументами, поскольку for
in
реализует очень похожее поведение.
-
Когда используется само по себе, ключевое слово in
сначала вызывает объект __contains__
method, который является еще одним "волшебный метод" (обратите внимание, что этот шаг пропускается при использовании for
in
). Используя in
самостоятельно на контейнере, вы можете сделать следующее:
1 in [1, 2, 3] # True
'He' in 'Hello' # True
3 in range(10) # True
'eH' in 'Hello'[::-1] # True
-
Если итерируемый объект НЕ является контейнером (т.е. не имеет метода __contains__
), in
следующий пытается вызвать метод __iter__
объекта. Как было сказано ранее: метод __iter__
возвращает то, что известно в Python, как iterator. В принципе, итератор - это объект, в котором вы можете использовать встроенную общую функцию next()
на 1. Генератор - это всего лишь один тип итератора.
- Если вызов
__iter__
выполняется успешно, ключевое слово in
применяет функцию next()
к повторяемому объекту и снова. (Помните: итерируемый объект может быть генератором или итератором контейнера или любым другим итерируемым объектом.) На самом деле, если быть точнее: он вызывает объект итератора __next__
).
- Если объект не имеет метода
__iter__
для возврата итератора, in
затем возвращается к протоколу итерации старого стиля, используя метод __getitem__
2.
- Если все вышеперечисленные попытки потерпят неудачу, вы получите
TypeError
исключение.
Если вы хотите создать свой собственный тип объекта для перебора (т.е. на нем можно использовать for
in
или просто in
), полезно знать о ключевом слове yield
, который используется в генераторах (как упоминалось выше).
class MyIterable():
def __iter__(self):
yield 1
m = MyIterable()
for _ in m: print(_) # 1
1 in m # True
Присутствие yield
превращает функцию или метод в генератор вместо обычной функции/метода. Вам не нужен метод __next__
, если вы используете генератор (он приносит __next__
вместе с ним автоматически).
Если вы хотите создать свой собственный тип объекта контейнера (т.е. вы можете использовать его in
, но НЕ for
in
), вам просто нужен метод __contains__
.
class MyUselessContainer():
def __contains__(self, obj):
return True
m = MyUselessContainer()
1 in m # True
'Foo' in m # True
TypeError in m # True
None in m # True
1 Обратите внимание, что для того, чтобы быть итератором, объект должен реализовывать протокол итератора. Это означает, что методы __next__
и __iter__
должны быть правильно реализованы (генераторы поставляются с этой функцией "бесплатно", поэтому вам не нужно беспокоиться об этом при их использовании). Также обратите внимание, что метод ___next__
фактически next
(без подчеркивания) в Python 2.
2 См. этот ответ для разных способов создания повторяющихся классов.
Ответ 2
Для цикла в основном используется метод next
объекта, который применяется к (__next__
в Python 3).
Вы можете имитировать это просто:
iter = (i for i in range(5))
print(next(iter))
print(next(iter))
print(next(iter))
print(next(iter))
print(next(iter))
# this prints 1 2 3 4
В этот момент нет нового элемента во входном объекте. Таким образом:
print(next(iter))
В результате будет выведено исключение StopIteration
. На этом этапе for
остановится. Итератором может быть любой объект, который будет реагировать на функцию next()
и выдает исключение, когда элементов больше нет. Он не должен быть каким-либо указателем или ссылкой (таких вещей нет в python в смысле C/С++), связанного списка и т.д.
Ответ 3
В python существует протокол итератора, который определяет, как оператор for
будет вести себя со списками и dicts и другими вещами, которые могут быть закодированы.
Это в python docs здесь и здесь.
Как обычно работает протокол итератора, он представляет собой генератор питона. Мы yield
значение, если у нас есть значение, пока мы не достигнем конца, а затем поднимем StopIteration
Итак, напишите наш собственный итератор:
def my_iter():
yield 1
yield 2
yield 3
raise StopIteration()
for i in my_iter():
print i
Результат:
1
2
3
Несколько вещей, чтобы отметить об этом. Функция my_iter - это функция. my_iter() возвращает итератор.
Если бы я написал вместо этого итератор:
j = my_iter() #j is the iterator that my_iter() returns
for i in j:
print i #this loop runs until the iterator is exhausted
for i in j:
print i #the iterator is exhausted so we never reach this line
И результат будет таким же, как и выше. Итер исчерпан к моменту ввода второго цикла.
Но что довольно упрощенно, что о чем-то более сложном? Возможно, может быть, в цикле, почему бы и нет?
def capital_iter(name):
for x in name:
yield x.upper()
raise StopIteration()
for y in capital_iter('bobert'):
print y
И когда он запускается, мы используем итератор для строкового типа (который встроен в iter). Это, в свою очередь, позволяет нам запускать цикл for и давать результаты до тех пор, пока мы не закончим.
B
O
B
E
R
T
Итак, теперь это задает вопрос, так что происходит между выходами в итераторе?
j = capital_iter("bobert")
print i.next()
print i.next()
print i.next()
print("Hey there!")
print i.next()
print i.next()
print i.next()
print i.next() #Raises StopIteration
Ответ заключается в том, что функция приостановлена на выходе, ожидая следующего вызова следующей().
B
O
B
Hey There!
E
R
T
Traceback (most recent call last):
File "", line 13, in
StopIteration
Ответ 4
Некоторые дополнительные сведения о поведении классов iter()
с __getitem__
, у которых отсутствует собственный метод __iter__
.
До __iter__
было __getitem__
. Если __getitem__
работает с int
от 0
- len(obj)-1
, то iter()
поддерживает эти объекты. Он построит новый итератор, который повторно вызывает __getitem__
с 0
, 1
, 2
, ...
, пока не получит IndexError
, который он преобразует в StopIteration
.
Подробнее о различных способах создания итератора см. этот ответ.
Ответ 5
Концепция 1
Все генераторы являются итераторами, но все итераторы не являются генераторами
Концепция 2
Итератором является объект со следующим (Python 2) или next (Python 3) Метод.
Концепция 3
Цитата из вики Generators Генераторы функции позволяют объявлять функцию, которая ведет себя как итератором, то есть его можно использовать в цикле for.
В вашем случае
>>> it = (i for i in range(5))
>>> type(it)
<type 'generator'>
>>> callable(getattr(it, 'iter', None))
False
>>> callable(getattr(it, 'next', None))
True
Ответ 6
Выдержка из книга практики Python:
5. Итераторы и генераторы
5,1. Итераторы
Мы используем оператор for для циклирования над списком.
>>> for i in [1, 2, 3, 4]:
... print i,
...
1
2
3
4
Если мы используем его со строкой, он перебирает свои символы.
>>> for c in "python":
... print c
...
p
y
t
h
o
n
Если мы используем его со словарем, он перебирает его ключи.
>>> for k in {"x": 1, "y": 2}:
... print k
...
y
x
Если мы используем его с файлом, он перемещается по строкам файла.
>>> for line in open("a.txt"):
... print line,
...
first line
second line
Таким образом, существует множество типов объектов, которые могут использоваться с циклом for. Они называются итерируемыми объектами.
Существует множество функций, которые потребляют эти итерации.
>>> ",".join(["a", "b", "c"])
'a,b,c'
>>> ",".join({"x": 1, "y": 2})
'y,x'
>>> list("python")
['p', 'y', 't', 'h', 'o', 'n']
>>> list({"x": 1, "y": 2})
['y', 'x']
5.1.1. Протокол итерации
Встроенная функция iter принимает итерируемый объект и возвращает итератор.
>>> x = iter([1, 2, 3])
>>> x
<listiterator object at 0x1004ca850>
>>> x.next()
1
>>> x.next()
2
>>> x.next()
3
>>> x.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Каждый раз, когда мы вызываем следующий метод на итераторе, мы получаем следующий элемент. Если элементов больше нет, он вызывает StopIteration.
Итераторы реализуются как классы. Вот итератор, который работает как встроенная функция xrange.
class yrange:
def __init__(self, n):
self.i = 0
self.n = n
def __iter__(self):
return self
def next(self):
if self.i < self.n:
i = self.i
self.i += 1
return i
else:
raise StopIteration()
Метод iter - это то, что делает объект итерабельным. За кулисами функция iter вызывает метод iter для данного объекта.
Возвращаемое значение iter - это итератор. Он должен иметь следующий метод и вызывать StopIteration, когда элементов больше нет.
Давайте попробуем:
>>> y = yrange(3)
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 14, in next
StopIteration
Многие встроенные функции принимают итераторы в качестве аргументов.
>>> list(yrange(5))
[0, 1, 2, 3, 4]
>>> sum(yrange(5))
10
В приведенном выше случае итератор и итератор являются одним и тем же объектом. Обратите внимание, что метод iter возвращался сам. Это не обязательно всегда.
class zrange:
def __init__(self, n):
self.n = n
def __iter__(self):
return zrange_iter(self.n)
class zrange_iter:
def __init__(self, n):
self.i = 0
self.n = n
def __iter__(self):
# Iterators are iterables too.
# Adding this functions to make them so.
return self
def next(self):
if self.i < self.n:
i = self.i
self.i += 1
return i
else:
raise StopIteration()
Если оба итератора и итератора являются одним и тем же объектом, он потребляется за одну итерацию.
>>> y = yrange(5)
>>> list(y)
[0, 1, 2, 3, 4]
>>> list(y)
[]
>>> z = zrange(5)
>>> list(z)
[0, 1, 2, 3, 4]
>>> list(z)
[0, 1, 2, 3, 4]
5,2. Генераторы
Генераторы упрощают создание итераторов. Генератор - это функция, которая производит последовательность результатов вместо одного значения.
def yrange(n):
i = 0
while i < n:
yield i
i += 1
Каждый раз, когда выполняется оператор yield, функция генерирует новое значение.
>>> y = yrange(3)
>>> y
<generator object yrange at 0x401f30>
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Таким образом, генератор также является итератором. Вам не нужно беспокоиться о протоколе итератора.
Слово "генератор" смутно используется для обозначения как генерируемой функции, так и того, что она генерирует. В этой главе я использую слово "генератор", чтобы означать, что сгенерированный объект и "функция генератора" означают функцию, которая его генерирует.
Можете ли вы подумать о том, как он работает внутри?
Когда вызывается функция генератора, она возвращает объект-генератор, даже не начиная выполнение функции. Когда в первый раз вызывается следующий метод, функция начинает выполнение до тех пор, пока не достигнет инструкции yield. Полученное значение возвращается следующим вызовом.
В следующем примере показано взаимодействие между выходом и вызовом следующего метода для объекта генератора.
>>> def foo():
... print "begin"
... for i in range(3):
... print "before yield", i
... yield i
... print "after yield", i
... print "end"
...
>>> f = foo()
>>> f.next()
begin
before yield 0
0
>>> f.next()
after yield 0
before yield 1
1
>>> f.next()
after yield 1
before yield 2
2
>>> f.next()
after yield 2
end
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Давайте посмотрим на пример:
def integers():
"""Infinite sequence of integers."""
i = 1
while True:
yield i
i = i + 1
def squares():
for i in integers():
yield i * i
def take(n, seq):
"""Returns first n values from the given sequence."""
seq = iter(seq)
result = []
try:
for i in range(n):
result.append(seq.next())
except StopIteration:
pass
return result
print take(5, squares()) # prints [1, 4, 9, 16, 25]