ジェネレータ関数を使った反復処理のカスタマイズ

PythonPythonBeginner
今すぐ練習

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

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

このセクションでは、ジェネレータ関数を使って反復処理をカスタマイズする方法を見ていきます。

問題

独自のカスタム反復処理パターンを作成したいとしましょう。

たとえば、カウントダウンです。

>>> for x in countdown(10):
...   print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1
>>>

これを行う簡単な方法があります。

ジェネレータ

ジェネレータは、反復処理を定義する関数です。

def countdown(n):
    while n > 0:
        yield n
        n -= 1

たとえば:

>>> for x in countdown(10):
...   print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1
>>>

ジェネレータは、yield 文を使用する任意の関数です。

ジェネレータの動作は通常の関数とは異なります。ジェネレータ関数を呼び出すと、ジェネレータオブジェクトが作成されます。関数はすぐに実行されません。

def countdown(n):
    ## 出力文を追加
    print('Counting down from', n)
    while n > 0:
        yield n
        n -= 1
>>> x = countdown(10)
## 出力文は表示されない
>>> x
## xはジェネレータオブジェクト
<generator object at 0x58490>
>>>

関数は __next__() 呼び出し時にのみ実行されます。

>>> x = countdown(10)
>>> x
<generator object at 0x58490>
>>> x.__next__()
Counting down from 10
10
>>>

yield は値を生成しますが、関数の実行を中断します。関数は次の __next__() 呼び出し時に再開されます。

>>> x.__next__()
9
>>> x.__next__()
8

ジェネレータが最終的に返すと、反復処理はエラーを発生させます。

>>> x.__next__()
1
>>> x.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in? StopIteration
>>>

注:ジェネレータ関数は、for文がリスト、タプル、辞書、ファイルなどで使用するのと同じ低レベルのプロトコルを実装しています。

演習6.4:単純なジェネレータ

反復処理をカスタマイズしたいときは、常にジェネレータ関数を考えるべきです。書きやすいです。望ましい反復処理ロジックを実行する関数を作成し、値を発行するために yield を使用します。

たとえば、このジェネレータを試してみてください。これは、一致するサブ文字列を含む行をファイルから検索します。

>>> def filematch(filename, substr):
        with open(filename, 'r') as f:
            for line in f:
                if substr in line:
                    yield line

>>> for line in open('portfolio.csv'):
        print(line, end='')

name,shares,price
"AA",100,32.20
"IBM",50,91.10
"CAT",150,83.44
"MSFT",200,51.23
"GE",95,40.37
"MSFT",50,65.10
"IBM",100,70.44
>>> for line in filematch('portfolio.csv', 'IBM'):
        print(line, end='')

"IBM",50,91.10
"IBM",100,70.44
>>>

これは面白いです。関数にカスタム処理を隠し、forループに供給するために使用できるという考え方です。次の例は、もっと珍しいケースを見ています。

演習6.5:ストリーミングデータソースの監視

ジェネレータは、ログファイルや株価ニュースなどのリアルタイムデータソースを監視する興味深い方法になります。このパートでは、この考え方を探っていきます。まず、次の指示に従ってください。

stocksim.py というプログラムは、株価データをシミュレートするプログラムです。出力として、このプログラムはリアルタイムデータを stocklog.csv というファイルに常に書き込みます。別のコマンドウィンドウで、`` ディレクトリに移動してこのプログラムを実行します。

$ python3 stocksim.py

Windowsの場合は、stocksim.py プログラムを見つけてダブルクリックして実行します。これで、このプログラムは忘れておいてください(ただ実行させておきます)。別のウィンドウを使って、シミュレータによって書き込まれている stocklog.csv ファイルを見てみましょう。数秒ごとに新しいテキスト行がファイルに追加されているはずです。再び、このプログラムはバックグラウンドで実行させておいてください。数時間実行されます(心配する必要はありません)。

上記のプログラムが実行されたら、ファイルを開き、末尾まで移動して、新しい出力を監視するための小さなプログラムを書きましょう。follow.py というファイルを作成し、次のコードを入れます。

## follow.py
import os
import time

f = open('stocklog.csv')
f.seek(0, os.SEEK_END)   ## ファイル末尾から0バイト分移動してファイルポインタを移動させる

while True:
    line = f.readline()
    if line == '':
        time.sleep(0.1)   ## 少し待ってから再試行
        continue
    fields = line.split(',')
    name = fields[0].strip('"')
    price = float(fields[1])
    change = float(fields[4])
    if change < 0:
        print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

このプログラムを実行すると、リアルタイムの株価情報が表示されます。このコードは、裏では、ログファイルを監視するために使用されるUnixの tail -f コマンドに似ています。

注:この例では、readline() メソッドの使用は、通常のファイルからの行の読み取り方法とは少し異なります(通常は for ループを使います)。ただし、この場合、ファイルの末尾を繰り返し調べて、新しいデータが追加されたかどうかを確認するために使用しています(readline() は新しいデータまたは空の文字列を返します)。

演習6.6:データ生成にジェネレータを使用する

演習6.5のコードを見ると、コードの最初の部分はデータの行を生成していますが、while ループの末尾の文はデータを消費しています。ジェネレータ関数の主な特徴は、すべてのデータ生成コードを再利用可能な関数に移動できることです。

演習6.5のコードを変更して、ファイル読み取りをジェネレータ関数 follow(filename) で行うようにします。以下のコードが機能するようにします。

>>> for line in follow('stocklog.csv'):
          print(line, end='')

... ここで出力の行が表示されるはずです...

株価情報表示のコードを変更して、次のようになるようにします。

if __name__ == '__main__':
    for line in follow('stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if change < 0:
            print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')
✨ 解答を確認して練習

演習6.7:あなたのポートフォリオを監視する

follow.py プログラムを変更して、株価データのストリームを監視し、ポートフォリオ内の特定の株に関する情報を表示する株価情報表示を行うようにします。たとえば:

if __name__ == '__main__':
    import report

    portfolio = report.read_portfolio('portfolio.csv')

    for line in follow('stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if name in portfolio:
            print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

注:これが機能するには、Portfolio クラスが in 演算子をサポートする必要があります。演習6.3を参照して、__contains__() 演算子を実装してください。

✨ 解答を確認して練習

考察

ここで非常に強力なことが起こりました。興味深い反復処理パターン(ファイルの末尾の行を読み取る)を独自の小さな関数に移動しました。follow() 関数は、今や、あらゆるプログラムで使用できる完全に汎用的なユーティリティになっています。たとえば、サーバーログ、デバッグログ、その他の同様のデータソースを監視するために使用できます。これはかなり面白いです。

まとめ

おめでとうございます!あなたは「反復処理のカスタマイズ」の実験を完了しました。あなたの技術を向上させるために、LabExでさらに実験を行って練習してください。