長い内包はしんどい
Pythonでは一般に、コレクションは内包表記で生成する(と思う)。
しかし少し複雑になると、内包表記は読むのがしんどい。
# Fizz Buzzリストを無理やり内包で
fb = ["Fizz Buzz" if i % 15 == 0
else "Fizz" if i % 3 == 0
else "Buzz" if i % 5 == 0
else i
for i in range(30)]
これなら、for文を書いた方が見やすい。
だが初期化前のオブジェクトを露出させたまま、少しずつ変更を加える書き方は、(関数型嗜好が強いと)なんとなく罪悪感がある。
fb = []
for i in range(30):
if i % 15 == 0:
fb.append("Fizz Buzz")
elif i % 3 == 0:
fb.append("Fizz")
elif i % 5 == 0:
fb.append("Buzz")
else:
fb.append(i)
どうするか?
ジェネレータ+コレクタ
yieldでジェネレータを定義し、それを元にコレクションを作る書き方が可能。
def _fb():
for i in range(30):
if i % 15 == 0:
yield "Fizz Buzz"
elif i % 3 == 0:
yield "Fizz"
elif i % 5 == 0:
yield "Buzz"
else:
yield i
fb = list(_fb)
もっと簡潔に
↑も悪くはないが、ジェネレータを定義した後、list関数を通し、変数に代入する手順が煩雑。
また、listにするのを忘れ、イテレータを直接変数で受けてしまうとバグの温床になりうる。
yieldを使った定義がそのままリストの定義として解釈されてほしい。
デコレータを使えば実現できる。
def comprehension(collector):
def ret(func):
return collector(func())
return ret
# 以下を実行すると、変数「fb」にリストが代入される。
@comprehension(list)
def fb():
for i in range(30):
if i % 15 == 0:
yield "Fizz Buzz"
elif i % 3 == 0:
yield "Fizz"
elif i % 5 == 0:
yield "Buzz"
else:
yield i
元ネタ
funcyというライブラリにあるcollectingというデコレータが第一の元ネタ。ジェネレータ関数を、リストを返す関数に変える。これを使うとFizz Buzzは以下のように書ける。
from funcy import collecting
@collecting
def _fb():
for i in range(30):
if i % 15 == 0:
yield "Fizz Buzz"
elif i % 3 == 0:
yield "Fizz"
elif i % 5 == 0:
yield "Buzz"
else:
yield i
fb = _fb()
でも一旦関数を定義する手間が面倒。
Racketのfor/listみたいに書ければ……と思って、デコレータで関数を返さない解法を思いついた。