反復処理の基本原理

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:
    ## 文

内部で何が起こるのでしょうか。

_iter = obj.__iter__()        ## イテレータオブジェクトを取得する
while True:
    try:
        x = _iter.__next__()  ## 次の要素を取得する
        ## 文...
    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: 反復処理のサポート

時には、自分自身のオブジェクトの 1 つに反復処理をサポートさせたい場合があります。特に、既存のリストやその他の反復可能オブジェクトをラップしているオブジェクトの場合です。新しいファイル 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 プロパティなどの追加メソッドがあります。report.pyread_portfolio() 関数を変更して、次のように 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 プログラムを実行してみると、Portfolio インスタンスが反復可能でないため、劇的に失敗することがわかります。

>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
... クラッシュ...

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 プログラムが再び機能するはずです。その際、新しい Portfolio オブジェクトを使用するように pcost.py プログラムを修正します。次のようになります。

## 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
>>>

これに関する重要な観察点は 1 つです。一般的に、コードが Python の他の部分が通常どのように機能するかの共通の語彙を使っている場合、そのコードは「Python 風」と考えられます。コンテナオブジェクトの場合、反復処理、インデックス付け、含まれるかどうかの判定、その他の種類の演算子をサポートすることは、この重要な部分の 1 つです。

まとめ

おめでとうございます!あなたは反復処理プロトコルの実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに多くの実験を行って練習することができます。