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]