効果的なPython その2

python

 読み込むのがかなり大変だった…。3章と4章の読解です。ざっくり解説なのでちゃんと本は買って読みましょう。

3章 クラスと継承

 Pythonはオブジェクト指向プログラミング言語なので、ひとしきり継承、ポリモーフィズム、カプセル化などなどサポートしています。

 この使い方のコツです。難解なので読むのに時間がかかってしまいました。

辞書やタプルで記録管理するよりもヘルパークラスを使う

 辞書。便利ですよねぇ。これを多用するな。って事ですね。

 辞書それ自身も含めて色々なオブジェクトをキーと共に管理できるので、辞書を内包したクラスってのを作りたくなります。

 ただどんどん拡張していくと、クラスの作りがだんだん複雑になってきがちで、拡張を重ねるごとに可読性を損ねてきます。

 やばいと思ったら、ヘルパークラスを作りましょう。って事の様です。

 これが例と共に示されていますが、長いので割愛。要点はこんな感じだと思います。

 この「やばい」の基準は、値が他の辞書だったり、長いタプルになるような場合だったり、内部状態の辞書が複雑になってきた場合。ってアドバイスです。

単純なインターフェースにはクラスの代わりに関数を使う

 関数の引数に、フックする関数(コールバックする関数)ってのがよくあって、それを使う時には素直に関数使おうね。って事かな。

 クラスのメソッドもコールバック関数に設定できるけど、それをするとそのクラスのメソッドが一体何なのかよくわからん。って事だと思います。

 複雑にもなりがちだし。

 どうしてもクラスを使ってコールバックさせたい場合には、クラスをcallableにしてあげて、callメソッドがそのコールバックのために作られた関数だよ。ってのを明示してあげることでわかりやすいコードになるよ。

 って事かな。これもなかなか難解で果たしてこの理解でよいのかもよくわかりません。

@classmethodポリモーフィズムを使ってオブジェクトをジェネリックに構築する

 ジェネリックってのがそもそも何なんだ?ってところからつまづきましたが、どうやらCでいうところのテンプレートで型を自在にしておく。みたいな事を言うみたいです。総称性って日本語に訳されているサイトも見ました。(java/C#の記事ですが)

 その後の使い方次第って意味では多態性と似た表現のようにも見えます。

 またこの@classmethodってのも初めましてです。調べるとインスタンス化しなくても使えるメソッド。って事のようです。C++で言うところのstaticなメソッドですかね。

 やっとタイトルの意味がわかって、staticなメソッドをオーバーライドさせて多態性を与える。って事を言っていますが、やはりこれだけでも中身はよくわかりません。

 このclassmethodはそのクラスのジェネレータとして使う事が多く、ある種のコンストラクタ代わりになりえるよ。それをうまく使って、派生クラスのジェネレートを効率よく書こうね。

 って事で理解しときます。そんな例が書かれています。

    @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

 cls自身のインスタンス列を返す関数で、このclsってのが特に限定されない記述になります。派生クラスなら派生クラスなりに動いてくれます。

 この時使っているinput_classも

    @classmethod
    def generate_inputs(cls, config)
        data_dir = config['data_dir']
        for file in os.listdir(data_dir):
            yield cls(os.path.join(data_dir, file))

 こんな感じ。同様にclsを限定してません。派生したinput_classでもこの関数は使いまわせます。

 多分こんなことを言ってるんだと思います。多分。

親クラスをsuperを使って初期化する

 直で親クラスを指定するのではなく、ちゃんとsuper.init(self)しようね。

 多重継承(非推奨)した時なんかは特に気をつけようね。

多重継承はmix-inユーティリティクラスだけ使う

 基本的に多重継承は非推奨です。せいぜいmix-inなクラスを継承する程度にしておきましょう。って言ってるのだと思います。

 同じような機能を有する複数のクラスを作ろうと思ったら、その同じような機能だけを実装したmix-inクラスを作って、それを継承する形が望ましい。

 mix-inってのは基本的にはインスタンス変数すなわちデータにあたる部分を持たず、機能だけを持ったクラスだと思えば良さそうです。

 ユーティリティ関数群だけを持ったクラスで、その振る舞いが継承することで変わるものの機能はちゃんと使える。ってそんな感じだと思います。

 クラスは継承する毎にどんどん特殊化していきます。そのデータと機能を持ったある特殊なクラスに対して汎用な機能を追加しようと思ったときに、このmix-inクラスも同時に継承することで、元のクラスが持つデータ構造は崩すことなく、機能だけ足せます。

 多重継承はそんな使い方にとどめておきましょう。なるほど。

プライベート属性よりはパブリック属性が好ましい

 protectedがないんですねぇ。privateは継承クラスからは触れません。原則。

 この原則が面白くて、プライベート変数にも本気になれば触れる。って。

 もちろん直接は触れないのですが、

class A:
    def __init__(self, b):
        self.__private_val = b

inst = A(10)
# print(insat.__private_val)  # error
print(inst._A__private_val)  # OK

 こんなして_クラス名をつけてやると触れます。じゃあなぜこんな逃げ道を用意しているのでしょうか?この理由が面白くて、

 「みんないい大人なんだから」

 だそうです。

 こんな思想なもんだから、隠す必要がないならパブリックにしとこーぜ。ってのが基本スタンス。見せる必要ないならプライベートにしとこーぜ。って思想では無いようです。

 触ったり、壊したりしてほしくない変数にはちゃんとドキュメンテーションを入れておいてね。

カスタムコンテナ型はcollections.abcを継承する

 とても便利なcollection.abcって抽象基底クラスがあるので、ちゃんと使いましょ。

スポンサーリンク

メタクラスと属性

 この章のタイトルは逆がいいと思います。「属性とメタクラス」の順で説明がなされています。属性の話は何とか読めたのですが、メタクラスが難しい。読解するにはまずメタクラスの勉強が必要でした。

 文中の例題をだいぶいじっています。そうでないと理解ができなかったので。わかりやすく読み解いたつもりですが、解釈があっているかどうかも自信がないです。それくらい難しかった…。

getやsetメソッドよりも素のままの属性を使う

 Pythonのコードを書く時も普通にセッターとゲッターを使ってました。でも素の値に直接代入したり取りだしたりするのがPython流らしいです。

 確かに素の値を使った方が可読性は良さそうです。ではなぜそれでいいのか?setやgetをした時にエラーチェック他色々オプションの処理したくなりますよね?

 そんな時に積極的に@propertyと.setterを使いましょう。という事のようです。

 ちょっと掟を破ってプライベート変数使ってますが、set/getなのでとりあえず。

class A:
    __name = ""
    __age = 0
    
    def __init__(self, name, age):        
        self.name = name
        self.age = age
                
    @property
    def name(self):
        return self.__name
    
    @name.setter
    def name(self, name):
        if name == "":
            self.__name = "No name"
        else:
            self.__name = name
    
    @property
    def age(self):
        return self.__age
    
    @age.setter
    def age(self, age):
        if age < 0:
            raise ValueError("age error")
        self.__age = age
        
b = A("jhon", 10)
print(b.name)
# b.age = -1  # ValueError: age error

# c = A("doe", -1) # ValueError: age error

d = A("", 9)
print(d.name)

 これはおしゃれ。

 継承を使うと、

class Z:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
class A(Z):
    def __init__(self, name, age):
        super().__init__(name, age)
                
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, name):
        if name == "":
            self._name = "No name"
        else:
            self._name = name
    
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, age):
        if age < 0:
            raise ValueError("age error")
        self._age = age
        
b = A("jhon", 10)
print(b.name)
# b.age = -1  # ValueError: age error

# c = A("doe", -1) # ValueError: age error

d = A("", 9)
print(d.name)

 これもおけ。継承により縛りを強くしたい場合なども安心して素の属性へのアクセスができます。

 注意点としては使う人を驚かせないように実装すること。getterなのに内部の値を変えたり、値をセットしただけなのにえらい遅いとか。まぁそりゃそうですね。コード上は素の値を読み書きしてるだけの記述に見えるわけだし。

属性をリファクタリングする代わりに@propertyを考える

 @propertyでその属性へのアクセス時の動きを変更できるので、しまったなぁこの属性もう少し拡張したいなぁ~でも使用者へはこれまでの使い勝手を残したいなぁ~。って思ったときに便利に使えます。

 でもこれも注意点で@propertyを使いすぎるようなら、真面目にそのクラスと呼び出し元をリファクタしましょう。だそうです。

再利用可能な@propertyメソッドにディスクリプタを使う

 前述のように名前と年齢レベルだったらさほど気になりませんが、テストの点数をset/getするときに同じようなつくりにすると、やれ数学だ化学だ英語だと同じ記述のコピペが始まります。スマートではありません。

 これを解決する手段としてディスクリプタを使いましょう。ということのようです。

 ただこれがややこしい。あまりなじみのない2つの仕組みを理解している必要があります。get, setの2つのディスクリプタの振る舞いと、WeakKeyDictionary(およびweakrefという概念)というクラスの振る舞い。

 これを詳しく書くとそれだけで記事になりそうな内容なので今回は別で読み込んでいる前提でこの教えに従ったコードを書くと、

from weakref import WeakKeyDictionary

class Score(object):
    def __init__(self):
#        self._value = {} # 一見これでも動くがメモリリークする。オブジェクトが死んでも削除されない。
        self._value = WeakKeyDictionary()
        print("call at once")
    
    def __get__(self, instance, instane_type):
        if instance is None: return self # instanceされてない時に呼ばれたらNoneで来る。
        return self._value.get(instance, 0) # デフォルト0
    
    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError("Score must be between 0 and 100")
        self._value[instance] = value # instanceをKeyにした辞書

class Exam(object): # このクラスが生まれたときにScore.__init__が呼ばれる
    sugaku = Score() # 4回@propertyを作る必要がない。
    eigo = Score()
    kagaku = Score()
    buturi = Score()

print("start") # Examがインスタンス化される前にScore.__init__が呼ばれる

john = Exam()
# john.sugaku = 110 # VlueError
john.sugaku = 99
john.eigo = 10
john.kagaku = 90
john.buturi = 80

print("joh", john.sugaku, " ", john.eigo, " ", john.kagaku, " ", john.buturi)

doe = Exam()
doe.sugaku = 0
doe.eigo = 90
doe.kagaku = 20
doe.buturi = 10

print("joh", john.sugaku, " ", john.eigo, " ", john.kagaku, " ", john.buturi)
print("doe", doe.sugaku, " ", doe.eigo, " ", doe.kagaku, " ", doe.buturi)

# print("Exam", Exam.sugaku) # instance = Noneで__get__が呼ばれる (cannot create weak reference to 'NoneType' object)

 こんな感じ

 数学、英語、化学、物理のそれぞれの点数毎に@propertyを作らなくてもスマートにset/getができるようになりました。

 ただこの記述を理解するためには前提知識がかなり必要…。

 この先のメタクラスのところで別記述もあります。こっちの方がわかりやすかったかな…。

遅延属性にはgetattr, getattribute__, setattrを使う

 この理解はかなり難儀しました。ひとまず狙いは、要素数や名前が不定のデータを扱うときにはこの仕組みを使うことでメモリ資源の節約ができるよ。ってことだと理解しました。

 先ほどの例だと数学、英語、化学、物理のスコアでしたが、人によっては国語、日本史、地理など要素の名前が違う複数の属性を用意する必要があります。

 考えられる属性すべてを持つ汎用クラスを作ってもよいのですが、理系と文系で履修する科目に偏りがあり、人数が膨大になったりすると全部を持つのは経済的ではない。という課題に対する回答だと思います。

 では任意の属性を追加したときに上と同じようなset/getができるようにしましょう。となるとset, と getだけではどうにもならない。そのためこの教えであるgetattributeが必要になってきます。

 実際の本文で例示されている題材と全然違う例ですが、このほうが自分自身しっくりきました。

 基本動作は別途 getattr, getattribute, setattrの動きの解説ページを調べるとして、1点注意点としては無限ループに入ってしまうリスクです。

 要素にアクセスした時点でgetattributeが呼ばれてしまうので、このアクセスする記述をgetattributeの中で書いてしまうと、無限に再入してしまい落ちます。値のアクセスは親クラスのgetattribute経由で行いましょう。

 使いこなすのが大変そう。

サブクラスをメタクラスで検証する

 また知らない単語が出てきた…。メタクラスだそうです。

 ここで例示されているのはサブクラスがいい塩梅で作られているかどうかを検証するためのメタクラスです。

# 多角形を確認するメタクラス
# メタクラスはtypeを継承する
class ValidatePolygon(type):
    def __new__(meta, name, bases, class_dict):
    # __new__関数はクラス生成時に実行される関数
    # クラスの情報を受け取ることができる
    # name:クラス名, bases:親クラス, class_dict:クラス属性
        if bases != (object,): #抽象クラスは検証しない
            if class_dict["sides"] < 3: # 3未満なら例外
                raise ValueError("Pylygon need 3 sides")
                
        return type.__new__(meta, name, bases, class_dict)


# 多角形クラス
# メタクラスの指定は「metaclass=...」で行う(Python3)
class Polygon(object, metaclass=ValidatePolygon):
    sides = None #子クラスで定義する
    
    @classmethod
    def interior_angles(cls):
        return (cls.sides - 2) * 180

# 3角形
class Triangle(Polygon):
    sides = 3

# 2角形
class Line(Polygon):
    sides = 2

 例を見ないと何とも動きがピンとこないですが、こんな感じでインスタンス化する前に(クラス定義した時点で)このクラスがOKかNGかがわかってしまいます。

 この記述だけで2角形がNGだとわかります。実装しただけでNGがわかるという何やら不思議な機能です。

クラスの存在をメタクラスで登録する

 なんともつかみどころのないメタクラスですが、次の使い道としてクラス定義の瞬間に呼ばれることを利用したクラス登録作業をさせましょう。ってことです。

 あるクラスを作ったときにそのクラスの存在と登録(シリアライズさせて登録)させるようなシチュエーションで、その登録忘れを防ぐための仕組みとしてメタクラスを使いましょう。

 登録するためにわざわざ関数呼びたくないし、呼び忘れそう。そして登録してもいないのに、削除の関数呼んで死んでしまったり。そんなことを防ぐ目的で使うとよいよ。って言ってると思います。

 ポイントはインスタンスを登録するのではなく、クラスを登録させる作業で使うときってことですね。

 クラスが宣言されたときにこのメタクラスさんは呼ばれてしまうので、インスタンスしようがしまいがこの関数呼ばれるので気を付けましょう。

 あまり個人的にクラス登録させたいと思ったことはないですが、どうやら「モジュラーなPythonプログラムを構築するための、有用なパターン」だそうです。

クラス属性をメタクラスで注釈する

 これはかなりおしゃれな記述が可能になって面白かったです。まずはおしゃれじゃない方

class Score(object):

    def __init__(self, name):
        self.name = name
        self.internal_name = "_" + self.name # 点数格納用のメンバ
    
    def __get__(self, instance, instance_type):
        if instance is None: return self
        return getattr(instance, self.internal_name, 0)
    
    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError("Score must be between 0 and 100")
        setattr(instance, self.internal_name, value)

class Exam(object):
    sugaku = Score("sugaku") # ここがダサい。sugakuメンバにsugakuの名前でクラス定義が必要
    eigo = Score("eigo")
    kagaku = Score("kagaku")
    buturi = Score("buturi")

john = Exam()
# john.sugaku = 110 # VlueError
john.sugaku = 99
john.eigo = 10
john.kagaku = 90
john.buturi = 80

print("joh", john.sugaku, " ", john.eigo, " ", john.kagaku, " ", john.buturi)

doe = Exam()
doe.sugaku = 0
doe.eigo = 90
doe.kagaku = 20
doe.buturi = 10

print("joh", john.sugaku, " ", john.eigo, " ", john.kagaku, " ", john.buturi)
print("doe", doe.sugaku, " ", doe.eigo, " ", doe.kagaku, " ", doe.buturi)

 メンバ変数定義時に同名でコンストラクタを呼ばないといけないです。タイプミスがあるかもしれないし、同じこと2回書くのはスマートじゃない。ってことですね。

 これが冗長なので、メタクラスを使って解決しましょう。という話です。

 ぶっちゃけ前述でもよいのでしょうが、weakrefが出てこなくてこっちの方がいいでしょ?って話のようです。個人的にもweakrefの仕組みがよくわかってないので、こっちの方が腹落ちしました。

class Score(object):

    def __init__(self):
        self.name = None
        self.internal_name = None

    def __get__(self, instance, instance_type):
        if instance is None: return self
        return getattr(instance, self.internal_name, 0)
    
    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError("Score must be between 0 and 100")
        setattr(instance, self.internal_name, value)

class Meta(type):
    def __new__(meta, name, bases, class_dict):
        for key, value in class_dict.items():
            if isinstance(value, Score):
                value.name = key
                value.internal_name = "_" + key
        cls = type.__new__(meta, name, bases, class_dict)
        return cls

class Exam(object, metaclass=Meta):
    sugaku = Score()
    eigo = Score()
    kagaku = Score()
    buturi = Score()

john = Exam()
# john.sugaku = 110 # VlueError
john.sugaku = 99
john.eigo = 10
john.kagaku = 90
john.buturi = 80

print("joh", john.sugaku, " ", john.eigo, " ", john.kagaku, " ", john.buturi)

doe = Exam()
doe.sugaku = 0
doe.eigo = 90
doe.kagaku = 20
doe.buturi = 10

print("joh", john.sugaku, " ", john.eigo, " ", john.kagaku, " ", john.buturi)
print("doe", doe.sugaku, " ", doe.eigo, " ", doe.kagaku, " ", doe.buturi)

 おしゃれ。Examクラスでは引数なしで宣言してるのに、Examメンバの名前で実態を作っている。これで先の記述と全く同じ効果が得られています。

つづく

  5章へつづく

python
スポンサーリンク
キャンプ工学

コメント

タイトルとURLをコピーしました