sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

python クラス 学習

Python チュートリアルを読んで気になったところをメモ。

method object

x.f()MyClass.f(x) が厳密に等価であるとは、どういうことか。

class MyClass:
    def f(self):
        print("f:", self)

print("\n---TEST 1 ---")
# クラスオブジェクトからそのまま参照
print("MyClass.f =", MyClass.f)  # MyClass.f = <function MyClass.f at 0x000002688BBF99E0>

# インスタンスを生成
x = MyClass()

# インスタンスから参照する bound method (クラスオブジェクトの参照とは異なる)
print("x.f =", x.f)  # x.f = <bound method MyClass.f of <__main__.MyClass object at 0x000001E31EF37950>>

print("\n---TEST 2 ---")

# 等価比較
x.f()  # f: <__main__.MyClass object at 0x000002339C00C440>
MyClass.f(x)  # f: <__main__.MyClass object at 0x000002339C00C440>

at 0x00000..... で示される数字は、オブジェクトのメモリアドレスを16進数で示したものです。hash 値と同じように、オブジェクトが同一のものかどうかを比較できます。なので、等価であると証明できた。

この動きを内部的な仕様から補足する。MyClass.ff は、関数オブジェクト function として格納されていて、たとえばインスタンス x から x.f() のようにアクセスすると function が内部的に特別な処理をして呼び出されている。

bound = MyClass.__dict__['f'].__get__(x, MyClass)

この特別な呼び出しは、class から x インスタンスが生成されて、そのインスタンスを通して method にアクセスした際に発生します。この特別な呼び出しを bound method といいます。つまり、このときの bound method の特殊な呼び出しによって(自動で)インスタンス自身である x が引数 self として f に渡されています。

なので、呼び出しは等価であるという説明になっています。bound method についても整理を追加する。

class MyClass:
    def f(self):
        print("f:", self)

print("\n---TEST 1 ---")
# クラスオブジェクトからそのまま参照
print("MyClass.f =", MyClass.f)  # MyClass.f = <function MyClass.f at 0x000002688BBF99E0>

# インスタンスを生成
x = MyClass()

# インスタンスから参照する bound method (クラスオブジェクトの参照とは異なる)
print("x.f =", x.f)  # <bound method MyClass.f of <__main__.MyClass object at 0x0000019B4FE6C7A0>>

print("\n---TEST 2 ---")

# 等価比較
x.f()  # <__main__.MyClass object at 0x0000019B4FE6C7A0>
MyClass.f(x)  # <__main__.MyClass object at 0x0000019B4FE6C7A0>

print("\n---TEST 3 ---")

func = MyClass.__dict__['f']  # クラスに格納されている function オブジェクトを直接取得
bound = func.__get__(x, MyClass)  # func インスタンスは descriptor protocol "__get__" を持っている

print("func =", func)  # <function MyClass.f at 0x000001F2E20399E0>

print("bound =", bound)  # <bound method MyClass.f of <__main__.MyClass object at 0x0000019B4FE6C7A0>>
bound()  # <__main__.MyClass object at 0x0000019B4FE6C7A0>

MyClass.fx.f がは指し示すメモリアドレス違う。クラスの関数と、インスタンスの関数は function と bound method なので別オブジェクト。ただ、bound method が呼び出す function は同じなので以下のようにして比較すると等価。

print(x.f.__func__ is MyClass.f)  # True

これはつまり、bound method に対応した関数オブジェクトは __func__ でアクセスできるし、インスタンスオブジェクト自身は __self__ でアクセスできる。なので、メソッドの第一引数の名前は慣例的に self とつけるし、特別な意味はないとされるが double underscore でアクセスできる特別なアクセスが用意されている。名前を変えないほうがいい。

反復子 iterator

反復子のメカニズムを確認するためのコード:

class CountDown:
    def __init__(self, start):
        self.cur = start
    def __iter__(self):  # 自分自身が iterator
        return self
    def __next__(self):
        if self.cur <= 0:
            raise StopIteration
        val = self.cur
        self.cur -= 1
        return val

for v in CountDown(3):
    print("CountDown:", v)
print()
CountDown: 3
CountDown: 2
CountDown: 1

generator

iterator と generator の関係は似ている。同じ機能を実装できてしまうから。どこが違うのか整理しておく必要があります。

def reverse(data):
    for i in range(len(data) - 1, -1, -1):
        yield data[i]

for ch in reverse("golf"):
    print(ch)          # f, l, o, g の順に表示

コーディングのレベルだと yield が出てきたら generator になる。

それで、終了判定の StopIteration は自動的に送出される。簡潔に書ける generator と仕組みをリッチに仕上げるクラス(iterator)の住み分けがされていると思います。

細かい違いでは、generator のほうが高速になる可能性が高い。具体的な実装のルートが、組み込みの iterator になるので、(クラスによって設計した)純粋な python__next__ 実装をするよりも最適化されやすい。(シンプルな iterator+組み込み最適化)

なので peek() や reset() などを使って iterator の動きに独自機能/特殊仕様を載せない限りは generator のほうが簡潔に書けるし、処理も高速に処理されやすい。

generator 式

generator 式は、その場で "使い捨て" の generator を使うための記法。リスト化しない分、メモリに優しくなるケースがある。特徴として generator 式という名前だけど yield は式の中で使わない。

式の書き方は次のとおり:

( 式 for … [if 条件] … )

括弧で括っているけど、これは generator 式の特徴なんだけど、わりと省略されてしまう。以下のようなケース。sum() などの関数の中で generator 式が現れると括弧を省略してしまう)

sum(x*y for x,y in zip(xvec, yvec))

有用だけど、すこし読みづらい例:

words = set(w for line in page for w in line.split())

generator 式の中では for が左から右に見ていく必要がある。

for line in pagefor w in line.split() の2重ループがあって、最終的に得られるものが、一番最初の w になる。

  1. page から要素を取り出して line にする
  2. line に対して in line.split() で単語ごとの iterator
  3. iterator から単語を w として1つ取り出し、yield で単語が尽きるまで繰り返す
data = "golf"
list(data[i] for i in range(len(data)-1, -1, -1))

range(3, -1, -1) は start, stop, step の関係。3, 2, 1, 0... と降順に進んでいって stop は番兵で含まれない数字の特徴。なので 3,2,1,0-1 を含まない)まで。

generator 式の中で range を使うのは組み合わせになっていて、C# の for と感覚的にすこし違ってみえたので何が違うのかを整理しておく。

以下だと n は番兵で含まれないし、単純な range(n) の形はこれだと思う。なのでこれは問題ないと思う。

for (int i = 0; i < n; i++)

今回の for i in range(n-1, -1, -1) の降順にすると、引数2つ目の条件の -1 がすこし奇妙に見える。C# だと(私は)以下のように書くし、そう頭で考えているからだと思う。

for (int i = n - 1; i >= 0; i--)
// 以下のように考えていなかった
for (int i = n - 1; i > -1; i--)

C# や C 言語の for も初期値、条件、step の並び。range も初期値、終了値、step の並びで同じだと思う。ここは、慌てずに同じように条件を見ればいい。

ただ、終了条件は C 言語は i >= 0 のように停止する値を(私は)書いてしまう。python だと含まない数字の -1仕様として書かないといけない。その違いがあったので、感覚的に違和感があったのだと思う。

参考