はじめに
前回の続きとして、今回はまずdescriptorの応用例を一個紹介します。
あくまでも個人の見解なので、間違いがあったらご容赦ください。
descriptorの応用例の前に
映画の情報を管理するコードを書くとしましょう。
class Movie:
def __init__(self, title, rating, runtime, budget, gross):
self.title = title
self.rating = rating
self.runtime = runtime
self.budget = budget
self.gross = gross
def profit(self):
return self.gross - self.budget
このコードは問題があって、例えば、映画の点数がマイナスになってはいけません。しかし、このコードはそういう例外処理をしていません。
最初に思い浮かべる修正方法は次のようになります。
class Movie:
def __init__(self, title, rating, runtime, budget, gross):
self.title = title
if rating < 0:
raise ValueError(f"Negative value not allowed: {rating}")
self.rating = rating
self.runtime = runtime
self.budget = budget
self.gross = gross
def profit(self):
return self.gross - self.budget
これは、インスタンス化するとき、値がマイナスであるかどうかをチェックします。もしマイナスでれば、例外をおこします。
しかし、これも不十分です、なぜかというと、初期化した後の修正は自由です。
例えば、次のようにやると
m = Movie("SWars", 1, 2, 3, 4)
m.rating = -10
print(m.rating) # -10
となります。
これは好ましくないですね。このような問題に対しては、ratingという属性に対してget/setメソッドを書くことで解決できます。しかし、それはPythonicなやり方ではありません。
ある程度Pythonについて詳しい方であれば、ここで@propertyを使えばいいじゃんと思い浮かべるでしょう。
そうですね、次のように、@propertyを使うと非常にきれいなコードが書けます。
class Movie:
def __init__(self, title, rating, runtime, budget, gross):
self.title = title
self.rating = rating
self.runtime = runtime
self.budget = budget
self.gross = gross
def profit(self):
return self.gross - self.budget
@property
def rating(self):
"""
動的に._ratingを追加する
"""
return self._rating
@rating.setter
def rating(self, value):
if value < 0:
raise ValueError(f"Negative value not allowed: {value}")
self._rating = value
こうすることで、インスタンス化するときも、後で値を修正しようと思うときも、値のチェックをしてくれるので、非常に便利です。
m = Movie("SWars", 1, 2, 3, 4)
try:
m.rating = -1
except ValueError as e:
print(e)
# Negative value not allowed: -1
try:
Movie("SWars", -10, 2, 3, 4)
except ValueError as e:
print(e)
# Negative value not allowed: -10
しかし、この@propertyも一個だけよくないところがあります。それは、違う属性に対して、重複利用できないという点です。
ここの例で言うと、rating, budget, runtime, grossのそれぞれに@propertyを定義する必要があります。
実際に書くと、以下のようになるので、非常にみにくいです。
class Movie:
def __init__(self, title, rating, runtime, budget, gross):
self.title = title
self.rating = rating
self.runtime = runtime
self.budget = budget
self.gross = gross
def profit(self):
return self.gross - self.budget
@property
def rating(self):
"""
動的に._ratingを追加する
"""
return self._rating
@rating.setter
def rating(self, value):
if value < 0:
raise ValueError(f"Negative value {value} not allowed for rating ")
self._rating = value
@property
def runtime(self):
"""
動的に._runtimeを追加する
"""
return self._runtime
@runtime.setter
def runtime(self, value):
if value < 0:
raise ValueError(f"Negative value {value} not allowed for runtime ")
self._runtime = value
@property
def gross(self):
"""
動的に._grossを追加する
"""
return self._gross
@gross.setter
def gross(self, value):
if value < 0:
raise ValueError(f"Negative value {value} not allowed for gross ")
self._gross = value
@property
def budget(self):
"""
動的に._budgetを追加する
"""
return self._budget
@budget.setter
def budget(self, value):
if value < 0:
raise ValueError(f"Negative value {value} not allowed for budget ")
self._budget = value
動作確認してみると、確かに予想通りに動きます。
m = Movie("SWars", 1, 2, 3, 4)
try:
m.budget= -1
except ValueError as e:
print(e)
# Negative value not allowed: -1
try:
Movie("SWars", 1, -2, 3, 4)
except ValueError as e:
print(e)
# Negative value -2 not allowed for runtime
このようにして、Movie()の外部からは使いやすいですが、実は内部は重複しているロジックが多くて、とてもきれいなコードと言えません。
@propertyの拡張版としてカスタムdescriptorを使おう
@propertyの拡張というのは、上記のような値チェックのロジックを満たすようなクラスを作ることです。
今回は以下のようなデータディスクリプタ/data descriptor(前回の記事を参照)を使います。
from weakref import WeakKeyDictionary
class NonNegative:
def __init__(self, name=None):
self.name = name
self.data = WeakKeyDictionary()
def __get__(self, instance, owner):
if instance is None:
return self
return self.data.get(instance, 0)
def __set__(self, instance, value):
if value < 0:
raise ValueError(f"Negative value {value} not allowed for {self.name}")
self.data[instance] = value
class Movie:
rating = NonNegative("rating")
runtime = NonNegative("runtime")
budget = NonNegative("budget")
gross = NonNegative("gross")
def __init__(self, title, rating, runtime, budget, gross):
self.title = title
self.rating = rating
self.runtime = runtime
self.budget = budget
self.gross = gross
def profit(self):
return self.gross - self.budget
同じく動作確認をすると、無事動きました。
m = Movie("SWars", 1, 2, 3, 4)
try:
m.gross = -10
except ValueError as e:
print(e)
# Negative value -10 not allowed for gross
try:
Movie("SWars", 1, 2, -3, 4)
except ValueError as e:
print(e)
# Negative value -3 not allowed for budget
おわりに
今回はとりあえずdesicriptorのカスタム応用例を一個説明しました。実際、djangoを触ったことのある方はこういう書き方を見たことはあるでしょう。descriptorはPythonにおいては非常に重要な存在です。それをぜひマスターしていただればと思います。