
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.f の f は、関数オブジェクト 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.f と x.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 page と for w in line.split() の2重ループがあって、最終的に得られるものが、一番最初の w になる。
- page から要素を取り出して line にする
- line に対して in line.split() で単語ごとの iterator に
- 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 を仕様として書かないといけない。その違いがあったので、感覚的に違和感があったのだと思う。


