Last Updated: 2/6/2024, 5:44:57 AM

# 誰がメソッドから関数に
変換しているの?

__get__ メソッド

前のページではメソッドが呼び出されると関数に変換されていることを 見てきました。

では、誰が変換を行なっているのでしょうか? それは __get__ メソッドです。

Hello, world!

# 疑問: __get__ メソッドってなに?

関数は __get__ メソッドを持っています。

def f():
    pass

hasattr(f, '__get__')
>>> hasattr(f, '__get__')
True
>>> 

だからなに?って感じなのですが __get__ メソッドは、 属性参照  された  時に呼び出されるという不思議な動作します。 以下のコードをコピペして実行してみてください。

# インタラクティブシェルに
# コピペして実行して見てください。
class C:
    def __get__(self, obj, objtype):
        print(self)
        print(obj)
        print(objtype)
        return 'Hello, world!'

class D:
    c = C()

d = D()

# インスタンス束縛
#     d.c 属性を参照しただけで
#     C.__get__ メソッドが実行されます。
print(d.c)

# クラス束縛
#     D.c 属性を参照した時にも
#     C.__get__ メソッドが実行されます。
print(D.c)
>>> # インスタンス束縛
... #     d.c 属性を参照しただけで
... #     C.__get__ メソッドが実行されます。
... print(d.c)
<__main__.C object at 0x105293198>
<__main__.D object at 0x105293160>
<class '__main__.D'>
Hello, world!
>>> 
>>> # クラス束縛
... #     D.c 属性を参照した時にも
... #     C.__get__ メソッドが実行されます。
... print(D.c)
<__main__.C object at 0x105293198>
None
<class '__main__.D'>
Hello, world!
>>> 

インスタンス束縛 (Instance Binding)
オブジェクトインスタンスへ束縛すると、a.x は呼び出し type(a).__dict__['x'].__get__(a, type(a)) に変換されます。

クラス束縛 (Class Binding)
クラスへ束縛すると、A.x は呼び出し A.__dict__['x'].__get__(None, A) に変換されます。

3.3.2.3. デスクリプタの呼び出し - Python 言語リファレンス (opens new window)

Python では この不思議な動作をする __get__ メソッドを使って、 メソッドから関数への変換を行なっています。 関数は __get__ メソッドを定義したクラスです。

ちなみに属性参照  した  ときに動作する __getattr__ というものもあります。 __get____getattr__ を区別すると理解が速いかなと思います。

# 対話モード >>> に
# コピペで動きます。
class C:
    def __getattr__(self, attr):
        print('Hello, world!')
        return attr

o = C()
o.a
>>> o.a
Hello, world!
'a'
>>>

# ◯ まとめ

特殊メソド __get__ が起動するのは属性参照 された ときです。 逆に属性参照 した ときに起動する __getattr__ という特殊メソッドもあります。 属性参照されたとき、というのを頭の片隅に置いておくと良いかなと思います。 Python では、この __get__ メソッドを使って メソッドから関数への変換を行なっています。

ポイント

__get__ メソッドは属性参照 された ときに呼び出される。

# 実験: メソッドを作ろう

# ◯ 疑似コード

関数オブジェクト function の __get__ メソッドを次のように定義することで、メソッドを簡単に実装できます。 このコードは、CPython の C 言語のコードを Python で書き直した疑似コードだそうです。

純粋な Python では、メソッドはこのように動作します。
In pure python, it works like this:

class Function(object):
    ...
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return types.MethodType(self, obj)

本当に疑似コードと同じような動作をするのでしょうか? ディスクリプタとクロージャを組み合わせて、 ごく簡単ではありますが、同じことができることを示してみたいと思います。

# ◯ 疑似コードを再現してみる。

__get__ メソッドの動作を、なんとなく理解するためにサンプルコードを用意しました。 インタラクティブシェルにペチペチ コピペできるようにしてあります。 各 Step のコードをコピペして例外がでる箇所をなんとなく眺めて見ていただければと思います。

# Step 0. メソッドは動的に追加できます。

class GirlFriend(object):
    def __init__(self, name):
        self.name = name


def change_name(person, name):
    person.name = name


GirlFriend.change_name = change_name


girl_friend = GirlFriend('岩倉玲音')

# 0.1. 関数呼び出し
GirlFriend.change_name(girl_friend, 'サーバルちゃん')
print(girl_friend.name)

# 0.2. メソッド呼び出し
girl_friend.change_name('バラライカ')
print(girl_friend.name)
>>> # 0.1. 関数呼び出し
... GirlFriend.change_name(girl_friend, 'サーバルちゃん')
>>> print(girl_friend.name)
サーバルちゃん
>>> 
>>> # 0.2. メソッド呼び出し
... girl_friend.change_name('バラライカ')
>>> print(girl_friend.name)
バラライカ
>>> 

# Step 1. 問題

ここで問題です。 関数をラップした Function クラスでを、関数、そしてメソッドとして使えるようにするにはどうすればよいでしょうか?

class GirlFriend(object):
    def __init__(self, name):
        self.name = name


def change_name(person, name):
    person.name = name


class Function(object):
    def __init__(self, function):
        self.function = function


GirlFriend.change_name = Function(change_name)


girl_friend = GirlFriend('岩倉玲音')

# 1.1. 関数呼び出し <--- そもそも、関数呼び出しができない。
GirlFriend.change_name(girl_friend, 'サーバルちゃん')
print(girl_friend.name)

# 1.2. メソッド呼び出し <--- メソッド呼び出しも当然できない。
girl_friend.change_name('バラライカ')
print(girl_friend.name)
>>> # 1.1. 関数呼び出し <--- そもそも、関数呼び出しができない。
... GirlFriend.change_name(girl_friend, 'サーバルちゃん')
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: 'Function' object is not callable
>>> print(girl_friend.name)
岩倉玲音
>>> 
>>> # 1.2. メソッド呼び出し <--- メソッド呼び出しも当然できない。
... girl_friend.change_name('バラライカ')
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: 'Function' object is not callable
>>> print(girl_friend.name)
岩倉玲音
>>> 

# Step 2. まずは関数として使えるようにする。

Function クラスのインスタンスオブジェクトは、関数として呼び出せません。 インスタンスオブジェクトを関数として呼び出せるようにするには __call__ メソッドをクラスで定義するとできるようになります。

class GirlFriend(object):
    def __init__(self, name):
        self.name = name


def change_name(person, name):
    person.name = name


class Function(object):
    def __init__(self, function):
        self.function = function
    
    def __call__(self, *args, **kwargs):
        return self.function(*args, **kwargs)


GirlFriend.change_name = Function(change_name)


girl_friend = GirlFriend('岩倉玲音')

# 2.1. 関数呼び出し
GirlFriend.change_name(girl_friend, 'サーバルちゃん')
print(girl_friend.name)

# 2.2. メソッド呼び出し <--- メソッド呼び出しができない。
girl_friend.change_name('バラライカ')
print(girl_friend.name)
>>> # 2.1. 関数呼び出し
... GirlFriend.change_name(girl_friend, 'サーバルちゃん')
>>> print(girl_friend.name)
サーバルちゃん
>>> 
>>> # 2.2. メソッド呼び出し <--- メソッド呼び出しができない。
... girl_friend.change_name('バラライカ')
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 6, in __call__
TypeError: change_name() missing 1 required positional argument: 'name'
>>> print(girl_friend.name)
サーバルちゃん
>>> 

__call__ の実践的な使い方についても Effective Python の 「3 章クラスと継承 項目23: 単純なインタフェースにはクラスの代わりに関数を使う」に書かれていました。

# Step 3. メソッドとしても使えるように

__get__ メソッドをクラスで定義するとできるようになります。

class GirlFriend(object):
    def __init__(self, name):
        self.name = name


def change_name(person, name):
    person.name = name


class Function(object):
    def __init__(self, function):
        self.function = function
    
    def __call__(self, *args, **kwargs):
        return self.function(*args, **kwargs)
    
    def __get__(self, obj, objtype):
        def method(*args, **kwargs):
            return self(obj, *args, **kwargs)
        return method


GirlFriend.change_name = Function(change_name)


girl_friend = GirlFriend('岩倉玲音')

# 3.1. 関数呼び出し <--- 今度は関数呼び出しができなくなった。
GirlFriend.change_name(girl_friend, 'サーバルちゃん')
print(girl_friend.name)

# 3.2. メソッド呼び出し
girl_friend.change_name('バラライカ')
print(girl_friend.name)
>>> # 3.1. 関数呼び出し <--- 今度は関数呼び出しができなくなった。
... GirlFriend.change_name(girl_friend, 'サーバルちゃん')
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 10, in method
  File "<stdin>", line 6, in __call__
TypeError: change_name() takes 2 positional arguments but 3 were given
>>> print(girl_friend.name)
岩倉玲音
>>> 
>>> # 3.2. メソッド呼び出し
... girl_friend.change_name('バラライカ')
>>> print(girl_friend.name)
バラライカ
>>> 

__get__ メソッドが、すこし厄介です。 __get__ メソッドは、method メソッドを返しています。 この method メソッドは、self と obj を持っています。

    def __get__(self, obj, objtype):
        def method(*args, **kwargs):
            return self(obj, *args, **kwargs)
        return method

これは関数閉包、クロージャと呼ばれるものです。 以下の記事でまとめさせて頂きました。 このあと staticmethod と classmehotd を実装していただく際に、 デコレータの知識が必要になります。

また obj には、メソッドを呼び出したオブジェクトが代入されます。 それが __get__ メソッドのポイントかなと思います。

# ディスクリプタ __get__ メソッドを使えば
girl_friend.change_name('バラライカ')
# ^^^^^^^^^ ここの頭の部分を参照できる

# Step 4. メソッドとしても関数としても使えるように

なぜ関数呼び出しができなくなったかというと関数呼び出しの時にも __get__ メソッドが呼ばれてしまっているからです。 そのため少し細工をしてあげる必要があります。

class GirlFriend(object):
    def __init__(self, name):
        self.name = name


def change_name(person, name):
    person.name = name


class Function(object):
    def __init__(self, function):
        self.function = function
    
    def __call__(self, *args, **kwargs):
        return self.function(*args, **kwargs)
    
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        
        def method(*args, **kwargs):
            return self(obj, *args, **kwargs)
        return method


GirlFriend.change_name = Function(change_name)


girl_friend = GirlFriend('岩倉玲音')

# 4.1. 関数呼び出し
GirlFriend.change_name(girl_friend, 'サーバルちゃん')
print(girl_friend.name)

# 4.2. メソッド呼び出し
girl_friend.change_name('バラライカ')
print(girl_friend.name)
>>> # 4.1. 関数呼び出し
... GirlFriend.change_name(girl_friend, 'サーバルちゃん')
>>> print(girl_friend.name)
サーバルちゃん
>>> 
>>> # 4.2. メソッド呼び出し
... girl_friend.change_name('バラライカ')
>>> print(girl_friend.name)
バラライカ
>>> 

# Step 5. 疑似コードと同じコードに

これでやっと疑似コードと同じようなコードが出てきました。

import types

class GirlFriend(object):
    def __init__(self, name):
        self.name = name


def change_name(person, name):
    person.name = name


class Function(object):
    def __init__(self, function):
        self.function = function
    
    def __call__(self, *args, **kwargs):
        return self.function(*args, **kwargs)
    
    def __get__(self, obj, objtype):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return types.MethodType(self, obj)

GirlFriend.change_name = Function(change_name)


girl_friend = GirlFriend('岩倉玲音')

# 4.1. 関数呼び出し
GirlFriend.change_name(girl_friend, 'サーバルちゃん')
print(girl_friend.name)

# 4.2. メソッド呼び出し
girl_friend.change_name('バラライカ')
print(girl_friend.name)
>>> # 4.1. 関数呼び出し
... GirlFriend.change_name(girl_friend, 'サーバルちゃん')
>>> print(girl_friend.name)
サーバルちゃん
>>> 
>>> # 4.2. メソッド呼び出し
... girl_friend.change_name('バラライカ')
>>> print(girl_friend.name)
バラライカ
>>> 

MethodType は、ユーザ定義クラスのインスタンスのメソッドの型です。

import types

class GirlFriend(object):
    def change_name(self, name):
        self.name = name

girl_friend = GirlFriend()
isinstance(girl_friend.change_name, types.MethodType)  # True

types.MethodType (opens new window)
ユーザー定義のクラスのインスタンスのメソッドの型です。

ポイント

メソッドから関数への変換には
__get__ メソッドが使われている。

# 実験: classmethod と staticmethod を実装してみよう

これまでディスクリプタの大体の動作を把握しました。 組み込み型の classmethod と staticmethod は、 ディスクリプタを使って実装されています。

# ◯ 情報工学実験

 ディスクリプタを使い組み込み型の classmethod と staticmethod を実装してください。  具体的には以下のサンプルコードの __get__ メソッドを実装してください。

実装にあたってはデコレータとクロージャの知識が必要になります。 デコレータとクロージャについては以下の記事で記述させていただきました。

実装にあたっては上記 Step 4 のコードを書き換えながら、 実装するような流れになるかなと思います。 Step 4 のコードをデコレータを使うと、以下のようになります。

# 対話モード >>> に
# コピペで実行できます。
class Function(object):
    def __init__(self, function):
        self.function = function
    
    def __call__(self, *args, **kwargs):
        return self.function(*args, **kwargs)
    
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        
        def method(*args, **kwargs):
            return self(obj, *args, **kwargs)
        return method


class GirlFriend(object):
    def __init__(self, name):
        self.name = name
    
    @Function
    def change_name(self, name):
        self.name = name


girl_friend = GirlFriend('岩倉玲音')

# 4.1. 関数呼び出し
GirlFriend.change_name(girl_friend, 'サーバルちゃん')
print(girl_friend.name)

# 4.2. メソッド呼び出し
girl_friend.change_name('バラライカ')
print(girl_friend.name)

# ◯ 解答

解答となるコードは、こちらにご用意させていただきました。

公式マニュアルに疑似コードが書かれているので、それを参考にしました。

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc
class StaticMethod(object):
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f
ポイント

お疲れ様でした。

# 用語: ディスクリプタ

__get__ メソッドを定義したクラスのことを難しい言葉で 「ディスクリプタ」と言います。 関数は __get__ メソッドを持っているので「ディスクリプタ」です。

このメソッドは、 __get__(), __set__(), および __delete__() です。 これらのメソッドのいずれかが、オブジェクトに定義されていれば、 それはデスクリプタと呼ばれます。
デスクリプタ HowTo ガイド (opens new window)

関数はディスクリプタの中でも 「非データディスクリプタ」と呼ばれるものに該当します。

あるオブジェクトが __get__()__set__() の両方を定義していたら、それはデータデスクリプタとみなされます。 __get__() だけを定義しているデスクリプタは、 非データデスクリプタと呼ばれます (これらは典型的にはメソッドに使われますが、他の使い方も出来ます)。
デスクリプタ HowTo ガイド (opens new window)

Python のオブジェクト指向機能は、関数に基づく環境の上に構築されています。 非データデスクリプタを使って、 この 2 つは(関数とメソッドは)シームレスに組み合わされています。
デスクリプタ HowTo ガイド (opens new window)

ちなみに __getattr__ を定義したクラスは、ディスクリプタとは呼ばれていません。

ポイント

関数は、非データディスクリプタです。

# 実験: メソッドを変更してみよう

メソッドを変更したときの挙動を、ちょっと復習してみます。 クラスオブジェクトの属性に関数を代入すると 配下のインスタンスオブジェクトのメソッドも全て変更されます。 これはなぜでしょうか?

#
# こんなクラスを作りました。
#
class GirlFriend:    
    def change_name(self, name):
        self.name = name

girl_friend_a = GirlFriend()
girl_friend_a.change_name('サーバルちゃん')
print(girl_friend_a.name)  # サーバルちゃん

girl_friend_b = GirlFriend()
girl_friend_b.change_name('岩倉玲音')
print(girl_friend_b.name)  # 岩倉玲音


#
# メソッドを変更します。
def new_change_name(self):
    self.name = 'バラライカ'

GirlFriend.change_name = new_change_name


# メソッドを変更した後にインスタンス化したオブジェクトの
# メソッドは変更できています。
girl_friend_c = GirlFriend()
girl_friend_c.change_name()
print(girl_friend_c.name)  # 'バラライカ'

# メソッドを変更する前にインスタンス化したオブジェクトの
# メソッドも変更できています。
girl_friend_a.change_name()
girl_friend_b.change_name()
print(girl_friend_a.name)  # 'バラライカ'
print(girl_friend_b.name)  # 'バラライカ'

何故なら、メソッドを呼び出されるとその度に、 クラスオブジェクトの関数に インスタンスオブジェクトを代入して実行しているだけからです。 当たり前といえば当たり前なのですが。

# これが呼び出されると
girl_friend.change_name('岩倉玲音') 

# 自動的にこれが呼びされている。
GirlFriend.change_name(girl_friend, '岩倉玲音') 

関数オブジェクトからインスタンスメソッドオブジェクトへの変換は、 インスタンスから属性が取り出されるたびに行われます。
3.2. 標準階層 - Python 言語リファレンス (opens new window)

# ◯ インスタンスメソッドオブジェクト

インスタンスメソッドオブジェクトとは、メソッドです。

関数オブジェクトからインスタンスメソッドオブジェクトへの変換は、 インスタンスから属性が取り出されるたびに行われます。
3.2. 標準階層 - Python 言語リファレンス (opens new window)

メソッドもインスタンスオブジェクトです。 このことは、メソッドが変数に代入できることからもわかります。 メソッドもインスタンスオブジェクトであることを強調するためにこのように表現しているものと思われます。

# Step1. インスタンスオブジェクトの生成
change_name_method = girl_friend.change_name

# Step2. メソッドの呼び出し
change_name_method('岩倉玲音')

# Step3. 実際には自動的にこれが呼びされている。
change_name_method.__func__(
    change_name_method.__self__, '岩倉玲音')

# Step4.
# __func__ は GirlFriend.change_name を
# __self__ は girl_friend オブジェクトを
# それぞれ参照している。
change_name_method.__func__ is GirlFriend.change_name
change_name_method.__self__ is girl_friend

インスタンスメソッドオブジェクトが呼び出される際、
根底にある関数 (__func__) が呼び出されます。
このとき、クラスインスタンス (__self__)
引数リストの先頭に挿入されます。
3.2. 標準階層 - Python 言語リファレンス (opens new window)

インスタンスを通してメソッド (クラスの名前空間内で定義された関数) にアクセスすると、 特殊なオブジェクトが得られます。それは束縛メソッド (bound method) オブジェクトで、 インスタンスメソッド (instance method) とも呼ばれます。

呼び出された時、引数リストに self 引数が追加されます。 束縛メソッドには 2 つの特殊読み出し専用属性があります。

m.__self__ はそのメソッドが操作するオブジェクトで、 m.__func__ はそのメソッドを実装している関数です。 m(arg-1, arg-2, ..., arg-n) の呼び出しは、 m.__func__(m.__self__, arg-1, arg-2, ..., arg-n) の呼び出しと完全に等価です。

4.12.4. メソッド - Python 標準ライブラリ (opens new window)

ちなみに Python 2 の頃はクラスオブジェクトの関数は unbound method と呼ばれていました。 Python 3 では単純に関数 function になりました。bound 束縛しているのは self を指しています。

>>> # Python 3
>>> girl_friend.change_name
<bound method GirlFriend.change_name ...>
>>> 
>>> GirlFriend.change_name
<function GirlFriend.change_name
>>>
>>> # Python 2
>>> girl_friend.change_name
<bound method GirlFriend.change_name ...>
>>> 
>>> GirlFriend.change_name
<unbound method GirlFriend.change_name>
>>>

Python 2 の unbound method は、そのクラスのインスタンスだけが、 第一引数に代入できるように限定された method (関数)です。

しかし、特定のクラスのインスタンスに限定しても、 コーディングをする上であまり意味がないことがわかり、 Python 3 では削除されました。

すべてをファーストクラスに - The History of Python.jp (opens new window)

Python3000 では、非束縛メソッドの考え方が削除され、 "A.spam" という式は、通常の関数オブジェクトを返すようになった。

結局、最初の引数をAのインスタンスの限定しても、 問題の分析の役に立つことはあまりない、ということになったのである。

逆に高度な使い方を使用とした場合に邪魔になることも多かったからである。 この高度な使い方というのは「duck typing self」という適切な名前で呼ばれている。

# ◯ まとめ

オブジェクトからメソッドを呼び出すと、 その都度、関数からメソッドへの変換が行われます。 生成されたメソッドの属性には、 呼び出したオブジェクトと関数が代入されています。

# 参照するだけでインスタンスメソッドオブジェクトが生成される。
change_name_method = girl_friend.change_name

マニュアルで「変換」という言葉が使われています。 これは作っておいたインスタンスメソッドをオブジェクトを単純に「切替」なのでしょうか? それとも参照される度にわざわざ新しくインスタンスメソッドオブジェクトを「生成」しているのでしょうか?

関数オブジェクトからインスタンスメソッドオブジェクトへは変換は、 インスタンスから属性が取り出されるたびに行われます。
3.2. 標準階層 - Python 言語リファレンス (opens new window)

ポイント

メソッドの呼び出しは、なんだか重そう...

# 疑問: 関数からメソッドに変換って、なにをしているの?「生成」それとも「切替」?

答え: 新しいインスタンスメソッドオブジェクトが、その都度「生成」されています(インスタンス化されています)。

# ◯ メソッドの呼び出しを高速化してみる。

最初からメソッドを変数に代入しておくと少しだけ処理を速くできます。 なぜ、速くなるかと言うと、属性を参照するたびに実行される関数からメソッドへの変換という処理を、省略できるからです。

# 変数の参照
python -m timeit -s "pop=[i for i in range(10000000)].pop" "pop()"

# メソッドの参照
python -m timeit -s "lst=[i for i in range(10000000)]" "lst.pop()"
$ # 変数の参照
$ python -m timeit -s "pop=[i for i in range(10000000)].pop" "pop()"
10000000 loops, best of 3: 0.0792 usec per loop
$
$ # メソッドの参照
$ python -m timeit -s "lst=[i for i in range(10000000)]" "lst.pop()"
10000000 loops, best of 3: 0.115 usec per loop
$

なお、関数オブジェクトからインスタンスメソッドオブジェクトへの変換は、インスタンスから属性が取り出されるたびに行われます。 場合によっては、属性をローカル変数に代入しておき、そのローカル変数を呼び出すようにするのが効果的な最適化になります。
3.2. 標準階層 - Python 言語リファレンス (opens new window)

可読性が落ちるので使っていいのか疑問だったのですが copy モジュールの中で、 このような実装をしているところを見かけました。このコードは list を deepcopy するコードです。

def _deepcopy_list(x, memo, deepcopy=deepcopy):
    y = []
    memo[id(x)] = y
    append = y.append
    for a in x:
        append(deepcopy(a, memo))
    return y

ただ、同じ copy モジュールの中でもこのような書き方がなされていないところもあります。 実行時間を計測して、どのくらい効果が見込めるのかと、可読性を天秤にかける必要があるかなと思います。

deepcopy は遅いんやって stackoverflow で文句を言われてるのを見かけました。 自分も昔使っていて、なんか重いなって印象がありました。履歴を見たら割と最近 2016/03/16 に、 このような書き方に変更されていたので、天秤をかけた結果の苦肉の策かなと思ったりもします。

# ◯ リスト内包表記が速い理由

また上記の理由から一般に for 文よりもリスト内包表記が速いです。 これは append というメソッドインスタンスオブジェクトを生成する必要がなくなるからです。

"""乱数のリストを生成する"""
import random

random_list1 = [random.randint(0, 999) for _ in range(1000)]

random_list2 = []
for i in range(1000):
    random_list2.append(random.randomint(0, 999))

じゃあ上の copy のコードもリスト内包表記を使った方が綺麗になりそうですが、 標準ライブラリのコードは後方互換性も必要になるので、 おそらくその関係で、ちょっと風変わりな書き方になっているのかなと思います。

上記のような書かれた方をしているのを見ると キャッシュを作って、インスタンスメソッドオブジェクトに参照先を「切替」るだけというようなことはしていなさそうです。

# ◯ identity

これを調べるためにインスタンスメソッドオブジェクトを生成した時に identity が、 どのように変化するかを通して確認しました。

identity というのは、オブジェクトが持っている番号です。id 関数で調べることができます。 以下の記事は identity について説明せいて頂いております。 全部読んでいただく必要は全くないのですが、 全てのオブジェクトは identity という数字を持っていることだけを、 なーんとなく押さえておいていただければと思います。

# ◯ 調査

# Step 1. 参照するだけ

identity は変化しませんでした。

id(girl_friend.change_name)
id(girl_friend.change_name)
>>> id(girl_friend.change_name)
4533991496
>>>
>>> # identity は変化しない
>>> id(girl_friend.change_name)
4533991496
>>> 

このことから、インスタンスメソッドオブジェクトは呼び出されているたびに、 生成されてないと言えるでしょうか?

identity が同じだったということは2つの可能性があります。 1つ目は、同じインスタンスメソッドオブジェクトを使いまわしている可能性。 2つ目は、インスタンスメソッドオブジェクトを生成して破棄して、 同じ identity を使ってインスタンスメソッドオブジェクトを生成している可能性。 もう少し追いかけてみたいと思います。

# Step 2. 属性をローカル変数に代入しておき、そのローカル変数を呼び出すようにする

identity が変化しました。

新しいインスタンスオブジェクトが生成さたことが、わかります。 どうやら呼び出される度にインスタンスオブジェクトが生成されていそうです。 また、オブジェクトの生成、破棄は、変数への代入と何らかの関係がありあそうです。

id(girl_friend.change_name)

# 変数にインスタンスメソッドオブジェクトを代入すると...
new_change_name_method = girl_friend.change_name

id(girl_friend.change_name)

id(new_change_name_method)
>>> id(girl_friend.change_name)
4438100040
>>> 
>>> # 変数にインスタンスメソッドオブジェクトを代入しておくと...
... new_change_name_method = girl_friend.change_name
>>> 
>>> # 次に呼び出した時には
>>> #  が変化する。
>>> id(girl_friend.change_name)
4438100104
>>> 
>>> id(new_change_name_method)
4438100040
>>> 

# Step 3. 別のインスタンスメソッドオブジェクトを変数に保存する。

identity が変化しました。

どうやら呼び出される度にインスタンスオブジェクトが生成されているのは、ほぼ確実そうです。

id(girl_friend.change_name)

class C(object):
    def f(self):
        pass

o = C()
m = o.f
id(m)

id(girl_friend.change_name)
>>> id(girl_friend.change_name)
4350253192
>>> 
>>> class C(object):
...     def f(self):
...         pass
... 
>>> o = C()
>>> m = o.f
>>> id(m)
4350253192
>>> 
>>> id(girl_friend.change_name)
4351437192
>>> 

# ◯ ガベレージコレクション

なぜ、1 では、インスタンスメソッドオブジェクトが、インスタンス化されたあとにすぐに削除されたのでしょうか? それは、どの変数にも代入されていなかったので、もう使われることはないオブジェクトだと判断されたからです。

使わなくなったオブジェクトが保存されたメモリから解放することは、とても重要です。 なぜならオブジェクトはメモリを占有してしまうからです。 使わなくなったオブジェクトを回収する機能をガベレージコレクションと言います。

使わなくなったオブジェクトは片付ける。

# 1. 参照カウント

CPython では、参照カウントを使ってガベレージコレクションを実装しています。 オブジェクトが、変数あるいは属性に代入されている数を保存しておきます。 例えば、オブジェクトが変数 a に代入されれば、参照カウントを1つ増やします。 また、変数 a を参照できなくなると、参照カウントを -1 減らします。 参照カウントが 0 になると、オブジェクトが参照できなくなった、 今後そのオブジェクトを利用することは無くなった、と判断して、オブジェクトを破棄します。

参照カウント法 - Python/C API リファレンスマニュアル (opens new window)
今日の計算機は有限の (しばしば非常に限られた) メモリサイズしか持たないので、参照カウントは重要な概念です; 参照カウントは、あるオブジェクトに対して参照を行っている場所が何箇所あるかを数える値です。 ... 中略 ... あるオブジェクトの参照カウントがゼロになると、そのオブジェクトは解放されます。... 後略

1, 2 では同じ identity を使ってインスタンス化→削除→インスタンス化→削除を繰り返していました。 なんでインスタンスメソッドオブジェクトの identity が変化しなかったのか?というのは、ひとつ疑問です。 おそらくそういうメモリの確保の実装をしているからだと思います

ほとんどの Python は C 言語で実装されています。C 言語で実装された Python を CPython と言います。 CPython の実装では は、メモリのアドレスです。 データを保存する場所であるメモリには、住所のような番号が割り振られています。 その住所のような番号をアドレスと言います。 この説明では、全くピンと来ないと思うので C言語, ポインタ などで検索するといいかもしれません。

1, 2, では、アドレスが 4533991496 のメモリを使って、 インスタンス化してはガベレージコレクションで 4533991496 のメモリを解放して、 また解放されたアドレスが 4533991496 のメモリを使って インスタンス化してはガベレージコレクションでアドレスが 4533991496 のメモリを解放するという動作を繰り返していました。

# 2. 循環参照ガベージコレクション

ちなみに参照カウント法では、相互に参照し合うようなケースではうまく動作しません。 そのような循環参照を検知するための別の GC が必要になるようです。

参照カウント法 - Wikipedia (opens new window)
オブジェクト同士が互いに参照しあうなど、参照が循環している場合、 参照カウントが0にならないためにオブジェクトが破棄されないという問題がある。(詳しくは後述)

参照カウント法 - Python/C API リファレンスマニュアル (opens new window)
言うまでもなく、互いを参照しあうオブジェクトについて問題があります; 現状では、解決策は "何もしない" です。 (ワイの注記: Python の参照カウント法が、循環参照に対して何もしないのであって、Python には循環参照を捉える機構はあるようです。)

現在の CPython 実装では参照カウント(reference-counting) 方式を使っており、 (オプションとして) 循環参照を行っているごみオブジェクトを遅延検出します。 この実装ではほとんどのオブジェクトを到達不能になると同時に処理することができますが、 循環参照を含むごみオブジェクトの収集が確実に行われるよう保証しているわけではありません。
3.1. オブジェクト、値、および型 - Python 言語リファレンス (opens new window)

循環参照 GC の話をしてくれています。 死ぬほどわかりやすく書いてくれていると思うのですが、自分はまだ 1 mm も理解できていません。

# ◯ まとめ

メソッドは呼び出されるたびにインスタンス化していました。 これによって速度を犠牲にすることになりましたが、 動的にクラスオブジェクトの関数が変更されると、 すぐにメソッドも変更されるという柔軟性を獲得しています。

ポイント

メソッドの呼び出しは重いけど柔軟な設計です。

# おわりに

ありがとうございました。 ここまで以下のような流れで追って来ました。

Python は、関数とディスクリプタを組み合わせて、メソッドを実装しました。 Python は、ごっちゃにひとまとめにしたメソッド定義文を定めて、メソッドを実装はしませんでした。

PEP 20 -- The Zen of Python (opens new window)
プログラマが持つべき心構え (The Zen of Python) - Qiita (opens new window)

Complex is better than complicated.
ごっちゃにひとまとめした実装するよりも、複数のパーツを組み合わて実装する方が良い。

Simple is better than complex.
複数のパーツを組み合わて実装するよりも、各パーツをバラバラに実装する方が良い。 (複数のパーツを組み合わせて1つのものを作って1つのものに同時に複数のことをさせるよりも、複数のパーツをバラバラして1つのパーツには1つのことをさせる方が良い。)

メソッド呼び出し以外のディスクリプタの用途としては、 Java で言う所の getter とか setter を書かずに、 直接、属性を参照したり属性を代入するだけで済ませることができるようになります。 身近な具体例としては Django の model で使われている気配があります。

ディスクリプタの動作と仕組みだけを手短に述べました。 こんな説明では "何言ってんだ、お前?" みたいな気分かもしれません。

しかし Google の Python のコーディング規約 (opens new window) に 「強力な機能 このような機能は避ける。」とあります。 なので必要性、興味がなければ必ずしもディスクリプタを学ぶ必要性はないかなとも思います。

ですが、もし興味がありましたらディスクリプタを理解するために、 自分は Effective Python (opens new window) の 「4 章 メタクラスと属性」が、いいかなと思います。

特に実際の使用例を示してくれているので、かなりありがたかったです。 1, 2, 3 章と 4 章は関係ないので、4 章だけ読むことができます。 この 30 ページの 4 章のためだけに 3,000 円以上を払う価値は十分にあると思います(理解したとは言っていない)。