技術

デコレータ関連で分かった事

昨日からデコレータについて色々テストコードを書きながら調べてて分かった事です。デコレータに限らない部分も多々。

昨日書いたデコレータの話で、デコレータ内で状態を保持するために、「f.call_count = -1」みたいに書いたけど、本当は普通に考えて以下のように書きたかった。

def negligence(n):
	def _(f):
		call_count = -1
		def __(*args, **kwargs):
			call_count += 1
			if call_count % n == 0:
				call_count = 0
				return f(*args, **kwargs)
		return __
	return _

でもコレだと関数の実行時”__(*args, **kwargs)”の実行時にUnboundLocalError例外が発生します。
これは”__(*args, **kwargs)”の最初の”call_count += 1″でブロック外の変数の参照と代入が行われるからです。
ブロック内で代入だけ(ローカル変数の作成)や参照だけ(上位ブロックの変数の参照)や代入後参照(ローカル変数の利用)なら例外は発生しません。
参照後代入で例外が発生します。(詳細

UnboundLocalErrorを発生させないようにするには例えば以下のように書きます。

def negligence(n):
	def _(f):
		status = dict(call_count = -1)
		def __(*args, **kwargs):
			status['call_count'] += 1
			if status['call_count'] % n == 0:
				status['call_count'] = 0
				return f(*args, **kwargs)
		return __
	return _

この例では”__(*args, **kwargs)”の一つ上のブロックで定義されているstatusという変数に直接代入してないためうまく動きます。

話はちょっと変わりますが、昨日書いた記事でcallableオブジェクトを返すデコレータのパターンを失敗作として紹介しましたが、その逆も可能なんではないかと思い試してみました。
つまり、「callableオブジェクトを返す”関数で実装された”デコレータ」の逆の「関数を返す”クラスで実装された”デコレータ」です。

class negligence(object):
	def __init__(self, n):
		self.__n = n
		self.__call_count = -1
	def __call__(self, f):
		self.__f = f
		def _(*args, **kwargs):
			self.__call_count += 1
			if self.__call_count % self.__n == 0:
				self.__call_count = 0
				return self.__f(*args, **kwargs)

これも正常に動作しました。
何も目新しいパターンではないと思いますが、状態を保持する必要があるデコレータではこちらを使った方が混乱は少ないかも。

あと、昨日書いた失敗作(callableオブジェクトを返すパターンのデコレータ)がメソッドに適用するとうまく動作しない理由をもうちょっと。
class文のブロック直下の関数は通常インスタンス化されたタイミングでbound method化するけど、callableオブジェクトはインスタンス化されてもcallableオブジェクトのままなため、bound methodでは当然受け取れるはずの第一引数のselfが受け取れないという事になるみたい。

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください