🙄

python3 構文トリビア

2021/09/06に公開

コロンの直後には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