Daydreaming in Brookline, MA

Effective Python一人輪読会(Item 37 to 51)

Table of Contents

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_inputsPathInputData を一つずつ返す 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_inputPathInputData にクラスメソッドとして組み込んでいます。

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 インスタンスの生成を行っています。

LineCountWorkerGenericWorker を継承します。他は変更なし。

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を継承せよ

これもタイトルの通りです。

listdict 等の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_quotaquota_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で同じストーリー展開で話が進みます。

  1. Grade の実体は一つで、複数インスタンスから参照されるため、 Grade が持つアトリビュートに値を入れても駄目(一つのインスタンスで値を変えると、全てのインスタンスで変わってしまう)
  2. 普通のアトリビュートでなく、オブジェクトをキーとするディクショナリにすれば解決、に見える
  3. そのディクショナリは強参照しているため、オブジェクトが不要となっても参照数が残り、ガベージコレクションがメモリを解放しない。つまり、メモリリークする。
  4. 弱参照にすればいいじゃん(いまここ)

何やらマニアックな話です。また新概念の登場です。 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

functoolswraps は関数をラッパーするデコレーターです。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という異なるタイプだからのようです。