ジェネレータ式の文法について調べてみた

Posted by rhoboro on 2017-07-08

先日ツイッター上でつぶやいたジェネレータ式についての話です。

Pythonのジェネレータ式

Pythonのジェネレータ式は下記のような内包表記を()で括ったものですね。
ジェネレータを生成してくれます。

>>> chars = ['a', 'b', 'c']
>>> gen = (c for c in chars) <= ジェネレータ式からジェネレータを生成
>>> type(gen)
<class 'generator'>

tupleやlistはジェネレータを引数として受け取れます。

>>> chars = ['a', 'b', 'c']
>>> gen = (c for c in chars)
>>> tuple(gen)
('a', 'b', 'c')
>>> tuple((c for c in chars))
('a', 'b', 'c')

ここまでは特に問題ありません。気になったのは下記がシンタックスエラーにならず動いたことでした。

>>> chars = ['a', 'b', 'c']
>>> tuple(c for c in chars)
('a', 'b', 'c')

「え?何で??()が1つ足りないじゃん!」と少し気になったのでPythonのジェネレータ式の文法について調べてみることにしました。

Pythonの文法の調べ方

プログラミング言語である書き方が動くか動かないかは少し試せばわかりますよね。これは簡単です。 ただ、文法に対しては「なぜその書き方で動くのか」ということをこれまで調べた経験がなかったので、いざ調べようとしてもどう調べればいいかわからず躓きました(笑)

結論から言うと、Pythonは公式ドキュメントに10. Full Grammar specificationとして文法仕様が記載されていました。 ちなみにこの記法はバッカス・ナウア記法(BNF)というものらしいです。

また、本題のジェネレータ式の()については、6.2.8. Generator expressionsで下記のように関数の唯一の引数として渡す場合、()を省略できることが記載されていました。

The parentheses can be omitted on calls with only one argument. See section Calls for details. (関数の唯一の引数として渡す場合には、丸括弧を省略できます。詳しくは 呼び出し (call) 節を参照してください。)

せっかくなので、Callsの文法を見てみます。

call ::= primary "(" [argument_list [","] | comprehension] ")"

BNFでは[]は省略可能を、|はいずれか一方を表しています。
つまり、callは 引数のリストまたは1つの内包表記(いずれも省略可能) を "(" と ")" でくくったもの。
これが本題のtuple(c for c in chars)がシンタックスエラーにならない理由でした。

>>> chars = ['a', 'b', 'c']
>>> tuple(c for c in chars) <= 1つの内包表記
('a', 'b', 'c')
>>>
>>> tuple(c for c in chars, 'd') <= 内包表記を書けるのは引数が1つのときだけ
  File "<stdin>", line 1
SyntaxError: Generator expression must be parenthesized if not sole argument

スッキリしました。

おまけ

comprehension(内包表記)へ飛ぶと下記のようになっています。

comprehension ::= expression comp_for
comp_for ::= [ASYNC] "for" target_list "in" or_test [comp_iter]
comp_iter ::= comp_for | comp_if
comp_if ::= "if" expression_nocond [comp_iter]

内包表記は 式(expression) と for文(comp_for) になっています。
式には任意の式が書けるため、 for c in chars のcを式中で必ずしも使う必要はありません。 (先日同僚が勘違いしていたので)