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

# copy.py のコードを読む

この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています
この記事は、いま書いています

標準ライブラリ copy のソースコードを読みます。 ただ、正直自分もまだ読みきれてはいません。

immutable, atomic について考える上で、 簡単に触れておいたもいいかなと感じ、 ここで自分が分かる範囲でご紹介させていただきます。

かなり重箱の隅を突くようなことをするので、 わからないところは適宜飛ばしていただいて、 これもコードの全体像と atomic, immutable の箇所だけをなんとなく把握していただければ幸いです。

immutable が、なぜ重要かは、以下の記事でご紹介させていただきました。

atomic は、なぜ重要なのでしょうか? atomic について意識しておくことは、値とはなにか?について考える時と、 今後の他の言語を学習する上でも大切だと感じたからです。

atomic については以下の2つの記事で触れてきました。

Python における値とはなにか?については、 この先、以下の記事でご紹介させていただきたいと思います。

他の言語で重要とはどういうことでしょうか? Java の話になりますが primitive 型と reference 型があります。 また Julia でも primitive 型と composite 型に分けれられています。 なんだかんだで他の言語を学ぶ時にも出てくる概念です。

Python は primitive と composite の違いを見えないようにしてくれています。 すこし解きほぐして2つの違いを曝露していきます。

話は変わりますが Python はクラス定義文で self を書きます。 これはそのようにした方が色々な書き方ができるからです。

Go, Rust ではその考えをさらに推し進めています。 Go, Rust といった言語では primitive に対しても メソッドのディッスパッチができるように、 ちょっと変わった構文になっているのです。 なぜ、そのような構文を採用したのでしょうか?

これらの詳細については、 この先の以下の記事でご紹介させていただきます。

# copy のソースコードはどこにあるの?

今回は書き換えたコードで全体の流れを追いますが、 copy.py のコードは以下の場所から取り出すことができます。

# GitHub から

以下のリンクから確認できます。

# 特殊属性 __file__ から

特殊属性 __file__ から copy.py がある場所を確認できます。

>>> import copy
>>> copy.__file__
'/usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py'
>>> 

特殊属性 __file__ と import 文については、 この先の以下の記事でご紹介させていただきます。

# copy.py の全体像

copy.py は2つの関数を定義しているだけです。 浅いコピーをする copy と深いコピーをする deepcopy です。

def copy(x):
    ...

def deepcopy(x, memo=None, _nil[]):
    ...

copy と deepcopy は、ほぼ同じ構成になっています。 前半と後半に分けられます。

# 前半戦

前半ではオブジェクトをコピーする関数が既に定義されていないかを探し、 変数 copier に代入し、copier(x) を実行して結果を返します。

def copy():
    # コピーする関数 copier があれば...
    if copier:
        # copier して返す。
        return copier(x)

# 後半戦

もし copier が見つけられなければ後半に突入します。 しかし、後半はなにをしているか、詳細は自分にもよくわかりません。

後半戦では reductor を探します。 これはクラスを引数にとり、インスタンスを生成するための材料 rv を作る関数です。 そしてその材料 rv を元にして _reconstruct 関数がインスタンスを生成します。

def copy():
    # reductor があれば
    if reductor:   
        # rv を生成して
        rv = reductor(x)
    ...
    # インスタンスを再生して返す。
    return _reconstruct(x, None, *rv)

list, dict などの組み込み型はコピーするメソッド list.copy があるので 後半戦に突入することはありません。 反面、class 定義文で定義したユーザ定義クラスのオブジェクトは基本的に後半戦で解決していきます。

# 浅いコピー 前半戦

前半戦では copier を探します。 あればめでたしめでたしで return です。

def copy(x):
    """Shallow copy operation on arbitrary Python objects.
    See the module's __doc__ string for more info.
    """

    cls = type(x)
    
    # 1) 組み込み型のインスタンスなら
    copier = _copy_dispatch.get(cls)
    if copier:
        return copier(x)

    # 2) typeクラスのインスタンスなら(メタクラスなら)
    try:
        issc = issubclass(cls, type)
    except TypeError: # cls is not a class
        issc = False
    if issc:
        # treat it as a regular class:
        return _copy_immutable(x)

    # 3) __copy__ メソッドが定義されたクラスの
    #    インスタンスなら
    copier = getattr(cls, "__copy__", None)
    if copier:
        return copier(x)

# 組み込み型

_copy_dispatch は辞書です。 キーは、クラスです。 そして返される値は、クラスのインスタンスをコピーする関数です。

    # 1) 組み込み型のインスタンスなら
    copier = _copy_dispatch.get(cls)
    if copier:
        return copier(x)

_copy_dispatch の中身はすこし複雑なので置いておいて、 まず前半戦の全体像を追いかけていきたいと思います。

# typeクラスのインスタンスなら(メタクラスなら)

isscTrue が返されるクラスは、どんなクラスでしょうか?

    # 2) typeクラスのインスタンスなら(メタクラスなら)
    try:
        issc = issubclass(cls, type)
    except TypeError: # cls is not a class
        issc = False
    if issc:
        # treat it as a regular class:
        return _copy_immutable(x)

これメタクラスと言います。 type クラスは、クラスオブジェクトをインスタンス化します。 このクラスオブジェクトをインスタンス化する type クラスを継承したクラスを メタクラスと言います。 説明が口説いですね...

メタクラスについては以下で解説させていただきました。 全く重要な知識ではありません、知らなくても先に進むことができます。

_copy_immutable でコピーしたことにしていますが、 自分自身を返す関数です。

def _copy_immutable(x):
    return x

まったくコピーしていないません。 おまけにメタクラスは、基本的には immutable ではなく mutable です。

なぜこのようになっているのでしょうか? これはクラスオブジェクトが 基本的にシングルトンとして扱われているからだと思われます。

クラスオブジェクトがシングルトンとして扱われている旨、 記述されている箇所を探し回ったのですが見つかりませんでした。

基本的にはそのような動作をしています。

# __copy__ メソッドが定義されたクラスのインスタンスなら

__copy__ メソッドが定義されていれば、それを使いコピーします。

    # 3) __copy__ メソッドが定義されたクラスの
    #    インスタンスなら
    copier = getattr(cls, "__copy__", None)
    if copier:
        return copier(x)

公式ドキュメントで __copy__ が特殊メソッドとして説明されている箇所を探しましたが見つかりませんでした。

# _copy_dispatch

説明を飛ばした _copy_dispath を見ていきます。 _copy_dispatch は組み込み型に対するコピー関数を保存した辞書です。

前半は _copy_immutable に対応したクラスを _copy_dispatch に叩き込み、 後半では copy メソッドを持つクラスを _copy_dispatch に叩き込んでいます。

ここで _copy_immutable が使われているクラスは基本的には immutable だと考えていいかなと思います。 ただし、メタクラスでも _copy_immutable が使われていたように mutable なシングルトンもこの中に混じっています。 types.FunctionType は mutable なシングルトンです。ややこしいですね。

# ここに immutable なクラスのオブジェクトが列挙されています。
for t in (type(None), int, float, bool, complex, str, tuple,
          bytes, frozenset, type, range, slice,
          types.BuiltinFunctionType, type(Ellipsis), type(NotImplemented),
          types.FunctionType, weakref.ref):

抜粋したコードが本当に immutable を列挙しようとしているのかを確認するために この for 文がなにをしているのか、もう少し見てみたいと思います。

ここでのポイントは変数 _copy_dispatch, d です。 変数 _copy_dispatch, d に組み込み型のオブジェクトを コピーする関数を保存しています。

組み込み型は int, str, list など最初から Python にはいってるクラスのことです。

変数 _copy_dispatch, d が、 どのように作成されているのか見てみたいと思います。

# _copy_dispatch は、組み込み型のコピー関数を返す辞書です。
# しかし、クラスオブジェクトが hashable だったとは....
_copy_dispatch = d = {}

# 使い方
# _copy_dispatch[クラス] = オブジェクトをコピーする関数
# d             [クラス] = オブジェクトをコピーする関数



#
# 1. immutable な組込型の copy 関数を辞書に代入。
#

# immutable なクラスは、そのままインスタンスオブジェクトをそのまま返す
def _copy_immutable(x):
    return x

# ここに immutable なクラスのオブジェクトが列挙されています。
for t in (type(None), int, float, bool, complex, str, tuple,
          bytes, frozenset, type, range, slice,
          types.BuiltinFunctionType, type(Ellipsis), type(NotImplemented),
          types.FunctionType, weakref.ref):
    # _copy_dispatch[クラス] = オブジェクトをコピーする関数
    # d             [t     ] = _copy_immutable
    d[t] = _copy_immutable

# なにしているのか、よくわからない。
t = getattr(types, "CodeType", None)
if t is not None:
    d[t] = _copy_immutable



#
# 2. mutable な組込型の copy 関数を辞書に代入。
#

# _copy_dispatch[クラス] = オブジェクトをコピーする関数
# d             [クラス] = オブジェクトをコピーする関数
d[list] = list.copy
d[dict] = dict.copy
d[set] = set.copy
d[bytearray] = bytearray.copy

# なにしているのか、よくわからない。
if PyStringMap is not None:
    d[PyStringMap] = PyStringMap.copy

# 変数削除
del d, t

_copy_dispatch がどのように使われているかと言うと copy 関数の中身で 以下のように使われています。

def copy(x):
    """Shallow copy operation on arbitrary Python objects.
    See the module's __doc__ string for more info.
    """

    cls = type(x)
    # _copy_dispatch は、組み込み型の copy 関数を返す辞書です。
    copier = _copy_dispatch.get(cls)
    if copier:
        return copier(x)

    ... # 省略

関数で呼び出しても copy.copy(リスト) メソッドで呼び出しても リスト.copy() 同じことしていることがわかります。

また、こうやって辞書を dispatch って表現することもあるんですね。 知らなかった... orz

# 浅いコピー 後半戦

僕もよくわからない後半戦です。 なぜよくわからないかというと pickel と関連しているからです。

def copy(x):

    ...

    # 1) copyreg に事前に登録していれば
    reductor = dispatch_table.get(cls)
    if reductor:
        rv = reductor(x)
    else:
        # 2) __reduce_ex__ メソッドが定義されていれば
        reductor = getattr(x, "__reduce_ex__", None)
        if reductor:
            rv = reductor(4)
        else:
            # 3) __reduce__ メソッドが定義されていれば
            reductor = getattr(x, "__reduce__", None)
            if reductor:
                rv = reductor()
            else:
                raise Error("un(shallow)copyable object of type %s" % cls)

    if isinstance(rv, str):
        return x
    return _reconstruct(x, None, *rv)