フック(hook)について
⇒pythonの組み込みAPIは、引数に関数を受け取ることができ、「ふるまい」を定義することができる。これをフックという。pythonの関数はファーストクラスオブジェクトであり、変数と同じように関数の引数として渡すことも可能である。
具体例を出すとsortメソッドのkey関数があげられる。
human_list = ['taro', 'jiro', 'saburo']
# 名前のスペル数によるソート
human_list.sort(key=len)
print(human_list)
>>>['taro', 'jiro', 'saburo']
pythonにおけるフックの多くは定義された引数と戻り値を持っていて、状態を持たない関数です。他の言語ではフックというと抽象クラスで定義されることもある。
副作用とは?
defaultdictを使った例を考えてみる。
以下の例は、もともと辞書のkeyにない値を追加しようとすると'no key'が出力される。
from collections import defaultdict
def no_key():
print('No key')
return 0
program_lang = {'python': 1, 'SQL': 1}
add_lang = [('python', 1), ('C', 2), ('Rust', 4)]
result = defaultdict(no_key, program_lang)
print('追加前:', dict(result))
for key, amount in add_lang:
result[key] += amount
print('追加後:', dict(result))
>>>
追加前: {'python': 1, 'SQL': 1}
No key
No key
追加後: {'python': 2, 'SQL': 1, 'C': 2, 'Rust': 4}
このように、no_key関数を定義することで、副作用を持たない部分とと副作用を持つ部分を分離することができる。
副作用(純粋関数)とは
…関数やメッソドを実行した際に、オブジェクトの属性を変化させることを指す。
例えば、関数内で今現在の日付を生成して処理する関数は、実行した日付によって結果が変化するので、「副作用を持つ関数」とすることができ、
一方、引数に日付を受け取って処理する関数は、関数の中で日付が変化することがなく、同じ日付を受け取れば、同じ値を返すので「副作用を持たない関数」といえる。
副作用を持つ部分と持たない部分を分離することでAPI構築やテストがしやすくなる。
クロージャを使ってみる
次に、新しくkeyを生成して値を追加した回数をカウントしてみる。
from collections import defaultdict
def add_counter(program_lang, add_lang):
added_count = 0
def add_count():
nonlocal added_count
added_count += 1
return 0
result = defaultdict(add_count, program_lang)
for key, amount in add_lang:
result[key] += amount
return result, added_count
program_lang = {'python': 1, 'SQL': 1}
add_lang = [('python', 1), ('C', 2), ('Rust', 4)]
result, count = add_counter(program_lang, add_lang)
assert count == 2
>>>
実行結果としてassertエラーが発生しなかったので、期待通りの結果が出た。
このように、副作用を持つ関数add_counterをクロージャで分けて記述することもできる。クロージャで副作用を分離しているので、後々の機能追加が楽になると考えられる。
しかし、この記述方法は、読みにくいことが難点として挙げられる。
クラスを定義しよう!
先ほどのコードもう少し見やすいものにするため、クラスを定義してみる。
from collections import defaultdict
class Count_add:
def __init__(self):
self.added = 0
def missing(self):
self.added += 1
return 0
program_lang = {'python': 1, 'SQL': 1}
add_lang = [('python', 1), ('C', 2), ('Rust', 4)]
counter = Count_add()
result = defaultdict(counter.missing, program_lang)
for key, amount in add_lang:
result[key] += amount
assert counter.added == 2
>>>
こうしてみると、exam3.pyよりも見やすくなった。
しかし、このコードにも問題点がある。それは、初めてこのexam4.pyのコードを見た際Count_addがどのように使われるのか明確ではないという点だ。
__call__メソッドを使ってみよう!
from collections import defaultdict
class Count_add:
def __init__(self):
self.added = 0
def __call__(self):
self.added += 1
return 0
program_lang = {'python': 1, 'SQL': 1}
add_lang = [('python', 1), ('C', 2), ('Rust', 4)]
counter = Count_add()
result = defaultdict(counter, program_lang)
for key, amount in add_lang:
result[key] += amount
assert counter.added == 2
上記のコードのように、missing関数を特殊メッソド__call__に変換している。
__call__メッソドとは
…インスタンスそのものを関数のように呼び出す機能を実装するための特殊メソッド。関数のようにオブジェクトを呼び出すことができるようになる。
exam5.pyとexam4.pyは挙動としてほとんど同じであるが、特殊メッソド__call__を使うことで、このクラスは関数引数のように呼び出されて使われる(callable)オブジェクトであると明示的に伝えることが可能になったといえる。
ポイント
・副作用を持つ部分と副作用を持たない部分は分離して書こう。
・関数引数のように使うクラスは__call__メソッドを定義して役割を明示的に伝えよう。
参考
・Effective Python
・副作用ってなに?
・【プログラミング】「副作用」について