概要

いやー、 Effective Python は1年以上前に読んでいた本だ。でも4章でよくわかんなくなって、読むのヤメちゃったんだよな。一念発起して、4章から先を読んで見るぞ。

前と同じで、楽しめたところをノートしていく。

 

楽しめたところノート

 

get や set は基本的には使わんでいい

他の言語から Python に移ってきたプログラマは、クラスの中で getter や setter メソッドを実装しようとするがそれは Pythonic ではない。素のまま public 属性で実装すればいい。

 

get や set するときに何かしたいなら property を使う

「何かしたい」というのは具体的には、「この変数には0~100の数値だけ格納されるようチェックしたいなー」みたいなときだ。

class Test:

    def __init__(self):
        self._foo = 0

    # @property をつけると getter になり、 foo をインスタンス変数みたいに呼び出せるようになる。
    @property
    def foo(self):
        # 取得するときに何か起こせることを確認するため、ムダに+1して返すようにします。
        return self._foo + 1

    # .setter をつけると setter になり、 foo へ値代入するとき起こることを定義できます。
    @foo.setter
    def foo(self, new_value):
        # 0~100のみ認めるようにします。
        if not (0 <= new_value <= 100):
            raise ValueError('0~100にしてくれ')
        self._foo = new_value


test = Test()
print(test.foo)  # 初期値の0に+1されて1
test.foo = 100   # OK
test.foo = 101   # ValueError: 0~100にしてくれ って出る。

こうやって値代入時にチェックを挟めるのは、 A 型みどりんとしては歓迎だね。

 

property が何個も増えたらやばくね?

上の例では foo だけだったから別にいいけど、まったく同じ変数が増えたら、やばいことにならね?

class Test:

    def __init__(self):
        self._foo = 0
        self._bar = 0
        self._baz = 0

    @property
    def foo(self):
        return self._foo + 1

    @foo.setter
    def foo(self, new_value):
        if not (0 <= new_value <= 100):
            raise ValueError('0~100にしてくれ')
        self._foo = new_value

    @property
    def bar(self):
        return self._bar + 1

    @bar.setter
    def bar(self, new_value):
        if not (0 <= new_value <= 100):
            raise ValueError('0~100にしてくれ')
        self._bar = new_value

    @property
    def baz(self):
        return self._baz + 1

    @baz.setter
    def baz(self, new_value):
        if not (0 <= new_value <= 100):
            raise ValueError('0~100にしてくれ')
        self._baz = new_value

ヤダ……サイテー……。 foo と同じ、0~100チェックを行う変数をひとつ増やすたびに、9行も増やす必要がある。でもこの処理を共通化するにはどうすりゃいいんだ?

 

ディスクリプタという考え方を使う

まったく知らん用語だ。けど、ようは property を共通化するクラスのことみたい。

import weakref


class Descriptor:

    def __init__(self):
        # {インスタンス化された Descriptor: 値} という辞書を作ります。
        # NOTE:
        #   メモリリークを防ぐため、普通の辞書ではなく WeakKeyDictionary を使います。
        #   なぜ普通の辞書だとメモリリークになるのかはよく理解できていない。
        #   まあ辞書と使用感は同じだから、「こっちのほうがいいらしい」という程度で使ってみよう。
        self._values = weakref.WeakKeyDictionary()

    def __get__(self, instance, instance_type):
        # instance: このプロパティ(この Descriptor はクラス変数となる)を持ってるインスタンス……か?
        # instance_type: このプロパティを持ってるインスタンスの型(クラス)。

        # NOTE:
        #   instance is None になる状況がわからないけど
        #   オライリーに書いてあったのでそのまま置いてあります……。
        if instance is None:
            return self

        # 値を取得するのと同時に初期値を定義しています。
        return self._values.get(instance, 0)

    def __set__(self, instance, new_value):
        if not (0 <= new_value <= 100):
            raise ValueError('0~100にしてくれ')
        self._values[instance] = new_value


class Test:

    # ここで、「インスタンス変数からクラス変数になっとるやんけ」と思うんだけど、
    # 実はそもそも上のやつもインスタンス変数ではなかったのだ。
    # 「プロパティ」がそもそもクラス変数のこと。
    # 「アトリビュート」がインスタンス変数とインスタンスメソッドのこと。
    foo = Descriptor()
    bar = Descriptor()
    baz = Descriptor()

具体的には __get__ __set__ を持っていることがディスクリプタの条件みたいだ。実際に使ってみよう。

test1 = Test()
test2 = Test()

test1.foo = 50
test2.foo = 100

print(test1.foo)  # +1されて51
print(test2.foo)  # +1されて101

test2.foo = 101   # ValueError: 0~100にしてくれ って出る。

うん、オッケーだね。

 

メタクラス

メタクラスを使うと、あるクラスのサブクラスを作るとき、その内容を検証してエラーをぶっ放せるようだ。実行時ではなく定義時に検証できるってのが、なかなかスゴイんじゃないか? 実際の実装方法は次のとおり。

# 親クラスを定義する。
# metaclass に指定している Meta で、サブクラスのチェックを実装する。
# ほんとなら Meta はこの上に書かないとダメだけど説明の順序のために下に書く。
class Parent(metaclass=Meta):

    # Python2 ではこんなふうに定義する。
    # __metaclass__ = Meta

    # この stuff をサブクラスで規定するつもり。
    stuff = None


# サブクラスを定義する。
class Child(Parent):

    # stuff をここで定義するんだけど、この値は0~100にしてほしいものとする。
    stuff = 101
# そこで出てくるメタクラス。
class Meta(type):

    # インスタンス化されるときではなく、クラスが定義されるときにすでに実行される。
    def __new__(meta, name, bases, class_dict):

        print(meta,         # self みたいなもん <class '__main__.Meta'>
              name,         # これが metaclass として設定されているクラスの名前
              bases,        # 継承関係にあるクラスが格納されている。 object を省略すると空タプルになる。
              class_dict)   # '__module__': '__main__', '__qualname__': 'Parent', ... }

        # Parent クラスではチェック要らない。要らないっていうか初期値は None だからエラー出る。
        # Parent であるかどうかは、 bases が空かどうかで判断。
        # あるいは object を省略していないときは (object,) かどうかで判断。
        if bases != () and bases != (object,):
            if not (0 <= class_dict['stuff'] <= 100):
                raise ValueError('0~100にしてくれ')

        # ここでクラスを実際に生成している。
        return type.__new__(meta, name, bases, class_dict)

すると Child を定義するときに「0~100にしてくれ」ってエラーが出る。ただクラスを定義しているだけで、何も実行していない(つもり)なのに検証エラーが出るというのは不思議なものだ。

 

メタクラスの他の使いみち

メタクラスの使いみちとして他によくあるのが registration というものらしい。だけどこのへんはとりあえずいいや。まあともかく、メタクラスは、クラスが完全に定義される前にクラス属性を修正することを可能にするのだ。

 

class を使わずに class を定義する

え!! class を使わずに class を!?

# 出来らあっ!
A = type('A', (), dict(a=1))

これで A というクラスが完成している。マジかよ。

これはこの本に書いてあったことじゃなく、メタクラスの最後に書いてある type.__new__ ってのは何なんだ? とという疑問から type について調べていくうちに知ったことだ。

これまでぼくにとって type とは、オブジェクトの型を教えてくれる組み込み関数に過ぎなかった。だけど実は type はクラスの型のことでもあったのだ。クラスのクラスってわけだな。クラスとは type のインスタンスだったのだ。

 

小休憩

網羅的に理解しようとせず、プロパティとメタクラスに絞って整理してみた。おかげでそのふたつについては分かったと思うぜ。それに、その延長として「プロパティとは何か」「アトリビュートとは何か」「type とは何か」という疑問に答えられるようになったのがちょっとうれしいかも。

  • プロパティはクラス変数のこと。
  • アトリビュートはインスタンス変数のこと。
  • type とは組み込み関数であり、かつクラスの型。

 

Effective Python 感想文一覧