Table of Contents
- 1. Chapter 5: クラスとインタフェース
- 1.1. Item 37: ビルトインタイプを何重にもネストさせるより(複数の)クラスを作れ
- 1.2. Item 38: シンプルなインタフェースにはクラスでなく関数を受け入れよ
- 1.3. Item 39: オブジェクトをgenericに作るためには@classmethodポリモーフィズムを使え
- 1.4. Item 40: superを使って親クラスを初期化せよ
- 1.5. Item 41: Mix-inクラスを使って機能をcomposeすることを考えよ
- 1.6. Item 42: プライベートなアトリビュートよりもパブリックな方が良い
- 1.7. Item 43: カスタムコンテナタイプを作るにはcollections.abcを継承せよ
- 2. Chapter 6: メタクラスとアトリビュート
- 2.1. Item 44: SetterやGetterメソッドよりもアトリビュートを普通に使え
- 2.2. Item 45: アトリビュートのリファクタリングよりも @property を使え
- 2.3. Item 46: 再利用可能な@propertyメソッドとしてデスクリプターを使え
- 2.4. Item 47: Lazyアトリビュートのために__getattr__, __getattribute__や__setattribute__を使え
- 2.5. Item 48: __init_subclass__を使ってサブクラスをvalidateせよ
- 2.6. Item 49: __init_subclass__を使ってクラスを登録せよ
- 2.7. Item 50: __set_name__を使ってクラスアトリビュートをannotateせよ
- 2.8. Item 51: クラス拡張を組み合わせるために、メタクラスよりもクラスデコレーターを使え
1 Chapter 5: クラスとインタフェース
1.1 Item 37: ビルトインタイプを何重にもネストさせるより(複数の)クラスを作れ
コードを拡張して以下のような複雑なコードになったら、複数のクラスを使うようにリファクタリングした方が良いです。
- ディクショナリを含むディクショナリ
- 3以上の個数を持つタプルを値として持つディクショナリ
- 複雑にネストした他のビルトインタイプ
長いタプルはnamedtupleを使う手もありますが、namedtupleには制限があります。
- デフォルト値が指定できない
- 数値のインデックスやiterationでアトリビュート値がアクセスできる
1.2 Item 38: シンプルなインタフェースにはクラスでなく関数を受け入れよ
current = {'green': 12, 'blue': 3}
このディクショナリに、
increments = [('red', 5), ('blue', 17), ('orange', 9)]
このタプルのリストを加えて、
result = {'green': 12, 'blue': 20, 'red': 5, 'orange': 9}
を得たいとします。
次の関数は、ステートフルなclosureを使って追加した(=ミスした)色の数をカウントしつつ、上記のことを行います。
def increment_with_report(current, increments): added_count = 0 def missing(): '''カウントを+1して0を返す''' nonlocal added_count # ステートフルなclosure added_count += 1 return 0 result = defaultdict(missing, current) # closureを仕込む for key, amount in increments: result[key] += amount return result, added_count
以前調べたとき、closuresは以下の定義でした。
- 関数と、そのインナー関数がある
- インナー関数は、外側の関数で定義された変数を参照する
- 外側の関数は、インナー関数を戻り値として返す
上記の例では最後の要件を満たしていませんが、 defaultdict()
にインナー関数 missing()
を渡すことで、closureとして機能していることになるようです。
また、上の例ではclosureをステートを保持する目的で使っています。これもclosureのユースケースの一つなのですね。
しかし、ステートフルなclosureは読みづらいために避けるべきと書いてあります。その代わりにクラスを用意し、更に __call__()
を実装することでクラスを関数のように使うことを推奨しています。
class BetterCountMissing: def __init__(self): self.added = 0 def __call__(self): self.added += 1 return 0 counter = BetterCountMissig() result = defaultdict(counter, current) # counterを関数として渡す for key, amount in increments: result[key] += amount
1.3 Item 39: オブジェクトをgenericに作るためには@classmethodポリモーフィズムを使え
えー、MapReduceって何でしたっけ。処理をたくさんのworkersにばらまいて、結果を統合していくやつでしたよね。この本は前提知識が高度すぎて、大変読みづらいです。ぼちぼち見ていきましょう。
これはインタフェース的な、継承されることを前提としたクラスですね。
class InputData: def read(self): raise NotImplementedError
パスを渡されてリードする処理を実装した子クラスです。
class PathIputData(InputData): def __init__(self, path): super().__init__() self.path = path def read(self): with open(self.path) as f: return f.read()
これもインタフェース的workerクラス。
class Worker: def __init__(self, input_data): self.input_data = input_data self.result = None def map(self): raise NotImplementedError def reduce(self, other): raise NotImplementedError
行数をカウントする処理を入れ込んだworker子クラス。
class LineCountWorker(Worker): def map(self): data = self.input_data.read() self.result = data.count('\n') def reduce(self, other): self.result += other.result
次は、ディレクトリをリストして、その中にあるファイルをリードできる PathInputData
オブジェクトをyieldするジェネレーター関数を定義します。
import os def generate_inputs(data_dir): for name in os.listdir(data_dir): yield PathInputData(os.path.join(data_dir, name))
そしてworkersを複数用意するところ。ワーカーとして LineCountWorker
を渡しています。
# 引数input_listはPathInputDataをyieldするジェネレーター def create_workers(input_list): workers = [] for input_data in input_list: workers.append(LineCountWorker(input_data)) return workers
次は用意したworkersを実行するところ。Pythonでスレッド使うやり方習いましたっけ? まあ読めばわかるのでいいや。 thread.join()
は作ったスレッドの実行が終わるまで待つ(ブロックされる)ということ。
from threading import Thread def execute(workers): threads = [Thread(target=w.map) for w in workers] for thread in threads: thread.start() for thread in threads: thread.join() first, *rest = workers for worker in rest: first.reduce(worker) return first.result
おー、なんか格好いいです。これがMapReduceの実装なのですね。先頭の worker に全ての結果を集約しています。
最後にこれらをまとめます。
def mapreduce(data_dir): inputs = generate_inputs(data_dir) # ジェネレーターを返す workers = create_workers(inputs) return execute(workers)
generate_inputs
は PathInputData
を一つずつ返す iterator を作り、 create_workers
でこれらを割り振ったワーカーのリストを用意して、 executee
で mapreduce します。うーん、格好いい!
ここまで理解したところで、やっと本節のテーマに入ります。導入が長すぎる。。。 まずは、上記のやり方に対してダメ出しです。問題は、パーツの結びつきがお互いに強すぎて、一つ変更するとみんな変更しなくてはいけないこと。独立性が低すぎるということです。
これに対する解はクラスメソッドのポリモーフィズムを使うことだと書いてあります。正直言って、意味がわかりません。読み進めましょう。
まずはgenericな InputData
クラスを定義します。追加したのはconfigパラメーターから設定を読み込むクラスメソッド(のインタフェース)。意味深です。
class GenericInputData: def read(self): raise NotImplementedError @classmethod def generate_inputs(cls, config): raise NotImplementedError
PathInputData
クラスでこれを継承します。
class PathInputData(GenericInputData): def __init__(self, path): super().__init__() self.path = path def read(self): with open(self.path) as f: return f.read() @classmethod def generate_input(cls, config): data_dir = config['data_dir'] for name in os.listdir(data_dir) yield cls(os.path.join(data_dir, name))
別関数だった generate_input
を PathInputData
にクラスメソッドとして組み込んでいます。
yield
の行は、 PathInputData
のインスタンスを作って、その引数としてconfigのディレクトリにある各ファイルのパスを渡しています。 @classmethod
を付けてクラスメソッドを用意することで、 __init__()
を使わない別のやり方でコンストラクターを定義することができる、ということでした。
また、genericなクラスではなく具体的な子クラスでそのクラスメソッドを実装することで、子クラスに合わせたフレキシブルな初期化ロジックを入れ込むことができます。
そしてgenericなworkerクラスです。
class GenericWorker: def __init__(self, input_data) self.input_data = input_data self.result = None def map(self): raise NotImplementedError def reduce(self, other): raise NotImplementedError @classmethod def create_workers(cls, input_class, config): workers = [] for input_data in input_class.generate_inputs(config): workers.append(cls(input_data)) return workers
こちらでも別関数だった create_workers
をクラスメソッドとして組み込んでいます。今回のポイントはクラスメソッドを使ったポリモーフィズムとのことですが、 create_workers
をジェネリックなworkerクラスに組み込んだところを言っているのでしょうか。
以前は mapreuce
関数で呼び出していた generate_inputs
はこの中で呼び出されるようになります。つまり、 create_workers
メソッドにおいて PathInputData
インスタンスの生成と、それを組み込んだ GenericWorker
インスタンスの生成を行っています。
LineCountWorker
は GenericWorker
を継承します。他は変更なし。
class LineCountWorker(GenericWorker): def map(self): ... def reduce(self, other): ...
mapreduce
関数は引数としてconfigを取ります。 generate_inputs
の呼び出しは create_workers
に含まれたのでここからは無くなっています。シンプルになりました。
def mapreduce(worker_class, input_class, config): workers = worker_class.create_workers(input_class, config) return execute(workers)
そして、全ての基点がここ。
config = {'data_dir': tmpdir} result = mapreduce(LineCountWorker, PathInputData, config) print(f'There are {result} lines') >>> There are 4360 lines
ところで、独立性が低すぎる件はこれで解決したのでしょうか。クラスのインスタンス生成をクラスメソッドとして組み込んだためスッキリしたとは思いますが、どこが解決されているのか今ひとつわかりません。。。
今回の節はかなり難しかったです。意味を理解するのに数時間以上悩みました。
追記です。このようなやり方でオブジェクトを作る方法は、static factory patternというようです。
1.4 Item 40: superを使って親クラスを初期化せよ
スーパークラスのコンストラクタを呼ぶときは、親クラスを名前で指定するのでなく、 super.__init__()
を指定するように、とのこと。普通にやっていますよね。この節はこれで言いたいことはおしまいですが、更に補足します。
二つの親クラスを継承した子クラスがあるとして、その二つの親クラスのどこかの先祖が同じクラスであるような継承をダイヤモンド継承と呼ぶらしいです。ダイヤモンド継承ではその同じ祖先のコンストラクタを複数回実行してしまうことで、副作用が出ることがあります。
例えば共有する先祖クラスにおいて、親クラスで操作する変数の初期化をしている場合は、一つの親クラスがその変数の初期化及び値を操作した後に、別の親クラスのコンストラクタでもう一度初期化してしまう場合がありえます。
しかし、 super
を使うと一つの祖先クラスのコンストラクターを1回しか実行しないことを保証してくれます。このあたりのルールは method resolution order (MRO)という仕様にて定義されています。
super
によるMROを使ったコンストラクターは、先祖クラスのコンストラクターを実行する順序が、ぱっと見の感覚と異なる場合があることに注意。
1.5 Item 41: Mix-inクラスを使って機能をcomposeすることを考えよ
Mixinsは知りませんでした。ここにわかりやすい定義があります。
A mixin is a class that defines and implements a single, well-defined feature. Subclasses that inherit from the mixin inherit this feature—and nothing else.
継承させることを目的に、ある一つの機能だけを定義、実装したクラスで、 xxxMixin
のような名前になるそうです。クラスを定義するときに、メインとなる親クラス一つと、複数のmixinクラスを継承するような使い方をします。
class SomeClass(Parent, AaaMixin, BbbMixin, CccMixin):
のような感じで。これは便利そうです。Javaのインタフェースがこれに相当するのでしたっけ。
この中に出てくるバイナリーツリーのコードの意味がピンときません。
class BinaryTree(ToDictMixin): def __init__(self, value, left=None, right=None): self.value = value self.left = left self.right = right
ToDictMinxin
はディクショナリ(リストや相当機能を持つクラスを含む)をたどってPythonのディクショナリに変換する to_dict
メソッドを実装したmixinです。
tree = BinaryTree(10, left=BinaryTree(7, right=BinaryTree(9)), right=BinaryTree(13, left=BinaryTree(11))) print(tree.to_dict()) >>> {'value': 10, 'left': {'value': 7, 'left': None, 'right: {'value': 9, 'left': None, 'right': None}}, 'right': {'value': 13, 'left': {'value': 11, 'left': None, 'right': None}, 'right': None}}
ここにコードスニップを書いて眺めていたら、ようやく理解できました。 Kindleデバイスでこの本を読むのは無理があります。。。
また、
import json class JsonMixin: @classmethod def from_json(cls, data): kwargs = json.loads(data) return cls(**kwargs) def to_json(self): return json.dumps(self.to_dict())
このクラスメソッド from_json()
がわかりづらかったです。
data
はjsonエンコードしてシリアライズされたデータで、これをディクショナリに変換(デコード)して kwargs
に入れます。そしてこれを引数に当該クラスのオブジェクトを作成して戻します。 JsonMixin
は、当該クラスをjsonにエンコードする機能と、逆にデコードしてクラスに戻す機能を付与する mixin でした。
これはスタティックファクトリーパターン、、、とは違うのかな。
1.6 Item 42: プライベートなアトリビュートよりもパブリックな方が良い
タイトルの通りです。
- プライベートアトリビュートは頑張れば子クラスからアクセスできてしまう
- プライベートアトリビュートを親クラスから更にその親クラスに移した場合など、子クラスで無理にアクセスしようとしていると名称が変わってアクセスできなくなる
- プライベートの仕組みを使ってアクセス制御するよりも、注意事項をドキュメントに書いた方がよい
- 唯一プライベートアトリビュートを使って良いのは、名前のコンフリクトを避けたい場合
1.7 Item 43: カスタムコンテナタイプを作るにはcollections.abcを継承せよ
これもタイトルの通りです。
list
や dict
等のPythonで定義されているコンテナタイプを継承してクラスが作れるとは知りませんでした。
list
等に用意されている便利なメソッドは多く、一から作るのは大変なので、 collections.abc
を継承するとよい、ということでした。
2 Chapter 6: メタクラスとアトリビュート
メタクラスって何ですか? StackOverflowのwhat-are-metaclass-in-pythonに、ものすごく詳しくてわかりやすい解説がありました。
2.0.1 メタクラスの定義
メタクラスはクラスを作るクラスです。クラスはそのクラスの実体(オブジェクト)がどのように振る舞うかを規定しますが、メタクラスはクラスがどう振る舞うかを規定します。クラスはメタクラスのインスタンスです。
そういう意味で、クラスはオブジェクトです。オブジェクトは以下の特徴を持ちます。
- それを変数にアサインできる
- コピーできる
- それにアトリビュートを追加できる
- 関数のパラメーターとして渡すことが出来る
2.0.2 typeが持つ別の顔
ご存じのように、 type
には type(1)
で <type 'int'>
を、 type("1")
で <type 'str'>
のように返す機能がありますが、その他に全く別のアビリティーを持ちます。それは、クラスを動的に作る機能です。
この機能の使い方は以下です。
type(name, bases, attrs) # name: クラスの名前 # bases: ペアレントクラスのタプル(空でもよい) # attrs: アトリビュートの名前と値を持つディクショナリ
例えば、
class Foo(): bar = True
は次のように書けます。
Foo = type('Foo', (), {'bar':True})
目からウロコです。なんだかすごくないですか!?
メソッドを定義することも出来ます。
def echo_bar(self): print(self.bar)
Fooクラスを継承したFooChildを作ります。
FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
これが、Pythonがキーワード class
を見つけるとメタクラス type
を使って行うことだそうです。
2.0.3 typeの正体
__class__
のアトリビュートを見ると、タイプがわかります。
>>> age = 35 >>> age.__class__ <class 'int'> >>> name = 'bob' >>> name.__class__ <class 'str'> >>> def foo(): pass ... >>> foo.__class__ <class 'function'> >>> class Bar(object): pass ... >>> b = Bar() >>> b.__class__ <class '__main__.Bar'>
おー、これはわかりやすいです。
一歩踏み込んで、 __class__
の __class__
を見てみると、、、
>>> age.__class__.__class__ <class 'type'> >>> name.__class__.__class__ <class 'type'> >>> foo.__class__.__class__ <class 'type'> >>> b.__class__.__class__ <class 'type'>
なんと、全て type
になっていました。つまり、メタクラス type
はPythonの様々なタイプオブジェクトのタイプなのでした。
ここまで踏み込んだところで、ようやく本文に入ります。
2.1 Item 44: SetterやGetterメソッドよりもアトリビュートを普通に使え
getter, setterを用意するのはpythonicでないそうです。
@property
, @<attr>.setter
デコレーターを使うと、アトリビュートの値を普通に参照、 =
で設定するときにこれらのメソッドが使われます。Introducing Pythonを見返すまで忘れていましたが。。
class Duck(): def __init__(self, input_name): self.hidden_name = input_name @property def name(self): return self.hidden_name @name.setter def name(self, input_name): self.hidden_name = input_name duck = Duck('No name') duck.name = 'Donald' print(duck.name) >>> Donald
注意点として、これらは副作用無く、素早く終わるようにすること、だそうです。
2.2 Item 45: アトリビュートのリファクタリングよりも @property を使え
@property
のアドバンストな使い方は、アトリビュートをその場で計算して返すこと。Linuxの/procと似てますね。
本題から外れてまたKindle版の悪口ですが、Click here to view code imageをクリックしてビットマップイメージでコードを見ても、まだインデントが間違っている箇所がたくさんあります。本当にひどい。返品したいくらいです。Kindle版の技術書がひどいのはある程度わかっていましたが、ここまでとは。もう安くても買いません。
えーと、leaky bucket(穴の開いたバケツ)です。最初、普通に穴の開いたバケツをイメージして読んだのですが、コードの意味がよくわかりません。検索してみると専門用語のようです。ここで言っているクオータは何でしょう。残量? 使用した量? 残量みたいですね。整理しながら読んでいきましょう。
まずはバケツを定義します。
class Bucket: def __init__(self, period): self.period_delta = timedelta(seconds=period) self.reset_time = datetime.now() self.quota = 0 # クオータを0に初期化 def __repr__(self): return f'Bucket(quota={self.quota})'
これはいいですかね。最初はクオータ0始まりです。
次は fill()
です。バケツに水を汲みます。
def fill(bucket, amount): now = datatime.now() if (now - bucket.reset_time) > bucket.period_delta: bucket.quota = 0 bucket.reset_time = now bucket.quota += amount
時間が bucket.period_delta
よりも長く経過していたら、タイマーをリセットし、クオータを amount
に設定して、まだだったら残量に amount
を追加しています。何ですかね。 fill()
を呼ばれたときだけ一気に漏れる(そしてフィルする)バケツなのでしょうか。何だかWikiで呼んだleaky bucket algorithmの定義と違うような。。。
そして、Kindle版でインデントがずれていた deduct()
です。
def deduct(bucket, amount): now = datetime.now() if (now - bucket.reset_time) > bucket.period_delta: return False # この期間にバケツはフィルされていない if bucket.quota - amount < 0: return False # 使いたい量がない bucket.quota -= amount return True
あれれ、残量によらず(?)期間にバケツがフィルされていなければ False
でリターンするのですね。フィルされずに bucket.period_delta
を過ぎていたら、既に漏れてしまっているのでdeductさせません(クオータ(残量)の値は変えないけど)という乱暴な作りなのでしょうか。
整理してみましょう。
- フィルするときに規定時間
buekct.period_delta
経っていなければ残量に追加量を加える - フィルするときに規定時間経っていれば期間のカウンターをリセットして追加量=残量とする
- 水を使おうとしたときに残量が必要量あり、規定時間経っていなければ、使わせる
- 水を使おうとしたときに規定時間経っていたら、(残量がどうあれ)使わせない
やはり、乱暴な作りでした。。。
この実装の問題は、そもそもバケツの残量がいくつから始まったのかわからないことだ、と書いてあります。そんなに大層な問題ですかね。。。
で、改善版です。
class NewBucket: def __init__(self, period): self.period_delta = timedelta(seconds=period) self.reset_time = datetime.now() self.max_quota = 0 self.quota_consumed = 0 def __repr__(self): return (f'NewBucket(max_quota={self.max_quota}, ' f'quota_consumed={self.quota_consumed})') @property def quota(self): return self.max_quota - self.quota_consumed
quota
アトリビュートを廃止して max_quota
と quota_consumed
を導入しました。そして @property
で廃止した quota
アトリビュートを動的に作って返します。今回のポイントは実はここだけですかね。
更にsetterの定義です。上で定義した fill()
と deduct()
がそのまま使えるように配慮しました、と書いてあります。
@quota.setter def quota(self, amount): delta = self.max_quota - amount # 減少量 = 最大量 - 設定値 if amount == 0: # 0を指定した場合は、新たな期間のためにリセットする self.quota_consumed = 0 self.max_quota = 0 eilf delta < 0: # 新たな期間のために残量をフィルする assert self.quota_comsumed == 0 self.max_quota = amount else: # 当該期間において、残量を使う assert self.max_quota >= self.quota_consumed self.quota_consumed += delta # 消費した量に減少量を加える
このメソッドだけ眺めても理解できないので、 fill()
や deduct()
と付き合わせて読みます。
まず amount == 0
の時は、 fill()
で規定時間経過時のフィルのために、いったん残量をゼロにするところです。確かにこれでOKです。
次の delta < 0
は、 fill()
で残量をゼロした後にフィルするところです。残量をゼロにしたときに quota_consumed = max_quota = 0
にしているので、 delta
は必ず負になります。
それ以外のケースは deduct()
された場合です。このメソッドでは残量(quota)に、残量から減少量(amount_deduct
とする)を引いた量を代入しているので、これがsetterメソッドで残量に設定する値になるから、setterメソッドの引数を amount_setter
と表記すると、、、
self.quota_consumed += delta は、 self.quota_consumed = self.quota_consumed + delta ということなので、 = quota_consumed + (max_quota - amount_setter) self略 = quota_consumed + (max_quota - (max_quota - quota_consumed - amount_deduct)) = quota_consumed x 2 + amount_deduct あれあれ???
うーむ、何をどう間違ったのか。 quota_comsumed
が2回足されてしまいました。1回で良かったのに。2時間ほど考えてみましたがわかりませんでした。悔しいけど飛ばして進みます。
2.3 Item 46: 再利用可能な@propertyメソッドとしてデスクリプターを使え
@property
の大きな問題は再利用性です。同じようなアトリビュートがたくさんあると、それら全てに @property
を用意しなければなりません。更に、無関係のクラスでは再利用できません。
@classmethod
はクラスメソッドで、そのクラスのオブジェクトを作らなくても使えるメソッドでした。 @staticmethod
は知っているような気もしますがクラスメソッドと何が違うのでしたっけ。ここによると、スタティックメソッドは引数としてclsを取らないところがポイントで、クラスのステータスを扱えません。クラスメソッド(やインスタンスメソッドも?)から下請的に使うもののようです。
次はデスクリプターです。Pythonのunder the hoodで活躍するもののようです。under the hood話はたまに聞くから良いのであって、これだけ続くとだいぶお腹いっぱいです。が、Real Pythonの長い記事を読みます。。。。。 。。。読みました。最後の二つの節は意味が追えなかったので飛ばしましたが。
デスクリプターは以下のデスクリプタープロトコルを一つ以上実装したクラスで、デスクリプター機能を実現したい他のクラスにアタッチして使います。
__get__(self, obj, type=None) -> object __set__(self, obj, value) -> None __delete__(self, obj) -> None
そしてEffective Pythonに戻って話を続けます。あれれ、話の展開がそっくりです。こんなのあり? でもお陰で読みやすいです。
以下の例で Grade
がデスクリプターです。
class Grade: def __get__(self, instance, instance_type): ... def __set__(self, instance, value): ... class Exam: math_grade = Grade() writing_grade = Grade() science_grade = Grade()
@property
と同様に、デスクリプターをアタッチした変数は、クラスインスタンスのアトリビュートとしてアクセスできるようになります。上の例では、 Exam
のインスタンスを作ったとき、例えば Exam_instance.writing_grade
のアトリビュートアクセスで、 Exam
にアタッチされているデスクリプター Grade
の __get__
、 __get__
が使われます。
具体例です。
exam = Exam() exam.writing_grade = 40
これは、次のように解釈されます。
Exam.__dict__['writing_grade'].__set__(exam, 40)
ここで、 __dict__
はPythonの全てのオブジェクトが持っているディクショナリのアトリビュートで、そのオブジェクトの全てのアトリビュートやメソッドが入っています。
例えば、以下のようになります。
print(Exam.__dict__) >>> {'__module__': '__main__', 'math_grade': <__main__.Grade object at 0x10c02afa0>, 'writing_grade': <__main__.Grade object at 0x10c02af70>, 'science_grade': <__main__.Grade object at 0x10c041070>, '__dict__': <attribute '__dict__' of 'Exam' objects>, '__weakref__': <attribute '__weakref__' of 'Exam' objects>, '__doc__': None}
__dict__['writing_grade']
は Grade
オブジェクトであることがわかりますね。同様に、
exam.writing_grade
これは、次のように解釈されます。
Exam.__dict__['writing_grade'].__get__(exam, Exam)
ここからしばらくReal PythonとEffective Pythonで同じストーリー展開で話が進みます。
Grade
の実体は一つで、複数インスタンスから参照されるため、Grade
が持つアトリビュートに値を入れても駄目(一つのインスタンスで値を変えると、全てのインスタンスで変わってしまう)- 普通のアトリビュートでなく、オブジェクトをキーとするディクショナリにすれば解決、に見える
- そのディクショナリは強参照しているため、オブジェクトが不要となっても参照数が残り、ガベージコレクションがメモリを解放しない。つまり、メモリリークする。
- 弱参照にすればいいじゃん(いまここ)
何やらマニアックな話です。また新概念の登場です。 weakref
モジュールの WeakKeyDictionary
は弱参照するディクショナリです(ここ)。強参照(strong reference)が普通の参照で、弱参照(weak reference)は弱い参照です。この強弱は何が違うのかと言うと、ガベージコレクションの際に参照数をカウントする、しないになります。
ガベージコレクション(GC)は参照カウントがゼロのオブジェクトを回収してメモリを解放しますが、弱参照はGCにカウントされません。このため、弱参照しか残っていないオブジェクトはGCによってメモリ解放(&参照を解除)されてしまいます。
そんな参照が役に立つのかというと、キャッシュなどのユースケースで使えます。キャッシュは本体がどこかにあるため、参照を消されてメモリ解放されても致命的ではないのです。
WeakKeyDictionary
を使った最終版です。
from weakref import WeakKeyDictionary class Grade: def __init__(self): self._values = WeakKeyDictionary() def __get__(self, instance, instance_type): ... def __set__(self, instance, value): ...
ここでは省略していますが、 __get__
, __set__
の実装が変わっているはず、、、です。
Exam
の実装は変わりません。
class Exam: math_grade = Grade() writing_grade = Grade() science_grade = Grade() first_exam = Exam() first_exam.writing_grade = 82 second_exam = Exam() second_exam.writing_grade = 75 print(f'First {first_exam.writing_grade}') print(f'Second {second_exam.writing_grade}') >>> First 82 Second 75
複数インスタンスで別の値を持つことができました。
2.4 Item 47: Lazyアトリビュートのために__getattr__, __getattribute__や__setattribute__を使え
Pythonのスペシャルメソッド __getattr__
はLazyなアトリビュート参照(参照された時に初めて見に行く)を実現します。クラスが __getattr__
を実装していたら、オブジェクトインスタンスのディクショナリにアトリビュートが見つからなかった際、 __getattr__
が呼ばれます。そしてそのアトリビュートはインスタンスディクショナリ __dict__
に登録され、次以降のアクセスはそのディクショナリから値を取り出します。
class SomeClass: def __init__(self, aaa): self.exists = 5 # オブジェクトが指定アトリビュートを持たない時に呼ばれる def __getattr__(self, name): print("getattr is called") value = 15 # 本当は、ここで外部DBなどから値を持ってくる setattr(self, name, value) return value sc = SomeClass() print(sc.exists) print("1st: ", sc.foo) print("2nd: ", sc.foo) >>> 5 getattr is called 1st: 15 2nd: 15
foo
を2回読んだとき、 __getattr__
が呼ばれたのは最初の1回だけだったことがわかります。
__dict__
にキャッシュされた値を取り出されると困る場合、例えば、アトリビュートが外部データベースの値を参照しているような時は、毎回実体のデータベースの値を読みにいく必要があります。これを実現するのが __getattribute__
スペシャルメソッドです。
class SomeClass: def __init__(self): self.exists = 5 def __getattribute__(self, name): print("__getattribute__ is called") value = 15 setattr(self, name, value) return value sc = SomeClass() print(sc.exists) print("1st: ", sc.foo) print("2nd: ", sc.foo) >>> __getattribute__ is called 15 __getattribute__ is called 1st: 15 __getattribute__ is called 2nd: 15
常に __getattribute__
が呼ばれていることがわかります。
Lazyにセットしたい場合は、これらに共通の __setattr__
を使います。これは毎回呼ばれるごとに、実体に値を設定します。
注意点が一つあって、 __getattribute__, __setattr__
を使うときにはその中でこれらが再帰的に呼ばれないようにしないと、無限recursiveによって落ちます。 super().__setaddr__
のようにします。
def __setaddr__(self, name, value): なにかチェックして例外を上げたり。。。 super().__setattr__(name, value)
2.5 Item 48: __init_subclass__を使ってサブクラスをvalidateせよ
メタクラスは type
を継承することで定義されます。デフォルトの挙動ではメタクラスは、関係するクラスのステートメントのコンテンツを __new__
メソッドで受け取ります。
class Meta(type): def __new__(meta, name, bases, class_dict): print(f'* Running {meta}.__new__ for {name}') print('Bases:', bases) print(class_dict) return type.__new__(meta, name, bases, class_dict) class MyClass(metaclass=Meta): stuff = 123 def foo(self): pass class MySubclass(MyClass): other = 567 def bar(self): pass
__new__
の引数は以下です。
meta: メタクラス name: (関連クラスの)名前 bases: ペアレントクラスのタプル class dict: クラスのアトリビュートやメソッドの入ったディクショナリ
これを実行すると、以下のようになります。
Running <class '__main__.Meta'>.__new__ for MyClass Bases: () {'__module__': '__main__', '__qualname__': 'MyClass', 'stuff': 123, 'foo': <function MyClass.foo at 0x10632f550>} Running <class '__main__.Meta'>.__new__ for MySubclass Bases: (<class '__main__.MyClass'>,) {'__module__': '__main__', '__qualname__': 'MySubclass', 'other': 567, 'bar': <function MySubclass.bar at 0x10632f5e0>}
このようにメタクラスでは、 __new__
メソッドにおいて関係クラスの情報を得たり修正することが出来るため、関連クラスの定義が完了する前に、そのパラメーターの有効性をチェックする目的で使えそうです。
しかしこのユースケースでは、メタクラスを使わずに、Python 3.6以降で用意されている __init_subclass__
スペシャルクラスメソッドを使った方が良いです。
class BetterPolygon: sides = None # サブクラスで指定必要 def __init_subclass__(cls): super().__init_subclass() if cls.sides < 3: raise ValueError('Polygons need 3+ sides') @classmethod def interior_angles(cls): return (cls.sides - 2) * 180
これを継承して、ポリゴンを作ります。
class Point(BetterPolygon): sides = 1 >>> Traceback ... ValueError: Polygons need 3+ sides
クラス Point
を作ろうとして BetterPolygon.__init_subclass()
にて例外が上がりました。
__init_subclass__
を使うと、複数のクラスチェック用クラスを継承したり、更にはダイヤモンド継承をしてもうまくハンドルしてくれます。
class Top: def __init_subclass__(cls): super().__init_subclass() print(f'Top for {cls}') class Left(Top): # これを定義する時にTop.__init_subclass__()が呼ばれる def __init_subclass__(cls): super().__init_subclass() # Rightにて既に実行済みのため何もしない print(f'Left for {cls}') class Right(Top): # これを定義する時にTop.__init_subclass__()が呼ばれる def __init_subclass__(cls): super().__init_subclass() # Top.__init_subclass__()を呼ぶ print(f'Right for {cls}') # これを定義する時にRight.__init_subclass__()が呼ばれる # その中で、top.__init_subclass__()を呼ぶ # 次に、Left.__init_subclass__()が呼ばれる # その中で、top.__init_subclass__()を呼ばない(Rightが実行済み) class Bottom(Left, Right): def __init_subclass__(cls): super().__init_subclass() print(f'Bottom for {cls}')
これを実行します。
Top for <class '__main__.Left'> # LeftからTopの__init_subclass__実行 Top for <class '__main__.Right'> # RightからTopの__init_subclass__実行 # BottomからRight.__init_subclass__実行->Topの__init_subclass__実行 Top for <class '__main__.Bottom'> Right for <class '__main__.Bottom'> # BottomからRight.__init_subclass__実行 Left for <class '__main__.Bottom'> # BottomからLeft.__init_subclass__実行
Bottom
は左経由、右経由と両方から Top.__init_subclass__
を呼んでしまいそうですが、実際には、 Top.__init_subclass__
はRight/Leftクラスで合わせて一度しか呼ばれていないことがわかります。 super().__init_subclass__()
のお陰でダイヤモンド継承をうまく処理している証拠です。
2.6 Item 49: __init_subclass__を使ってクラスを登録せよ
モジュラーなPythonプログラムを作るために、次のような関数を使って作成したクラスを全て登録すること(class registration)は役に立つパターンの一つだそうです。
registry = {} def registr_class(target_class): registry[target_class.__name__] = target_class
クラスを作ったら、必ず register_class
を呼ぶ必要があるのですが、忘れないようにこれを自動で行うために __init_subclass__
が使えます。
class Register(): def __init_subclass__(cls): super().__init_subclass__() register_class(cls) class NewClass(Register): pass print(registry) >>> {'NewClass': <class '__main__.NewClass'>}
メタクラスも使えますが、 __init_subclass__
を使った方がクリアで理解しやすいとのことです。
2.7 Item 50: __set_name__を使ってクラスアトリビュートをannotateせよ
__set_name__
はPEP 487で提唱され、Python 3.6以降に入っています。デスクリプターの問題(の一つ)は、それを含むクラスの情報を持たないことです。例えば、アトリビュートを __dict__
に登録するユースケースにおいて、 __get__
が呼ばれるまで登録するアトリビュートの名前を知りません。
__set_name__
はクラス作成の際に、このデスクリプターをアタッチする全てのアトリビュートに対して呼ばれ、デスクリプターがアトリビュート名の知識を得ることが出来ます。
class Field: def __init__(self): self.name = None self.internal_name = None def __set_name__(self, owner, name): # Called on class creation for each descriptor self.name = name self.internal_name = '_' + name def __get__(self, instance , instance_type): if instance is None: return self return getattr(instance, self.internal_name, '') def __set__(self, instance, value): setattr(instance, self.internal_name, value)
Field
はデスクリプターです。 __set_name__
によって、このデスクリプターをアタッチするクラスのアトリビュート名をとその内部名を覚えておいてくれます。
class FixedCustomer: first_name = Field() last_name = Filed() prefix = Field() suffix = Field() >>> cust = FixedCustomer() >>> cust.__dict__ {} >>> cust.first_name.__dict__ # 変数名はデスクリプターField内の__dict__に保持 {'name': 'first_name', 'internal_name': '_first_name'} >>> cust.first_name = 'Joy' >>> cust.__dict__ # 変数の値は各custオブジェクトの__dict__に保持 {'_first_name': 'Joy'} >>> cust.first_name.__dict__ {'name': 'first_name', 'internal_name': '_first_name'}
first_nameを設定した時に、 __set__
の指定通りに内部名が dict
に登録されていることがわかります。
first_nameは実際にはそのクラスのアトリビュートではなく、デスクリプター内に保持されている情報であるため、当該クラスオブジェクトの __dict__
には登録されていません。
2.8 Item 51: クラス拡張を組み合わせるために、メタクラスよりもクラスデコレーターを使え
クラスの全てのメソッドに対し、ヘルパーを使って引数や戻り値、上がる割り込みをプリントすることを考えます。全てのメソッドにデコレーターを付けることは面倒です。メタクラスを使って仕込むことも、対象クラスの親クラスが別のメタクラスを既に使っていた場合に問題となります。
これを解決するために、クラスデコレーターがあります。クラスデコレーターはクラス定義の前に @<decorator_name>
を付けることで機能します。
まずはシンプルなクラスデコレーターの例です。
def my_class_decorator(klass): klass.extra_param = 'hello' return klass @my_class_decorator class MyClass: pass print(MyClass) print(MyClass.extra_param) >>> <class '__main__.MyClass'> hello
デコレーターによって、 extra_param = 'hello'
が設定されています。
次は上で述べたデバッグ用デコレーターです。
from functools import wraps def trace_func(func): if hasattr(func, 'tracing'): # Only decorate once return func @wraps(func) def wrapper(*args, **kwargs): result = None try: result = func(*args, **kwargs) return result except Exception as e: result = e raise finally: print(f'{func.__name__}({args!r}, {kwargs!r}) -> ' f'{result!r}') wrapper.tracing = True return wrapper
functools
の wraps
は関数をラッパーするデコレーターです。Item 26で学んでいました。 try
ブロックの finally
は、リターンしようが、例外が上がろうが、実行されます。
次に示す trace
はクラスデコレーターとして機能する関数です。 dir
ビルトイン関数によってデコレートするクラスのアトリビュート一式(メソッドなど含む)の名前(キー)を取得し、これが trace_types
に含まれるタイプなら(ie, メソッド等だったら) trace_func
でラップして、ラップしたものを setattr
で __dict__
に設定します。つまり、そのメソッドの代わりにラップしたものが呼ばれるようになります。
import types trace_types = ( types.MethodType, types.FunctionType, types.BuiltinFunctionType, types.BuiltinMethodType, types.MethodDescriptorType, types.ClassMethodDescriptorType) def trace(klass): for key in dir(klass): value = getattr(klass, key) if isinstance(value, trace_types): wrapped = trace_func(value) setattr(klass, key, wrapped) return klass
@trace
クラスデコレーターで、 dict
を継承したクラス TraceDict
をデコレートします。
>>> @trace ... class TraceDict(dict): ... pass ... >>> trace_dict = TraceDict([('hi', 1)]) __new__((<class '__main__.TraceDict'>, [('hi', 1)]), {}) -> {} >>> trace_dict['there'] = 2 >>> trace_dict['hi'] __getitem__(({'hi': 1, 'there': 2}, 'hi'), {}) -> 1 1 >>> try: ... trace_dict['does not exist'] ... except KeyError: ... pass # Expected ... __getitem__(({'hi': 1, 'there': 2}, 'does not exist'), {}) -> KeyError('does not exist')
実行結果を見ると、 trace_func
内のprint文は TraceDict
がnewされた時と、キー'hi', 'doesn not exist'の値をそれぞれ参照しようとしたときに動いています。'there'に値2を入れた時に動いていないのはどうしてでしょうか。
>>> isinstance(trace_dict.__new__, types.MethodType) True >>> isinstance(trace_dict.__getitem__, types.MethodType) True >>> isinstance(trace_dict.__setitem__, types.MethodType) False # あれ!? >>> trace_dict.__new__ <bound method dict.__new__ of {'hi': 1, 'there': 2}> >>> type(trace_dict.__new__) <class 'method'> >>> type(trace_dict.__getitem__) <class 'method'> >>> type(trace_dict.__setitem__) <class 'method-wrapper'>
__new__, __getitem__
共にmethodタイプなのに対して、 __setitem__
はmethod-wrapperという異なるタイプだからのようです。