##前置き
ハロウィンネタ第三弾。
- 第一弾: ハロウィンなのでPythonにほげほげさせてみる
- 第二弾: ハロウィンなのでPythonを八つ裂きにしてみる 自信作
例えば、こんなコードがあったとする。世にありふれたFizzbuzzコードだ。
def fizzbuzz(n):
if n % 15 == 0:
return 'fizzbuzz'
if n % 3 == 0:
return 'fizz'
if n % 5 == 0:
return 'buzz'
return str(n)
for i in range(20):
print(fizzbuzz(i+1))
**実行結果** (クリックで開く)
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
16
17
fizz
19
buzz
しかし、fizzbuzz(i+1)
の i+1
が醜悪に映る。
せっかくfor文に突っ込んでいるのだから、引数くらい空気読んで推測してほしいと思わないだろうか? 1
次のように呼び出しても同じように動作してほしいものだ。
for _ in range(20):
print(fizzbuzz())
実引数の神隠しに挑戦する。
###本稿の最終成果物
こんなものを作ってみた。
# ↓ ここから右は気にしないこと
def fizzbuzz(n=(lambda mi: (__import__('sys').setprofile(mi.hook), mi)[-1])(type('MyInt', (object, ), {'__init__': (lambda self, n: setattr(self, '_n', n)), 'hook': (lambda self, frame, event, _: setattr(self, '_n', self._n + ((frame.f_code.co_name, event) == ('fizzbuzz', 'return')))), '__mod__': (lambda self, other: self._n % other), '__str__': (lambda self: str(self._n)),})(1))):
if n % 15 == 0:
return 'fizzbuzz'
if n % 3 == 0:
return 'fizz'
if n % 5 == 0:
return 'buzz'
return str(n)
for _ in range(20):
print(fizzbuzz())
実行結果 ⇒ Wandbox
もちろん普通に実行でき、実行結果は改造前のものと同じだ。
本稿ではどのようにこのコードが完成したか解説する。
##作ってみよう: まずは素直に
###ステップ0: 方針の確認
単に実引数を省略するだけなら、いくらでも実現方法がある。
本節では素直な各解法とその難点 (あるいは難癖) をつらつら書き、目標を明確にしていきたいと思う。
####モジュール変数を利用する解法
default_n = 1
def fizzbuzz(n=None):
global default_n
if n is None:
n = default_n
default_n += 1
if n % 15 == 0:
...
悪しきglobal宣言が登場し、しかも関数の実装にも手を付けなければならない。論外である。
####クロージャを利用する解法
def make_fizzbuzz(n_start=1):
default_n = n_start
def _fizzbuzz(n=None):
nonlocal default_n
if n is None:
n = default_n
default_n += 1
if n % 15 == 0:
...
return _fizzbuzz
fizzbuzz = make_fizzbuzz()
直前のモジュール変数を用いる方法に対し、いくつかの点で優れている。
- nの初期値(n_start)をfizzbuzz関数生成時に指定できる。
- default_nのスコープが関数内に閉じ込められる。モジュール変数より幾倍もマシ。
しかしfizzbuzz関数の実装に大きく手を加える必要がある。
####クラスを利用する解法
class CountedUpInt:
def __init__(self, start_n=1):
self._n = start_n
def __int__(self):
n = self._n
self._n += 1
return n
def fizzbuzz(n=CountedUpInt()):
n = int(n)
if n % 15 == 0:
...
素直な解決法。分かり易く2、改修も容易である。
しかし、この方法でもfizzbuzz関数の実装内部に手を付けなければならない点に変わりは無い。
fizzbuzz関数のコード部に手を触れず、デフォルト引数の工夫だけで神隠しを実現することを目標とする。
####クロージャを利用する解法・改
高階関数を上手く活用すれば、fizzbuzz関数の実装にも触れずに済む。
デコレータを併せて使うと可読性も向上する。
def countup_wrapper(n_start):
default_n = n_start
def _inner1(func):
def _inner2(n=None):
nonlocal default_n
if n is None:
n = default_n
default_n += 1
return func(n)
return _inner2
return _inner1
@countup_wrapper(1)
def fizzbuzz(n):
...
記事を大方書き終えた後に思い付いた。 もうこれで良くね?
せっかく書いた記事が無駄になってしまうので、nonlocalアレルギーを盾に取って潔く無視させて欲しい。
デフォルト引数だけに手を加える縛りとする。
###ステップ1: 数のように振る舞うオブジェクトを作る
コード部に手を触れない以上、デフォルト値は数値と同じように取り扱うことができなければならない。
例えば剰余算を実現する為には、次のようにクラスを組めば良い。3
class MyInt:
def __init__(self, n):
self._n = n
def __mod__(self, other):
return self._n % other
def __str__(self):
return str(self._n)
print(16 % 6) # => 4
print(MyInt(16) % 6) # => 4
__mod__
は剰余算を担う特殊メソッドで、a % b
は a.__mod__(b)
と同値である。
参考: Python 言語リファレンス » 3. データモデル » 3.3.8. 数値型をエミュレートする
この時点で、次のようなコードを書くことが可能だ。
ただしカウントアップ処理は未実装であるので、当然同じ数を繰り返し判定し続けることとなる。
def fizzbuzz(n=MyInt(1)):
if n % 15 == 0:
...
###ステップ2: 適切なタイミングでカウントアップする方法を考える
fizzbuzz関数の実装に関与しないのであれば、関数の呼び出し/脱出をフックするしか無い。
ここでは sys.setprofile 関数を 悪用 転用し、フックを実現しようと思う。
class MyInt:
...
def succ(self):
self._n += 1
...
import sys
myInt = MyInt(1)
def _hook(frame, event, arg):
# frame.f_code.co_name イベント対象の関数名
# event 'call' あるいは 'return'
if (frame.f_code.co_name, event) == ('fizzbuzz', 'return'):
myInt.succ()
sys.setprofile(_hook)
#
def fizzbuzz(n=myInt):
if n % 15 == 0:
...
参考: Python 標準ライブラリ » inspect - 型とメンバー
この時点で、前述の目的『fizzbuzz関数のコード部に手を触れず~』は既に達成している。
###成果物α: 素直(?)な実装
class MyInt:
def __init__(self, n):
self._n = n
def succ(self):
self._n += 1
def __mod__(self, other):
return self._n % other
def __str__(self):
return str(self._n)
import sys
myInt = MyInt(1)
def _hook(frame, event, arg):
if (frame.f_code.co_name, event) == ('fizzbuzz', 'return'):
myInt.succ()
sys.setprofile(_hook)
#
def fizzbuzz(n=myInt):
if n % 15 == 0:
return 'fizzbuzz'
if n % 3 == 0:
return 'fizz'
if n % 5 == 0:
return 'buzz'
return str(n)
for _ in range(20):
print(fizzbuzz())
実行結果 ⇒ Wandbox
##作ってみよう: 行数を減らす
しかし先の成果物αには、fizzbuzz関数以外の記述が多過ぎる。
あくまでfizzbuzzが主目的であることを強調する為にも、できるだけ行数を減らして書いてやろう4。
###ステップ1: オブジェクトをまとめる
後の記述を少しでも楽にする為に、オブジェクトをある程度まとめておく。
例えば_hook関数はMyIntクラスのメソッドとして取り入れてしまっても支障無いだろう。
class MyInt:
...
def hook(self, frame, event, arg):
if (frame.f_code.co_name, event) == ('fizzbuzz', 'return'):
self._n += 1
myInt = MyInt(1)
sys.setprofile(myInt.hook)
後の改変の都合上、succメソッドは取り除いた。
###ステップ2: MyIntクラスのワンライン化
####ステップ2.1: メソッド内処理のワンライン化
メソッドの実装を一つの式文、あるいはreturn文のみにする。
- 代入文はsetattrで代替可能である。5, 6
- if文は条件演算子、あるいは
(値1, 値2)[条件]
で単純に代替可能であるが、ここでは True/False がそれぞれ 1/0 であることを利用してやりたいと思う。
class MyInt:
def __init__(self, n):
setattr(self, '_n', n)
def hook(self, frame, event, arg):
setattr(self, '_n',
# 条件が真であるとき self._n + 1 と同値、偽であるときは self._n + 0 と同値である。
self._n + ((frame.f_code.co_name, event) == ('fizzbuzz', 'return'))
)
...
####ステップ2.2: 関数定義文の排除
ラムダ式を使って、各メソッドを定義し直す。
class MyInt:
__init__ = (lambda self, n:
setattr(self, '_n', n)
)
hook = (lambda self, frame, event, _:
setattr(
self, '_n',
self._n + ((frame.f_code.co_name, event) == ('fizzbuzz', 'return'))
)
)
__mod__ = (lambda self, other:
self._n % other
)
__str__ = (lambda self:
str(self._n)
)
メソッドは結局一つのクラス変数であるから、その置き換えは存外簡単である。
####ステップ2.3: クラス定義文の排除
type関数を使えばクラスオブジェクトを式内で生成することができる。
クラスの属性は辞書にまとめて渡してやる。
MyInt = type('MyInt', (object, ), {
'__init__': (lambda self, n:
setattr(self, '_n', n)
),
'hook': (lambda self, frame, event, _:
setattr(
self, '_n',
self._n + ((frame.f_code.co_name, event) == ('fizzbuzz', 'return'))
)
),
'__mod__': (lambda self, other:
self._n % other
),
'__str__': (lambda self:
str(self._n)
),
})
この時点でクラス定義はワンライナーになっている。
MyInt = type('MyInt', (object, ), {'__init__': (lambda self, n: setattr(self, '_n', n)), 'hook': (lambda self, frame, event, _: setattr(self, '_n', self._n + ((frame.f_code.co_name, event) == ('fizzbuzz', 'return')))), '__mod__': (lambda self, other: self._n % other), '__str__': (lambda self: str(self._n)),})
###ステップ3: sys.setprofile(~)のワンライン化
import文の代わりに __import__
関数を使い、次のように書くことができる。
__import__('sys').setprofile(MyInt(1).hook)
実はこのコードは不完全である。
せっかく作ったMyIntインスタンスが取りこぼされ、回収できなくなっているのだ7。
次のようにラムダ式を上手く利用するのが常道である。
myInt = (lambda mi:
# タプルを作り、その最後の要素を返す。
(__import__('sys').setprofile(mi.hook), mi)[-1]
# オブジェクトを作るのは一度だけ。
)(MyInt(1))
###ステップ4: 全体のワンライン化
MyIntの部分に先のtypeオブジェクトを直接埋め込んでやるだけだ。
myInt = (lambda mi:
(__import__('sys').setprofile(mi.hook), mi)[-1]
)(type('MyInt', (object, ), {'__init__': (lambda self, n: setattr(self, '_n', n)), 'hook': (lambda self, frame, event, _: setattr(self, '_n', self._n + ((frame.f_code.co_name, event) == ('fizzbuzz', 'return')))), '__mod__': (lambda self, other: self._n % other), '__str__': (lambda self: str(self._n)),})(1))
###成果物β: 余計な行が増えない実装
引数nのデフォルト値myIntに先のオブジェクトを利用し、前掲の最終成果物を得る。
def fizzbuzz(n=(lambda mi: (__import__('sys').setprofile(mi.hook), mi)[-1])(type('MyInt', (object, ), {'__init__': (lambda self, n: setattr(self, '_n', n)), 'hook': (lambda self, frame, event, _: setattr(self, '_n', self._n + ((frame.f_code.co_name, event) == ('fizzbuzz', 'return')))), '__mod__': (lambda self, other: self._n % other), '__str__': (lambda self: str(self._n)),})(1))):
if n % 15 == 0:
return 'fizzbuzz'
if n % 3 == 0:
return 'fizz'
if n % 5 == 0:
return 'buzz'
return str(n)
for _ in range(20):
print(fizzbuzz())
実行結果 ⇒ Wandbox
##まとめ
以上のように、実引数を省いてコードを書くことができた。
コードを書く際、分かりきった引数を省略できるなら... どんな手段でも実現したいと思わないだろうか?
私は思わない。
##拡張のヒント
今後改良するにあたって、拡張できそうな機能を列挙する。
- カウントのリセット
- 引数を明示的に渡した場合、デフォルト値をそれに追従させる
- 関数名を変更しても、デフォルト値に影響が出ないようにする
print(fizzbuzz()) # => 1
print(fizzbuzz()) # => 2
print(fizzbuzz()) # => fizz
fizzbuzz.reset(10)
print(fizzbuzz()) # => buzz
print(fizzbuzz()) # => 11
print(fizzbuzz()) # => 1
print(fizzbuzz()) # => 2
print(fizzbuzz()) # => fizz
print(fizzbuzz(10)) # => buzz
print(fizzbuzz()) # => 11
拡張3については現段階で仮実装が済んでいる。
1, 2についても、関心があるのでそのうち書くと思う。記事にするかどうかは分からない。
-
私は思わない。 ↩
-
ただし、Pythonのデフォルト引数の取り扱いについての理解が必要である。デフォルト値は関数定義時に一度だけ評価される。まあデフォルト式でもクラス変数を利用すれば同じようなコードは組める。 ↩
-
MyIntオブジェクトを剰余算の右オペランドに置きたい場合は、
__rmod__
メソッドの実装も必要である。 ↩ -
芸風が狭いとか言ってはいけない。図星過ぎて傷付く。 ↩
-
Python3.8で代入演算子(通称セイウチ演算子)が導入されたが、使用できる文脈には制限があるようだ。 ↩
-
できる限り使わず、ステートレスな処理を書きたいものだが。setattrを安易に使ったら負けという感覚は私だけのものだろうか? ↩
-
というのは、厳密には嘘である。しかし
myInt = sys.getprofile().__self__
で回収するのは何とも煩わしい。 ↩