ゆくゆくは有へと

おかゆ/彼ノ∅有生 の雑記

括弧を憎みすぎた人間の末路(Pythonで関数合成)

本実装はともかく、ちょっと試しにprintしてみたいときあるじゃないですか。そういうときに、

print(f(g(h(1, 2))))

などと、括弧が多すぎて死にそうになることがある。

まあこれくらい我慢しろっても思うんだけど、ちょっと機嫌が悪くてブチ切れた

魔法少女になって括弧を消し去りたいッ!

せめて関数合成を

functoolsモジュールに関数合成のためのツールあるかなと思ったらなかった…作るしかない…。

というわけで作ったのがこれ。Composableクラス。

class Composable:
    def __init__(self, callable_obj):
        self.__callable = callable_obj

    def __call__(self, *args, **kwds):
        return self.__callable(*args, **kwds)

    def __matmul__(self, other):
        @Composable
        def composite_function(*args, **kwds):
            return self(other(*args, **kwds))
        return composite_function

    def __imatmul__(self, other):
        return self @ other
        
    def __getattr__(self, name):
        return getattr(self.__callable, name)

Composable を関数デコレータとして使うことで@演算子を2関数に使うことができるようになる。

@Composable
def add_10(n):
    return 10 + n

@Composable
def product_3(n):
    return 3 * n

print((add_10 @ product_3)(10)) # => 40

これで入れ子の括弧はかなり無くなる!嬉しさがある。

def chain(*func):
    composite = Composable(lambda x: x)
    for func in func_rest:
        composite @= Composable(func)
    return composite

print(chain(add_10, add_10, product_3)(3)) # => 29

ついでにこういうのも作っておいたので、これでなんとかしてもいい。

殲滅させたい

でも括弧を憎みすぎて、もっと括弧を減らしたかった。というわけで魔改造したのがこれ。

class Composable:
    def __init__(self, callable_obj):
        self.__callable = callable_obj

    def __call__(self, *args, **kwds):
        return self.__callable(*args, **kwds)

    def __matmul__(self, other):
        @Composable
        def composite_function(*args, **kwds):
            return self(other(*args, **kwds))
        return composite_function

    def __imatmul__(self, other):
        return self @ other

    def __rshift__(self, other):
        return other(self)

    def __rrshift__(self, other):
        return self(other)

    def __mod__(self, other):
        return self.apply(other)

    def apply(self, *applied_args, **applied_kwds):
        @Composable
        def applied_function(*args, **kwds):
            return self(*applied_args, *args, **applied_kwds, **kwds)
        return applied_function

    def __getattr__(self, name):
        return getattr(self.__callable, name)
@Composable
def xruti(func):
    return func()

@Composable
def add_10(n):
    return 10 + n

@Composable
def product_3(n):
    return 3 * n

@Composable
def add(n, m):
    return n + m

@Composable
def sub(n, m):
    return n - m

@Composable
def product(n, m):
    return n * m


print((sub % 200 @ add % 50 @ product % 4 @ add % 5)(10)) #(1)
print(10 >> add % 5 >> product % 4 >> add % 50 >> sub % 200) #(2)
print(sub % 5 % 1 >> xruti) #(3)

多分3日後の自分は理解できない

まず (1) はさっきと同じで関数合成してるけど、部分適用の % をぶち作った。

(2)では、>> を導入。

こういうときって、「引数にほにゃして、もにゃして、ぽちゃして、どみゃして」って思考なのに関数で書くと逆になるのがイヤ!

ならメソッドチェーンで書けばいいんだけど、Python は絶妙にメソッドチェーンできなくて辛いし…。

なので、>>に左の値を右の関数にぶち込んで値を返すということをしてもらって、括弧をなくしました。

(3) は % がちょっとポンコツで、全部値が埋まってても値を返さずになお関数オブジェクトを返してくるので、xruti関数(ロジバンで "return")を使って値を返すように改造してある。

「キーワード引数」?知らん!!!!!!!!!

お気を確かに

確かに関数で包みまくってると嫌になりますが、まあ流石に上はやり過ぎ感がある。

もう少し穏当にするならまあこんな感じだろうか。

from functools import reduce
def compose(callables):
    callables.reverse()
    def apply(*args, **kwds):
        accum = callables.pop(0)(*args, **kwds)
        return reduce((lambda x, f: f(x)), callables, accum)
    return apply
print(compose([product_3, add_10, product_3, add_10])(10)) #=> 210

引数はアンパックしてもいいかもね。まあこれくらいが平和かも

「実践Python3」の Factory method がもやもやする

題のごとし。

ググって理解したところによれば、factory method って「具象クラスごとに、同種手続き(たいてい抽象クラスで定義されてるメソッド)時に呼び出されるクラス可変的なコンストラクタをメソッド化したもの(で、具象クラス実装時に中身が書かれるもの)」っぽい。

で、「同種手続き」というのを抽象クラスのレベルで書いちゃうというのがどうやら template method パティーンらしい。ググった感じだと、factory method パティーンは template method パティーンに更に組み込んで使うっぽい。

んだけど、「実践Python3」の場合、この「同種手続き」というのに共通性がほとんどなくて、「えぇ…」ってなっちゃった。

たぶん、Board の上に駒を置いていく操作、populate_board が同種手続き(factory method パティーンでいうanOperation?)に相当しそうなんだけど、そもそも anOperation は具象クラスでオーバーライドしない(したら意味ないし)から、「???」って感じだ。

でも、create_piece は「ファクトリー関数」らしくて、これのおかげで、populate_board は「「ファクトリーメソッド」と呼ぶことができる」らしい。そうすると、同書においては、__init__ が anOperation に相当して、populate_board がファクトリーメソッドということになるのだろうか…。

Factory Method パターン - Wikipedia 見てたら、

factoryMethodは、[...] パラメータを取り、それによって生成するクラスを変えることもある。

ともあるので、それならむしろ create_piecefactoryMethod では……?

そう考えると、AbstractBoard はこうしたくなる(ちなみに個人的な趣味で ABC にしておいた)

from abc import ABCMeta, abstractmethod

class AbstractBoard(metaclass=ABCMeta):

    def __init__(self, rows, columns):
        self.rows = rows
        self.columns = columns
        self.board = [[None for _ in range(columns)] for _ in range(rows)]
        self.populate_board()

    def populate_board(self):
        for row in range(self.rows):
            for column in range(self.columns):
                self.board[row][column] = self.create_piece(row, column)

    @abstractmethod
    def create_piece(row, column):
        pass

    def __str__(self):
        ...

populate_board は各マスに対して、サブクラスで実装されたcreate_piece(row, column) を呼び出して相応しいインスタンスをもらえる。populate_boardanOperation に相当するはずで、個別の具象ボードにおいて共通のプロセス。

ほんで、ChessBoard 具象クラスでは

class ChessBoard(AbstractBoard):

    def __init__(self):
        super().__init__(8, 8)

    def create_piece(self, row, column):
        piece_dict = self._init_piece_map()
        name_dict = {PAWN: "ChessPawn", ROOK: "ChessRook",
                     KNIGHT: "ChessKnight", BISHOP: "ChessBishop",
                     KING: "ChessKing", QUEEN: "ChessQueen"}
        if (row, column) in piece_dict:
            kind, color = piece_dict[(row, column)]
            name = name_dict[kind]
            return globals()[color + name]()
        else:
            return None

    def _init_piece_map(self):
        colors = (((0, 1), BLACK), ((6, 7), WHITE))
        kinds = (((0, 7), ROOK), ((1, 6), KNIGHT), ((2, 5), BISHOP),
                 ((3,), QUEEN), ((4,), KING), ((0, 1, 2, 3, 4, 5, 6, 7), PAWN))
        piece_dict = {(row, column): (kind, color)
                      for row, color in colors for column, kind in kinds}
        return piece_dict

ヘルパーメソッドとして _init_piece_map を用意した。_init_piece_map で初期状態における各コマの座標の辞書を返してもらって、今の位置がその辞書にあれば相応のインスタンスを渡してあげるという形。

んん…でもこれならもういっそこうした方が…

from abc import ABCMeta, abstractmethod

class AbstractBoard(metaclass=ABCMeta):

    def __init__(self, rows, columns):
        self.rows = rows
        self.columns = columns
        self.board = [[None for _ in range(columns)] for _ in range(rows)]
        self.populate_board()

    def populate_board(self):
        for row in range(self.rows):
            for column in range(self.columns):
                self.board[row][column] = self.create_piece(row, column)

    def create_piece(self, row, column):
        piece_dict = self.initial_piece_map()
        name_dict = self.get_name_dict()
        if (row, column) in piece_dict:
            kind, color = piece_dict[(row, column)]
            name = name_dict[kind]
            return globals()[color + name]()
        else:
            return None

    @abstractmethod
    @staticmethod
    def initial_piece_map():
        pass

    @abstractmethod
    @staticmethod
    def get_name_dict():
        pass

    def __str__(self):
        ...


class ChessBoard(AbstractBoard):

    def __init__(self):
        super().__init__(8, 8)

    @staticmethod
    def get_name_dict():
        name_dict = {PAWN: "ChessPawn", ROOK: "ChessRook",
                     KNIGHT: "ChessKnight", BISHOP: "ChessBishop",
                     KING: "ChessKing", QUEEN: "ChessQueen"}
        return name_dict

    @staticmethod
    def initial_piece_map():
        colors = (((0, 1), BLACK), ((6, 7), WHITE))
        kinds = (((0, 7), ROOK), ((1, 6), KNIGHT), ((2, 5), BISHOP),
                 ((3,), QUEEN), ((4,), KING), ((0, 1, 2, 3, 4, 5, 6, 7), PAWN))
        piece_dict = {(row, column): (kind, color)
                      for row, color in colors for column, kind in kinds}
        return piece_dict

今回のテーマだと本質的に差異が生じるところって「どこにどんな駒を置くのか」なので、それを具象クラスで決めるという風にしたほうが簡潔そう…。特に、駒の位置情報を司るinitial_piece_map が分離するおかげで、外部ファイルからの読み込みに変更するのも簡単。

ただ、これが factory method パティーンなのかと言われると……。そもそもこのテーマ設定がファクトリーメソッドパティーンに向いてないのかしら。

しかし考えてみれば、population の仕方を抽象メソッドで固定してしまうというのはハードコーディングな気もする。population の仕方は同書のやり方もあるし、おかゆのやり方もあるわけで(たとえばおかゆのやり方は、列真ん中付近の無駄なところまで走査してるので、パフォーマンスが悪い)。そうなると、やはり @abstractmethod にするのは populate_board にしておくべきなのだろうか…。

いや、でも、仮にそうだったとしても、populate_board が factory method とは思えない。こいつは別に anOperation (この状況でいえば__init__) に対してクラス差異的にオブジェクトを生成してるわけではなくて、個別クラス特有的にインスタンスの内部状態を変更してるだけ。単なる素朴なオーバーライドにしか見えない。

もやもやする

追記

2016-11-29-15:20

えーっと。factory パターンというのがあるのを知った。理解によれば、「factoryクラスに指示(引数)を与えて、それに見合ったオブジェクトを持ってきてくれるというクールなやつをつくる」パティーンのことらしい。言い方としては「指示に見合ったクラスのインスタンスを作り出す」といったほうがいいか(関数でよくない…?シンプルなファクトリなら関数でもできる(create_pieceがまさにそう)けど、ファクトリの内部状態によって指示への従い方を変えていく…みたいなことをするなら確かにクラスである必要がありそう(でもファクトリってそんな複雑なことする(していい)のかな))。

個人的には router みも感じる。

これを踏まえると、なるほど、create_piece は確かに「ファクトリー関数」ではあっても factory method ではない気がしてきた。

じゃあ、問題はどれがanOperation で、factory method なのか。

サルでもわかる 逆引きデザインパターン 第2章 逆引きカタログ ロジック編 Factory/Factory Method(ファクトリ/ファクトリメソッド) をみる。

それでは、ファクトリパターンとファクトリメソッドパターンの違いは何でしょうか?

ファクトリパターンは、オブジェクトの生成処理だけでなく、どのオブジェクトを生成するかの判断もオブジェクトェクトの使用者から隠してくれるパターンです。 ファクトリパターンでは、生成するオブジェクトの種類の変更をファクトリの処理の中で動的に行います。

しかし、ファクトリパターンでは、生成するオブジェクトの種類が増えたり、生成処理手順が複雑化した場合、ファクトリ内の処理が冗長で複雑になってしまいます。 そこで登場するのがファクトリメソッドパターンです。

せやな。続き:

ファクトリメソッドパターンでは、生成するオブジェクトごとにファクトリを用意し、ファクトリに対して共通のスーパークラスを設けることで、オブジェクトの生成処理を柔軟に行います。 スーパークラスでは、オブジェクト生成に共通な処理の実装と、オブジェクトのnewを行うメソッドやオブジェクト固有の生成手順を抽象メソッドとして定義します。 サブクラスでは、抽象メソッドの実装(オブジェクトのnew)を行うだけです。 オブジェクトの生成はサブクラスで行い、サブクラスでは生成処理の差分のみを実装すればよいので、生成処理を簡略化できます(注4)。

ファクトリメソッドパターンは、1つのファクトリは1つのオブジェクトの生成のみを行うため、生成するオブジェクトの種類の変更を行う場合、ファクトリクラスを切り替える必要があります。 ファクトリメソッドパターンでは多くの場合、オブジェクトの使用者はファクトリのスーパークラスを使用します。 そしてファクトリ指定は、オブジェクト使用者の生成時にコンストラクタで渡したり、ファクトリ設定用メソッドを設けるなどの手段が必要になります。

やっぱりとにかくファクトリメソッドパターンというのは

  • 共通処理は抽象クラスで
  • 差分処理(たいていコンストラクタの指定)は具象クラスで

だと思うんだよな。で、ファクトリである以上、多分、クライアントに渡されるのはプロダクト(を加工したもの)であるべきで、さらにそのプロダクトというのはファクトリーメソッドによって可変的に持ってきたもの。

と考えたときに、やっぱり「実践Python3」のゲームボードの例は気持ちが悪い…。

なんか、もしかして「ファクトリ関数を中に含んだメソッド」をファクトリメソッドと呼んでるんじゃないかコイツ?という感がしてきた。

あ~ もやもやする

追記

16:50

これを Factory method パターンと理解しようとするのを諦めた。

で、こういうの見つけた

doloopwhile.hatenablog.com

あまりにも自由度が高くなってきたらクラスを引数で渡してやれっていう…えぇ…いやそりゃそうするけど……

なんか基本的に批判的な目で読んだほうがいいんかも分からんね(まあ元々がC++デザインパターンだし仕方ないけども)

python でリストのパターンマッチっぽい何か

「実践Python3」のコラム見て、ハッと閃いたのでメモ。(Python3.5)

シーケンスのアンパックは引数定義のところでよく使われますね。

def func(*args, **keywords):
  pass

みたいな感じで。アンパックはまだ使いみちがあって、入れ子になったシーケンスの内側にあるシーケンスをアンパックして平たくできます。

[1, 2, 3, *[4, 5, 6]]
# >> [1, 2, 3, 4, 5, 6]

ところで、Pythonではアンパック代入というのができて、・・・要はタプルの内部に変数を置けます(パターンマッチ的な代入?なんて言うのか分からない)

tpl = (1, 2, 3)
a, b, c = tpl # (a, b, c) = tpl に同じ
assert a == 1 and b == 2 and c == 3

で、このアンパック代入、対タプルならイミュータブルなので割と使えますが、ミュータブルなシーケンス(リストとか)だとふつうの使い方では使い勝手が悪い。

ここで*の出番です。

seq = [1, 2, 3, 4, 5]
x, *xs = seq
assert x == 1
assert xs == [2, 3, 4, 5]

アンパック代入時、最後に*を付した変数を入れておけば、余った部分は全部そこに収容されるので、多い日もあんしん!((少ないときはifして))

で、これ使うと Haskell みたいなリストの取り扱い方(x:xsみたいなの)ができそうだと思って、「すごいH本」4章の実装練習を Python でやってみました。

from typing import List, TypeVar, Tuple, Sequence, Any
from abc import ABCMeta, abstractmethod

T = TypeVar('T')
S = TypeVar('S')
CT = TypeVar('CT', bound=Comparable)

class Comparable(metaclass=ABCMeta):
    @abstractmethod
    def __lt__(self, other: Any) -> bool: pass
    @abstractmethod
    def __gt__(self, other: Any) -> bool: pass
    def __le__(self, other: Any) -> bool:
        return not self > other
    def __ge__(self, other: Any) -> bool:
        return not self < other

def sum_(xs: Sequence[float]) -> float:
    if xs:
        x, *xs = xs
        return x + sum(xs)
    else:
        return 0

def maximum_(xs: Sequence[T]) -> T:
    if xs:
        x, *xs = xs
        if xs:
            return max(x, maximum_(xs))
        else:
            return x
    else:
        raise ValueError("maximum of empty list!")

def take_(n: int, xs: Sequence[T]) -> List[T]:
    if n >= 0 and xs:
        x, *xs = xs
        return [x] + take_(n-1, xs)
    else:
        return []

def reverse_(xs: Sequence[T]) -> List[T]:
    if xs:
        x, *xs = xs
        return reverse_(xs) + [x]
    else:
        return []

def zip_(xs: Sequence[S], ys: Sequence[T]) -> List[Tuple[S, T]]:
    if xs and ys:
        x, *xs = xs
        y, *ys = ys
        return [(x, y)] + zip_(xs, ys)
    else:
        return []

def elem_(a: T, xs: List[T]) -> bool:
    if xs:
        x, *xs = xs
        if a == x:
            return True
        else:
            return elem_(a, xs)
    else:
        return False

def quicksort_(xs: List[CT]) -> List[CT]:
    if xs:
        x, *xs = xs
        smaller_or_equal = [a for a in xs if a <= x]
        larger = [a for a in xs if a > x]
        return quicksort_(smaller_or_equal) + [x] + quicksort_(larger)
    else:
        return []

という感じで。え? pop使えって?うるせーニシキヘビぶつけんぞ。とはいえ確かにジェネレータとかに対しては不向き(特に無限イテレータ)ですね。

とはいえ、ジェネレータで実装する場合はそもそも上のような書き方をしないのでは、とも思うので、もっぱらリスト向きの(トイな)書き方ではある。

ジェネレータで再帰するならyield fromを使うとよさそう

from typing import Iterable, Iterator

def reverse_2(xs: Iterable[T]) -> Iterator[T]:
    xs = iter(xs)
    x = next(xs)
    yield from reverse_2(xs)
    yield x

assert list(reverse_2(range(5))) == [4, 3, 2, 1, 0]

一周してジェネレータを積極的に使いたみというオチになってしまった。ちなみに、遅延評価の Haskell なら簡単にかけるrepeatPython ではジェネレータで書けます。

def repeat(x: T) -> Iterator[T]:
    yield x
    yield from repeat(x)

ジェネレータ便利やね。(全く同じ形式で、yield from repeat(x+1) とすればカウンターになるのも面白い)

ジェネレータ、ループにはwhile使うのが常套手段っぽいけど、再帰のほうがなんかかっこよくていいですよね(?)

P.S.

ところで、Comparable の抽象基底クラスってなんでないんやろ

__getattribute__とsuperのメモ

ディスクリプタについて勉強してから10か月くらい前か~

iuk.hateblo.jp

qiita.com

このQiita記事が公式のHowtoディスクリプタよりも詳しい&分かりやすくてよきよき。特に__getattribute__の中身の擬似コードあるのがいいよね。

Effective Python にはディスクリプタ使おうとして __getattribute__ のぬかるみにハマるなよ!って書いてあるけど、知らないに越したことないでしょ。良質なドキュメントさえあれば理解に難くもないし。

とりあえず、インスタンスの属性アクセスのときに使われる__getattribute__(objectクラスのやつ)

  1. 当該属性名をクラス属性(スーパークラス含め)から探し、それがデータディスクリプタなら__get__(obj, type(obj))呼び出し
  2. インスタンスの属性辞書(__dict__)から当該属性名を探し、あればそれをそのまま返す
  3. 1.の非データディスクリプタ
  4. エラー

次に、クラスの属性アクセスのときに使われる__getattribute__(typeクラスのやつ)*1

  1. 当該属性名をメタクラス属性から探して、それがデータディスクリプタなら__get__(cls, type(cls))呼び出し
  2. クラス(スーパークラス含め)の属性辞書から当該属性名を探し、それがディスクリプタなら__get__(None, cls)呼び出し、違うならそのまま返す。
  3. 1.の非データディスクリプタ
  4. エラー

多分、setの方もこれと同じだと思う。

で、あとは各々のディスクリプタ__get____set__が色々してくれる。おわり!

すーぱー

superクラスはまた別の__getattribute__を持っているらしい。

2. 組み込み関数 — Python 3.5.2 ドキュメント

これも上のリンクから擬似コードをもってきますと、

def super_getattribute(su, key):
    "Emulate super_getattro() in Objects/typeobject.c"
    starttype = su.__self_class__
    mro = iter(starttype.__mro__)
    for cls in mro:
        if cls is su.__self_class__:
            break
    # Note: mro is an iterator, so the second loop
    # picks up where the first one left off!
    for cls in mro:
        if key in cls.__dict__:
            attr = cls.__dict__[key]
            if hasattr(attr, '__get__'):
                return attr.__get__(su.__self__, starttype)
            return attr
    raise AttributeError

メソッド内で呼び出した場合は、superを呼び出したクラスの__mro__自分自身以降の属性辞書を探索して、それがディスクリプタ(大抵は__init__とか上位クラスのメソッドを呼ぶわけで、つまり大体の場合でディスクリプタ)なら、呼び出したところのインスタンスとクラスで__get__を呼び出す(__init__selfに呼び出した時点のインスタンスを束縛したバウンドメソッドを返すわけですな)。

もちろん、親メソッドを連鎖させるためには、それぞれのクラスがsuperで親クラスを呼び出していくことで実現する。

なんか Python2 だとあんまいい感じに動かない仕様だったらしく、Python3 使っててよかった~って感じだ。

ところで、さっきの擬似コードと少し挙動の異なるところを見つけたのでそれを調べる。確かめたいのは次の2つ:

  • __get__ の引数はこれで本当にあってるのか
  • superが第1引数に取ったクラスはどこで使われてるのか(上の擬似コードだとこれは不使用)
class Descriptor():
    def __init__(self, value):
        self.value = value

    def __get__(self, obj, obj_cls=None):
        return (obj, obj_cls, self.value)


class C():
    def __init__(self):
        super().__init__()

    x = Descriptor("C")


class D(C):
    def __init__(self):
        super().__init__()

    x = Descriptor("D")


class D2(C):
    def __init__(self):
        super().__init__()

    x = Descriptor("D2")


class E(D, D2):
    def __init__(self):
        su = super()
        su.__init__()

    x = Descriptor("E")


class F(E):
    def __init__(self):
        super().__init__()

    x = Descriptor("F")

if __name__ == '__main__':
    print("F mro :", F.__mro__)
    print("E mro :", E.__mro__)
    print("D mro :", D.__mro__)
    print("D2 mro :", D2.__mro__)
    print("C mro :", C.__mro__)
    print()
    print("F() :", F().x)
    print("super(F, F()) :", super(F, F()).x)
    print("super(E, F()) :", super(E, F()).x)
    print("super(D, F()) :", super(D, F()).x)
    print("super(D2, F()) :", super(D2, F()).x)
> python func_annotations.py
F mro : (<class '__main__.F'>, <class '__main__.E'>, <class '__main__.D'>, <class '__main__.D2'>, <class '__main__.C'>, <class 'object'>)
E mro : (<class '__main__.E'>, <class '__main__.D'>, <class '__main__.D2'>, <class '__main__.C'>, <class 'object'>)
D mro : (<class '__main__.D'>, <class '__main__.C'>, <class 'object'>)
D2 mro : (<class '__main__.D2'>, <class '__main__.C'>, <class 'object'>)
C mro : (<class '__main__.C'>, <class 'object'>)

F() : (<__main__.F object at 0x000001C4756C4470>, <class '__main__.F'>, 'F')
super(F, F()) : (<__main__.F object at 0x000001C4756C4470>, <class '__main__.F'>, 'E')
super(E, F()) : (<__main__.F object at 0x000001C4756C4470>, <class '__main__.F'>, 'D')
super(D, F()) : (<__main__.F object at 0x000001C4756C4470>, <class '__main__.F'>, 'D2')
super(D2, F()) : (<__main__.F object at 0x000001C4756C4470>, <class '__main__.F'>, 'C')

すべてのクラスが非データディスクリプタであるxをもっています。super第二引数は最下層のF()に固定して、第一引数を色々動かして、xにアクセスしてみました。もちろん、isinstance(F(), cls)の範囲内で。

Descriptorは見てもらえばその通りですが、__get__の引数と、ディスクリプタ初期化の際に入れた文字列(それぞれのクラス名にしてある)のタプルを返してくれます。

まず、擬似コードにおける__get__の引数は間違ってないようです。それぞれ obj, obj_cls に渡されるのは、super第二引数のオブジェクトsu.__self__と、そのクラスsu.__self_class__です。

一方で、呼び出されているディスクリプタsuper第一引数によって変化しています!第一引数とディスクリプタの所属するクラスを比べてみると、どうやらsuper第一引数は擬似コードにおけるstarttypeに相当しそうです(もちろん、__get__の第二引数にはもはやstarttypeは使えず、su.__self_class__を代入するように書き換える必要がありますが)。つまり、属性辞書の探索は、第二引数オブジェクトのクラス以降ではなく、super第一引数以降ということになりそうです。

というわけで、変更すべき箇所はこのあたりです:

def super_getattribute(su, key):
    "Emulate super_getattro() in Objects/typeobject.c"
    starttype = su.__self_class__
    mro = iter(starttype.__mro__)
    for cls in mro:
        if cls is su.__self_class__:
            break

superオブジェクトがどんな属性を持っているのか、日本語のドキュメントが見つからなくてムムムしていましたが、どうやらsuper第一引数は__thisclass__に収納されてるようです。よって、まず、starttype = su.__thisclass__とすべきです。

さらに、__thisclass__starttype とした場合、mro についても変更すべきです。属性辞書の探索はmroに従って行われるわけですが、もしmrostarttype.__mro__を元につくられるとしたら、super(D, F()).x(下から2行目)がD2のディスクリプタに辿り着くことは不可能だからです(D.__mro__参考)。というわけで、mro = iter(su.__self_class__.__mro__) とすべきです。

さらに、イテレータの巻き上げは、su.__self_class__ ではなく、 starttype で止めるべきですね。

というわけで、恐らくより正しい擬似コードは以下の通りです:

def super_getattribute(su, key):
    "Emulate super_getattro() in Objects/typeobject.c .. and revised by iuk."
    starttype = su.__thisclass__
    mro = iter(su.__self_class__.__mro__)
    for cls in mro:
        if cls is starttype:
            break
    # Note: mro is an iterator, so the second loop
    # picks up where the first one left off!
    for cls in mro:
        if key in cls.__dict__:
            attr = cls.__dict__[key]
            if hasattr(attr, '__get__'):
                return attr.__get__(su.__self__, su.__self_class__)
            return attr
    raise AttributeError

super第一引数は mro の巻き上げに使われてたってことですね。

というわけで、super のもう少し詳しい挙動が分かりました。

super(type, obj)の属性アクセスは、objのクラスの__mro__に従って、type以降(typeは含まない)のクラスの属性辞書を順に探索し、それがディスクリプタなら__get__(obj, obj.__class__) を呼び出し、ディスクリプタでないならそのまま返す。

まあディスクリプタのあたりの挙動は他と特に変わらないですね。ちなみにsuper(fromtype, type)の属性アクセスの場合は __get__(None, type) になります。

*1:オブジェクトの属性アクセスはそのオブジェクトのクラスの__getattribute__に支配されるわけで、つまりオブジェクトがクラスの場合はメタクラスの__getattribute__。

関数がディスクリプタであることを利用してインスタンスに動的にメソッドを追加する

だからタイトル長くない?

kk6.hateblo.jp

types.MethodType 使えば個々のインスタンスにメソッドを追加できるんだけど、typesをわざわざ import するのもなって感じにもなる。

で、

qiita.com

を読んでたら、関数オブジェクト(こいつは非データディスクリプタ)の__get__内でtypes.MethodTypeがよしなに呼び出されているのを見つけたので、これを使えばいいなと思いました。

class Klass():
    def __init__(self):
        self.value = 10


def meth(self, value):
    self.value += value
    return self.value


if __name__ == '__main__':
    c = Klass()
    c.meth = meth.__get__(c)
    print(c.value) # => 10
    print(c.meth(5)) # => 15
    print(c.__dict__) # => {'meth': <bound method meth of <__main__.Klass object at 0x000001994F40D9E8>>, 'value': 15}

ちょっと面白いのは、当たり前だけど、こうやって追加すると、クラスに定義されたメソッドではないから、インスタンスの属性辞書に直接メソッド(バウンドメソッド)が登録されるってことですね。まあ逆に言えば、いつもはインスタンスの属性辞書にメソッドは登録されておらず、あくまでクラスの属性を引っ張ってきてる(そのときにディスクリプタによってselfがバウンドされる)ってことですね。先にバウンドしておくか、呼び出し時にその都度バウンドしてもらうかってところが異なります。

ところで、メソッド(バウンドメソッドも)は新たに属性を加えられないっぽいですが、関数オブジェクトには色々属性を追加できます。

    Klass.hoge.atr = 100
    print(Klass.hoge.atr)
    print(c.hoge.atr) # => 100

クラスでメソッドを定義した場合、クラスからアクセスすればそれはただの関数オブジェクトなので、上の例みたいに新たに属性を追加できますが、インスタンスにバウンドメソッドをもたせた場合はそういうことができません(まあそんな使う性質でもないし、ほとんど差はないって思っていいとは思うけど)。

propertyはデータディスクリプタだからインスタンス属性辞書より優先される

タイトル長くない?

class C():
    def __init__(self):
        self.x = 10

    @property
    def x(self):
        return self.__dict__["x"]

    @x.setter
    def x(self, value):
        print("eh-eh")
        self.__dict__["x"] = value

データディスクリプタインスタンス属性辞書よりもアクセスが優先されるので、上のように書いてもC().xC().__dict__["x"]ではなく、type(obj).__dict__["x"].__get__(obj, type(obj)) に繋がる表現になってくれる。

(単純なフックとして使われてる(?))プロパティ内で用いる属性変数は大抵アンダースコア1つ付けたものにするけど、それだと直接その変数に(比較的簡便に)アクセスできちゃうし、こっちのほうがより隠されてる感はある(単純な属性アクセスではディスクリプタが優先され、無理やりアクセスしようとする場合には__dict__直接覗かないといけないという意味で)