蛇の悪魔を寄せ付けないための不変の活用
概要
本章では不変な変数は値書き換えの追跡の必要性を減らし、悪魔を寄せ付けないことを良しとしている。これはPythonにおいて最も重要な課題と言っても過言ではない。というのも、Pythonにおける値の書き換えは悪魔を寄せ付ける因子であるためと考えられるためである。
4.1 再代入
前章でもそうであったように、退魔の書では変数の再代入をすべきではないとしている。値の追跡が困難になったり、計算の内容の理解が分からなくなるためである。それを防ぐ方法として、final修飾子によって値の変更を防ぐという方法を紹介している。これも前章と同じだ。
新しいインスタンスの作成による一時変数の説明
リーダブルコードでは関連するコード改善案として、8章で式の分割の方法について解説がある。退魔の書での悪魔を呼ぶコードでは、一時変数tmpを何度も書き換えていた。一方でリーダブルコード8章では、逐次的に計算を小分けすることを推奨しつつも、値を「一時変数」ではなく「説明変数」や「要約変数」という新しい変数に代入することを推奨している。すなわち、コードを読むときにどのような処理をしているのかを分かりやすい別名の変数に代入していくべきだ。
これに関し、リーダブルコード2章2節ではtmpなどの汎用的な変数名を避けるようにしている。tmpが許されるのは、tmpの登場が一度だったり寿命が数行だったりと、命名しなくても役割が明瞭なときに限られるべきである。それ以外の時はたとえ一時変数でも、2章1節で説明されるようにカラフルな変数名を用いることで、処理の内容を明確にすべきである。
引数の不変
Pythonでは次の二点において、関数に渡したオブジェクトのインスタンスが変更されない保証がない。
① Pythonのプリミティブ型以外のあらゆるオブジェクトは参照渡しで渡されるためである。これはcopy.deepcopy()で対処できると言えば対処できるが、冗長になる。② 引数に変更を加えない保証をする術がない。C++でいうconst修飾子などが存在しない。Finalも関数引数部分には使えない。また、Final型の変数を代入しても、関数内での破壊的変更を防げない。 デコレータを使うことで、不変の明示とまではいかないが、引数をすべてdeepcopyして参照元への影響をかなり減らす方法をTwitterのフォロワー(@longpupupu氏)に教えてもらったので残す(スペルなど細かい箇所だけ編集させてもらった)。
とてもおファックである。引数は基本的にメンバメソッドでインスタンスを再生成してそれを別変数に代入して使うか、deepcopy()するなど、もとのオブジェクトへのアクセスを最小限にすべきだろう。
以下の関数用デコレータを実装することで、その関数内のすべての引数はすべてdeepcopyされる。
from copy import deepcopy
def immutator(func):
"""
This decorator passes deepcopied aruments list.
It is guaranteeed that the all original arguments will not be overwritten.
"""
def wrapper(*args, **kwargs):
args_copy = tuple(deepcopy(arg) for arg in args)
kwargs_copy = {
key: deepcopy(value) for key, value in kwargs.items()
}
return func(*args_copy, **kwargs_copy)
return wrapper
なおこれを基に、selfだけ可変にするdecolatorも実装できる。これは、自身だけは値の変更をしたいが他には影響を与えたくないようなクラスメソッドに有用。(ただし、この実装は完璧ではなく、第一引数のクラスメソッドと同名の外部関数がこのデコレータを使用することを許してしまう。まぁ悪用が大変にはなっているのだが……。これは今後の課題である)
def self_mutator(func):
"""
This decorator passes deepcopied aruments list other than itself.
It is guaranteeed that the all original arguments will not be overwritten.
"""
"""
TO DO: 可能であれば、第一引数がselfかどうかのチェックをしたい。
現在の問題点として、第一引数のオブジェクトが有するメソッドど同名のグローバル関数にこのデコレータを付けても問題なく動いてしまう。
"""
def wrapper(*args, **kwargs):
# Checking wheatehr the first arg is self
# Check the length of the arguments and get the firstr argument
if len(args)==1:
first_arg = args
elif len(args)==0:
raise InvalidDecorator("This decorator is used in class method with a self argument.")
else:
first_arg = args[0]
# extract the name of the method which is calling this decorator
func_name = func.__name__
# try to get the id of class method
try:
first_arg.__getattribute__(func_name)
except AttributeError:
# Meaning that the object does not have the method whose name is tha same as the method calling this decorator.
raise InvalidDecorator("This decorator must be used in class method.")
if len(args)==1:
args_copy = args
else:
args_copy = tuple([args[0]]) + \
tuple(deepcopy(arg) for arg in args[1:])
kwargs_copy = {
key: deepcopy(value) for key, value in kwargs.items()
}
return func(*args_copy, **kwargs_copy)
return wrapper
4.2 可変がもたらす意図せぬ影響
内容自体は実施例だが、解説されている悪魔については本当に留意すべき内容である。Pythonは大体の代入操作が参照渡しであるため、一か所変更を加えるとすべて書き換わる「悪魔」がよく発生する。何度も経験している。オブジェクトの代入はdeepcopyで行うことを徹底してもいいかもしれない。どうせガーベッジコレクタ動くわけだし。処理速度気にするならPython使うべきではないし。
4.3 不変と可変の取り扱い方針
退魔の本では、変数は基本的に不変であるべきであり、可変にするときは専用のメソッドを作り、変更の追跡を容易にすべきとしている。どうしても可変にしたい場合、オブジェクトは① 正しく状態変更するメソッドを作る、② コードがいのやり取りは局所化する、としている。前者のメソッドはミューテーター呼称している。
ロブストPythonではミューテーターメソッドを実装するにあたり、不変式を満たすよう注意喚起している。__init__特殊メソッドで不変式を満たすように初期化しても、ミューテーターでそれが維持されなければならないとしている。
まとめ
Final型を適切に使い、変数の書き換えをしないようにする。
一時変数の名前もカラフルにして読みやすくする。
deepcopyを使い、関数内での値書き換えなど、参照渡しに起因する値書き換えを防ぐ。
余談
Rustのletが羨ましい。