2018/05/29

yield の使い方

Python のキーワード yield の使い方について説明します。


目次


  • yield とは
  • yield でジェネレータを作る
  • yield でコンテキストマネージャを作る
  • yield from でジェネレータを入れ子にする
  • その他の使い方


yield とは


yield は英語で「生み出す」「生む」「起こす」といった意味の単語とのことで、 Python における「 yield 」は、 コードを構成する構成要素(「キーワード」)のひとつで、 yield 式を作るためのもの です。

def get_abc():
    yield 'a'
    yield 'b'
    yield 'c'

print(list(get_abc()))  # => ['a', 'b', 'c']
print(list(get_abc()))  # => ['a', 'b', 'c']

yield 」は、関数(あるいはメソッド)の中にのみ記述することができるもので、 yield が記述された関数をジェネレータ関数化するものです。

ジェネレータ関数 」とは、関数の中でも少し特殊な関数で、通常の関数が 1 つの値を return で返すのに対して、ジェネレータ関数はジェネレータオブジェクトを返します。

ジェネレータオブジェクト 」(もしくは「ジェネレータイテレータ」(ジェネレータイテレータは『ゼニヤッタ・モンダッタ』みたいで語感がいいですね))とは、ジェネレータによって生成されたイテレータオブジェクトです。

イテレータオブジェクト 」とは、 for ループや組み込みの next() 関数に渡せば値を 1 つずつ返してくれるオブジェクトのことです(厳密には、 __iter__() __next__() の 2 つのメソッドを持っていて、 __iter__() メソッドの戻り値が self (自分自身)のオブジェクトです。参考: Iterator Types )。

for item in イテレータ:
    print(item)

まとめます。

  • yield キーワード: 関数の中で yield 式として使われ、関数をジェネレータ関数にするもの。
  • ジェネレータ関数: 「ジェネレータオブジェクト」を返すもの。
  • ジェネレータオブジェクト: ジェネレータ関数の呼び出しによって生成されたイテレータ。


少し余談ですが、このあたりの用語に関してややこしいと私が思うのは、 Python のプログラムの中では「ジェネレータオブジェクト」に「 generator (ジェネレータ)」という表記がされていて、その一方で、 Python.org の公式ドキュメントでは「ジェネレータ関数」のことを「ジェネレータ」と呼んでいるところです。

ジェネレータオブジェクトのことを「ジェネレータ」と呼ぶのであればそれを生成する関数の方は「ジェネレータ関数」、ジェネレータ関数のことを「ジェネレータ」と呼ぶのであればそれが生成するものは「ジェネレータオブジェクト」と呼ぶ、というように、人と話をするときにはどちらかに統一してから話をしないと混乱しそうです。

この節の最後に、公式のドキュメントからジェネレータの説明に関する部分を抜粋してご紹介します。

Generators are a special class of functions that simplify the task of writing iterators. Regular functions compute a value and return it, but generators return an iterator that returns a stream of values.

翻訳: ジェネレータ(訳注: このジェネレータは「ジェネレータ関数」の意味です)はイテレータをシンプルに書けるようにしてくれる特殊な関数です。 通常の関数はある値を計算して返しますが、ジェネレータは値のストリームを返すイテレータを返します

When you call a generator function, it doesn’t return a single value; instead it returns a generator object that supports the iterator protocol.

翻訳: ジェネレータ関数を呼ぶと、単一の値を返す代わりに、イテレータプロトコルをサポートするジェネレータオブジェクトを返します(訳注: これは上の「値のストリームを返すイテレータ」と同じ意味です)。

The big difference between yield and a return statement is that on reaching a yield the generator’s state of execution is suspended and local variables are preserved. On the next call to the generator’s __next__() method, the function will resume executing.

翻訳: yieldreturn 文の大きな違いは、 yield の場合は処理が yield の行に到達したときにジェネレータの実行状態が一時停止されてローカル変数が保持される点です。ジェネレータの __next__() メソッドが次回呼ばれたときにジェネレータ関数は実行を再開します。

参考

概念的な説明だけではわかりづらいので、つづいてサンプルコードを見ながら説明していきます。


yield でジェネレータを作る


yield 式は上述のとおり、関数の中で使われ、関数をジェネレータ関数に変える働きをします。

def gen_etos():
    ETOS = '子丑寅卯辰巳午未申酉戌亥'
    for eto in ETOS:
        yield eto

この関数を呼び出すと、「ジェネレータオブジェクト」(あるいは「ジェネレータイテレータ」)と呼ばれるオブジェクトが返されます。

g1 = gen_etos()

# ジェネレータ関数の戻り値はジェネレータオブジェクト
type(g1)
# => generator

# ジェネレータかどうかのチェックは types の GeneratorType でできる
import types
isinstance(g1, types.GeneratorType)
# => True

ジェネレータオブジェクトは、 for 文または next() 関数で回すことができます。

g1 = gen_etos()
for value in g1:
    print(value)
# =>
# 子
# 丑
# 寅
# 卯
# 辰
# 巳
# 午
# 未
# 申
# 酉
# 戌
# 亥

g1 = gen_etos()
try:
    while True:
        print(next(g1)) 
except StopIteration:
    pass
# =>
# 子
# 丑
# 寅
# 卯
# 辰
# 巳
# 午
# 未
# 申
# 酉
# 戌
# 亥

ジェネレータ関数は同時に複数のジェネレータオブジェクトを返すことができます。それら複数のジェネレータオブジェクトの内部状態は通常(あえて nonlocal 変数を共有するようなことをしなければ)互いに独立です。

g1 = gen_etos()
g2 = gen_etos()
 
for _ in range(3):
    print(next(g1))
# =>
# 子
# 丑
# 寅

# g2 の内部状態は g1 とは独立
for _ in range(2):
    print(next(g2))
# =>
# 子
# 丑

# g1 の内部状態は g2 とは独立
for _ in range(2):
    print(next(g1))
# =>
# 卯
# 辰

説明をシンプルにするために上の gen_etos() は引数を持たないようにしましたが、ジェネレータ関数も通常の関数と同様に引数を受け取ることができます。実用上は引数を渡すケースの方が多いかと思います。

def get_nonblank_lines(path):
    with open(path) as f:
        for line in f:
            if line.strip():
                yield line

g1 = get_nonblank_lines('sample.dat')
for line in g1:
    print(line, end='')

また上のコードではいずれもわかりやすくするためにジェネレータオブジェクトにあえて g1 という名前を付けましたが、実践ではジェネレータオブジェクトには名前を付けずに、次のように for ループの右側で直接ジェネレータ関数を呼び出す形が多いのではないかと思います。

for line in get_nonblank_lines('sample.dat'):
    print(line, end='')

混乱しやすいのですが、ジェネレータ関数そのものがイテレータなのではなく、ジェネレータ関数の戻り値がイテレータであるという点を覚えておく必要があります。

上の説明には「イテレータ」「 for 文」「 next() 関数」ということばが出てきました。これらの意味を押さえておかないと上の説明を完全に理解するのは難しいので、これらの意味がちょっと怪しいぞという方は一度これらのワードを検索するなり書籍にあたるなりしてからこの記事に戻ってきていただくと意味がスッキリとわかるのではないかと思います。

以上で yield 式とジェネレータの基本の説明は終わりです。続いて、 yield の少し応用的なお話に進みます。

yield でコンテキストマネージャを作る


コンテキストマネージャとは with 式に渡して使えるオブジェクトで、コードブロックに一時的なコンテキストを提供するものです。

with open('sample.log', 'a') as f:
    print('これはファイルに書き込まれます。', file=f)

例えば、組み込み関数の open()with 文に渡すとコンテキストマネージャとしてふるまうオブジェクトを返す関数です。

このコンテキストマネージャの仕組みそのものは yield とは独立のものですが、 yield で作るジェネレータ関数を使って、独自のコンテキストマネージャをかんたんに作ることができます。具体的には、コードブロックを挿入したい位置に yield 文を書いたジェネレータ関数を書き、それを @contextlib.contextmanager でデコレートします。

次のコードは print() の出力先を一時的に指定されたファイルに切り替えるコンテキストマネージャを yield を使って定義した例です。

from contextlib import contextmanager
import sys


@contextmanager
def switch_stdout(path):
    try:
        # print() の出力先を指定されたファイルに切り替える
        sys.stdout = open(path, 'a')
        # with のコードブロックの処理がここで実行される
        yield
    finally:
        # print() の出力先を標準出力に戻す
        # sys.__stdout__ はオリジナルの標準出力を格納したもの
        sys.stdout = sys.__stdout__


print('これは標準出力に出ます。')
with switch_stdout('/tmp/sample.log'):
    print('これは標準出力には出ません。')
    print('これも標準出力には出ません。')
print('これも標準出力に出ます。')

このコードを実行すると、 with のコードブロック内の 2 つの print() 関数の文字列は /tmp/sample.log に出力されます。一方、 with のコードブロックの外(=前後)にある print() 関数の文字列は通常どおり標準出力に出力されます。

ちなみに、この switch_stdout() と同等の機能を持つ @contextlib.redirect_stdout が標準ライブラリにすでに用意されています。


上の例のコンテキストマネージャでは yield 式に値を渡していませんが、 yield に値を渡すこともできます。コンテキストマネージャとなるジェネレータ関数の yield に渡された値は、利用時に with 文の as で受け取ることができます。

例えば、次のような中身の bookmarks テーブルを持つ SQLite のデータベースファイルがあるものとします。

url,title 
"https://www.google.co.jp","Google"
"https://www.instagram.com","Instagram"

$ sqlite3 db.sqlite3 >> EOS
CREATE TABLE bookmarks (url text, title text);
INSERT INTO bookmarks VALUES ("https://www.google.co.jp", "Google");
INSERT INTO bookmarks VALUES ("https://www.instagram.com", "Instagram");
EOS

このデータベースのアクセスするためのコンテキストマネージャとして次のようなものを作ることができます。ここで yield に渡されたコネクションオブジェクトは withas で取得・利用することが可能です。

from contextlib import contextmanager
import sqlite3


@contextmanager
def sqlite_conn(path):
    try:
        conn = sqlite3.connect(path)
        # コネクションオブジェクトをコンテキストに渡す
        yield conn
    finally:
        if 'conn' in locals():
            conn.close()

# コネクションオブジェクトを as で受け取る
with sqlite_conn('db.sqlite3') as conn:
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM bookmarks')
    for row in cursor:
        print(row)

このように、通常のジェネレータ関数の yield とコンテキストマネージャの yield は意味合い・使い方が大きく異なるので注意が必要です。ジェネレータ関数の yield 式は「何らかの値を受け取ること」や「繰り返し呼ばれること」が前提で使われてますが、コンテキストマネージャの yield 式は値を返すとはかぎらず、原則一度しか呼ばれません。

他のスクリプト言語に馴染みのある方には、 Python のコンテキストマネージャは、 Ruby であれば「ブロックを受け取るメソッド」、 JavaScript であれば「無名のコールバック関数」に近いものと考えるとわかりやすいかと思います。


yield from でジェネレータを入れ子にする


Python 3.3 以降では yield from 文で、ジェネレータ関数の入れ子がかんたんに実現できるようになっています。

次のジェネレータ関数 gen_numbers_all() は、ジェネレータ関数 gen_numbers_1()gen_numbers_2() のジェネレータオブジェクトをそのまま結合したようなふるまいをします。

def gen_numbers_1():
    for n in 'AKQJ':
        yield n

def gen_numbers_2():
    for n in '98765432':
        yield n

def gen_numbers_all():
    # 他のジェネレータ関数で作られたジェネレータオブジェクトが 
    # yield した値をそのまま yield する
    yield from gen_numbers_1()
    yield from gen_numbers_2()

print(list(gen_numbers_1()))
# =>
# ['A', 'K', 'Q', 'J']

print(list(gen_numbers_2()))
# =>
# ['9', '8', '7', '6', '5', '4', '3', '2']

print(list(gen_numbers_all()))
# =>
# ['A', 'K', 'Q', 'J', '9', '8', '7', '6', '5', '4', '3', '2']

この yield from 式については、ジェネレータオブジェクトの内部状態を変更する send() メソッド等を使った使い方等もう少し複雑な使い方の例も Python.org の公式ドキュメントでは紹介されています。 yield from についてもっと詳しく知りたい方は公式のドキュメントにも目を通してみてください。



その他の使い方


上で見た使い方の他に、 asyncio で利用するコルーチンを定義するためにも yield 式は利用することができます。ただ、この使い方は用途が限られる&私は asyncio を使う必要に迫られず経験が不足しているので、ここで語るのはやめておきます。

気になる方は公式のページ等をご覧ください。


以上、 Python の yield の使い方についてでした。


参考


他サイト

ブログ内

0 件のコメント: