python3 構文トリビア
コロンの直後には1文だけ書ける
exec("for i in range(10):\n\tprint(i)")
これは
for i in range(10):print(i)
もともと1行で書ける。execとか要らない。
これはコロンを使う全ての構文であてはまる。def,class,while,try/catch,if....他なにかあったっけ。
フレームワークでよく見かける、空の例外クラス定義はこれに当てはまる。
class OreOreException(Exception): pass
式の継続が明らかな場合は、任意箇所で改行できる
例えば内包表記
xx = [ "even" if i % 2 == 0 else "odd"
for i in range(10) ]
こう書ける。
同じ理由で、丸括弧をつければ、閉じるまでが1行であることが確定するので、無理くり複数行にも書ける。
丸括弧+カンマを付けると、後述のとおり、ジェネレータ式に化けるので注意。
コロンでネストした直後は、{クラス|関数}定義もネストできる。
def fun1():
class cls1():
def method1(self):
def fun3(self):
return self
return fun3
pass
return cls1
- class/defのネスト自体は通常のクラス定義だが、defの直後で使えている。
- defのネストは、無名関数に近い挙動をする。
- fun1を実行するまで、cls1,fun3は存在すらしていない。
- fun1は何回も実行できる。
- fun1を実行するたびに、内側の全て cls1,fun3 は再定義される。
- 表面上は fun1 しか見えず、外から直接 cls1, fun3 を使う手段はない。
- 引数付きデコレータは、この構文を積極的に利用している。
上のfun1を実際に使ってみると次のようになる
>>> fun1
<function fun1 at 0x7f0a0a878af0>
>>> c1=fun1()()
>>> c1
<__main__.fun1.<locals>.cls1 object at 0x7f0a0a868f70>
>>> c1.method1()
<function fun1.<locals>.cls1.method1.<locals>.fun3 at 0x7f0a0a878b80>
<locals>という特殊な名前空間が使われてることがわかる。
3項演算子はないが条件式(conditional expression)はある
特殊な表記ではあるが、式構文のなかで実は if が使える。
6. 式 (expression) — Python 3.9.2 ドキュメント
PEP 308 -- Conditional Expressions | Python.org
i_is = "even" if i % 2 == 0
なお、Python/PEP-308ではこれは明確に「条件式」と呼んでるので、3項演算子と呼ぶのは間違いである。
(極めて類似している機能ではある)
内包表記等で、for の後ろに付けるのも、仕様上は 条件式になるらしい。
even_nums = [i for i in range(10) if i % 2 == 0]
or 演算子は、論理和ではなくて、偽ではない最初値が出てくる
>>> 0 or False or None or [] or "A"
'A'
coalesceと呼ばれる挙動で、SQLにはそのもの関数がある。
うまく使えば3項演算子的に使えなくもない
ジェネレータ式
内包表記は、カギカッコ(pythonのドキュメントでは角括弧と呼んでるようだ)で囲むと直接値を返すが.....
>>> [ "even" if i % 2 == 0 else "odd"
for i in range(10) ]
['even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd']
丸括弧の場合は、generatorを返す。これは関数からyieldで帰ってくるものと同じものだ。
タプル内包表記とか言っちゃってる人が居たが、明確に誤解である。
ただ、構文自体は内包表記と全く同じであるなので、言いたいのはわからんでもない。
>>> ( "even" if i % 2 == 0 else "odd"
for i in range(10) )
<generator object <genexpr> at 0x7f308442a200>
このジェネレータ式に対して、おなじみの角括弧の内包表記の方は、正式にはリスト内包表記と呼ぶようだ。
ジェネレータ式も、リスト内包表記も、結果的にiterableインターフェースを持つため、ループで使える。
が、ジェネレータ式は次の値をその場で作るので、内包表記内部の処理&繰り返し数によっては、リスト内包表記よりメモリを節約できる可能性がある。
関数型プログラミング HOWTO — Python 3.9.1 ドキュメント
ちなみに関数呼び出しの場合、いきなりジェネレータ式を受け付ける。
関数呼び出しの引数構文の中なので、式を仮定した状態から始まってるからだと思われる。
>>> type( "even" if i % 2 == 0 else "odd"
for i in range(10) )
<class 'generator'>
type()の引数として、ジェネレータオブジェクトが生成されてるのがわかる。
しかし、行頭でいきなりジェネレータ式を裸で使っても怒られるだけである。
"even" if i % 2 == 0 else "odd" for i in range(10)
File "<stdin>", line 1
"even" if i % 2 == 0 else "odd" for i in range(10)
^
これらの違いは、エラーの位置はforなので、パーザが式として確定しきれてないためだと思われる。
よほどの頓智ならともかく、丸括弧は必須だと思ったほうがよいだろう。
応用例
pythonとしてはマイナーなORMだが、Pony ORM では、積極的にこの挙動を利用してる。
Queries — Pony ORM documentation
query = select(c for c in Customer
if sum(o.total_price for o in c.orders) > 1000)
ジェネレータオブジェクトをAST解析することにより、検索内容を逆算して、SQLを生成するようだ。
高度かつスタイリッシュな実装とは言えるが、オーバーヘッドが気にはなる。
関数呼び出しの、末尾の余計なカンマ1個は無視してくれる
これは引数展開時の独特の挙動のようだ。
ちょっと調べた程度では、ドキュメントに明記が見当たらなかった。
>>> str(1,)
'1'
>>> str(1)
'1'
Discussion