Основы итеративного процесса

Beginner

This tutorial is from open-source community. Access the source code

Введение

В этом разделе рассматривается основательный процесс итерации.

Итерация повсюду

Многие разные объекты поддерживают итерацию.

a = 'hello'
for c in a: ## Перебираем символы в a
  ...

b = { 'name': 'Dave', 'password': 'foo'}
for k in b: ## Перебираем ключи в словаре
  ...

c = [1,2,3,4]
for i in c: ## Перебираем элементы в списке/кортеже
  ...

f = open('foo.txt')
for x in f: ## Перебираем строки в файле
  ...

Итерация: Протокол

Рассмотрим оператор for.

for x in obj:
    ## statements

Что происходит "под капотом"?

_iter = obj.__iter__()        ## Получаем объект-итератор
while True:
    try:
        x = _iter.__next__()  ## Получаем следующий элемент
        ## statements...
    except StopIteration:     ## Больше элементов нет
        break

Все объекты, которые работают с циклом for, реализуют этот низкоуровневый протокол итерации.

Пример: Ручная итерация по списку.

>>> x = [1,2,3]
>>> it = x.__iter__()
>>> it
<listiterator object at 0x590b0>
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in? StopIteration
>>>

Поддержка итерации

Знание о итерации полезно, если вы хотите добавить его в свои собственные объекты. Например, создать пользовательский контейнер.

class Portfolio:
    def __init__(self):
        self.holdings = []

    def __iter__(self):
        return self.holdings.__iter__()
 ...

port = Portfolio()
for s in port:
 ...

Упражнение 6.1: Итерация, проиллюстрированная

Создайте следующий список:

a = [1,9,4,25,16]

Вручную итерируйтесь по этому списку. Вызовите __iter__(), чтобы получить итератор, и вызовите метод __next__(), чтобы получить последовательные элементы.

>>> i = a.__iter__()
>>> i
<listiterator object at 0x64c10>
>>> i.__next__()
1
>>> i.__next__()
9
>>> i.__next__()
4
>>> i.__next__()
25
>>> i.__next__()
16
>>> i.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

Встроенная функция next() - это сокращение для вызова метода __next__() итератора. Попробуйте использовать ее для файла:

>>> f = open('portfolio.csv')
>>> f.__iter__()    ## Примечание: Возвращает сам файл
<_io.TextIOWrapper name='portfolio.csv' mode='r' encoding='UTF-8'>
>>> next(f)
'name,shares,price\n'
>>> next(f)
'"AA",100,32.20\n'
>>> next(f)
'"IBM",50,91.10\n'
>>>

Продолжайте вызывать next(f), пока не дойдете до конца файла. Посмотрите, что произойдет.

Упражнение 6.2: Поддержка итерации

Иногда вы можете захотеть, чтобы один из своих собственных объектов поддерживал итерацию - особенно если ваш объект оборачивает существующий список или другой итерируемый объект. В новом файле portfolio.py определите следующий класс:

## portfolio.py

class Portfolio:

    def __init__(self, holdings):
        self._holdings = holdings

    @property
    def total_cost(self):
        return sum([s.cost for s in self._holdings])

    def tabulate_shares(self):
        from collections import Counter
        total_shares = Counter()
        for s in self._holdings:
            total_shares[s.name] += s.shares
        return total_shares

Этот класс предназначен для обертки вокруг списка, но с некоторыми дополнительными методами, такими как свойство total_cost. Измените функцию read_portfolio() в report.py, чтобы она создавала экземпляр Portfolio так:

## report.py

...

import fileparse
from stock import Stock
from portfolio import Portfolio

def read_portfolio(filename):
    '''
    Считывает файл портфеля акций в список словарей с ключами
    name, shares и price.
    '''
    with open(filename) as file:
        portdicts = fileparse.parse_csv(file,
                                        select=['name','shares','price'],
                                        types=[str,int,float])

    portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
    return Portfolio(portfolio)

...

Попробуйте запустить программу report.py. Вы обнаружите, что она spectacularly завершается из-за того, что экземпляры Portfolio не итерируемы.

>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
... crashes...

Исправьте это, изменив класс Portfolio для поддержки итерации:

class Portfolio:

    def __init__(self, holdings):
        self._holdings = holdings

    def __iter__(self):
        return self._holdings.__iter__()

    @property
    def total_cost(self):
        return sum([s.shares*s.price for s in self._holdings])

    def tabulate_shares(self):
        from collections import Counter
        total_shares = Counter()
        for s in self._holdings:
            total_shares[s.name] += s.shares
        return total_shares

После внесения этих изменений программа report.py должна снова работать. В то же время исправьте программу pcost.py, чтобы она использовала новый объект Portfolio. Так:

## pcost.py

import report

def portfolio_cost(filename):
    '''
    Вычисляет общую стоимость (количество акций * цена) файла портфеля
    '''
    portfolio = report.read_portfolio(filename)
    return portfolio.total_cost
...

Протестируйте ее, чтобы убедиться, что все работает:

>>> import pcost
>>> pcost.portfolio_cost('portfolio.csv')
44671.15
>>>

Упражнение 6.3: Создание более правильного контейнера

Если вы создаете класс-контейнер, вы часто хотите сделать больше, чем просто итерацию. Измените класс Portfolio так, чтобы он имел некоторые другие специальные методы, как это:

class Portfolio:
    def __init__(self, holdings):
        self._holdings = holdings

    def __iter__(self):
        return self._holdings.__iter__()

    def __len__(self):
        return len(self._holdings)

    def __getitem__(self, index):
        return self._holdings[index]

    def __contains__(self, name):
        return any([s.name == name for s in self._holdings])

    @property
    def total_cost(self):
        return sum([s.shares*s.price for s in self._holdings])

    def tabulate_shares(self):
        from collections import Counter
        total_shares = Counter()
        for s in self._holdings:
            total_shares[s.name] += s.shares
        return total_shares

Теперь попробуйте провести некоторые эксперименты с использованием этого нового класса:

>>> import report
>>> portfolio = report.read_portfolio('portfolio.csv')
>>> len(portfolio)
7
>>> portfolio[0]
Stock('AA', 100, 32.2)
>>> portfolio[1]
Stock('IBM', 50, 91.1)
>>> portfolio[0:3]
[Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1), Stock('CAT', 150, 83.44)]
>>> 'IBM' in portfolio
True
>>> 'AAPL' in portfolio
False
>>>

Одно важное наблюдение по этому поводу - в общем, код считается "питонистским", если он использует общий язык, которым другие части Python обычно работают. Для контейнерных объектов поддержка итерации, индексирования, вхождения и других типов операторов является важной частью этого.

Резюме

Поздравляем! Вы завершили лабораторную работу по Протоколу итерации. Вы можете практиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.