読者です 読者をやめる 読者になる 読者になる

ディープではないcopy.deepcopyの話

コピーについて考える

最近、強化学習の勉強の一環としてSuttonBookのLispコードをpythonに書き直してうごかしています。
Lispオブジェクト指向ではないけれど、(Common Lisp Object Systemというオブジェクト指向システムを持っているみたい。勘違いしてました(;´・ω・) )どうせならと思って移植の際にクラス化したりしていたら、グリーディな行動選択をさせる際に オブジェクトをコピーする必要が出てきて、安易にcopy.deepcopyを使ったらクッソ遅くなってしまった。
結局、新しいインスタンス生成して必要なメンバのみコピーするように書き換えたらサクサク動くようになりました。

(そういえば研究で書いてた重いコードも安易にdeepcopyつかっていたような…)

そんなわけで、pythonでオブジェクトや変数をコピーするときどういう風にコピーするのが一番いいのか気になって調べたことをまとめる。

実際どれくらい遅いのか

まず、deepcopyを使う方法と使わない方法でどれくらいcopyの速さが違うのか確認しておく


例1 単純なリスト

長さ1000のリストをlist()で複製した場合とdeepcopyした場合の処理時間を計測する。 処理回数は10000とした。


コード

from timeit import timeit
num = 10000
print timeit("list(test_list)",setup='test_list = [i for i in xrange(1000)]',number=num)
print timeit("copy.deepcopy(test_list)",setup='import copy ;test_list = [i for i in xrange(1000)]',number = num)

結果

0.272120951146
1793.11939573

例2 適当なクラスをコピーする

以下に定義したtest_copyクラスをコンストラクタで複製した場合とdeepcopyで複製した場合の処理時間を計測する。 処理回数は同じく10000回


コード
test_copy.py

class test_copy(object):
    def __init__(self,test_obj=None):
        if test_obj is None:
            self.a = 1
            self.l = [1,2,3,4,5,6,7,8,9]
            self.text ="aiueo"
        elif isinstance(test_obj,test_copy):
            self.a = test_obj.a
            self.l = list(test_obj.l)
            self.text = str(test_obj.text)
num = 10000
print timeit("test_copy(test_obj)",setup='from test_copy import test_copy; test_obj = test_copy()',number=num)
print timeit("copy.deepcopy(test_obj)",setup='from test_copy import test_copy;import copy ;test_obj = test_copy()',number = num)

結果

0.814853623342
40.5248817692

というわけで、deepcopyは確かに処理が重いことが確認できた。

pythonレファレンスによると、deepcopyの場合は単にコピーするのではなく、コピー済みオブジェクトを記録するなど、色々と処理を行っているようだ。

実際copy.pyの中身をみるとメモ化や型チェックで100行ほどのコードになっている。
またネストしたリストのような複雑なオブジェクトをコピーするためにcopy.deepcopyを再帰的に呼び出すような処理もある。(deepcopy内の_reconstruct()は内部でdeepcopyを呼んでいる) deepcopyの処理の遅さはこの再帰呼び出しによるものかと思われる。

def deepcopy(x, memo=None, _nil=[]):
    """Deep copy operation on arbitrary Python objects.

    See the module's __doc__ string for more info.
    """

    if memo is None:
        memo = {}

    d = id(x)
    y = memo.get(d, _nil)
    if y is not _nil:
        return y

    cls = type(x)

    copier = _deepcopy_dispatch.get(cls)
    if copier:
        y = copier(x, memo)
    else:
        try:
            issc = issubclass(cls, type)
        except TypeError: # cls is not a class (old Boost; see SF #502085)
            issc = 0
        if issc:
            y = _deepcopy_atomic(x, memo)
        else:
            copier = getattr(x, "__deepcopy__", None)
            if copier:
                y = copier(memo)
            else:
                reductor = dispatch_table.get(cls)
                if reductor:
                    rv = reductor(x)
                else:
                    reductor = getattr(x, "__reduce_ex__", None)
                    if reductor:
                        rv = reductor(2)
                    else:
                        reductor = getattr(x, "__reduce__", None)
                        if reductor:
                            rv = reductor()
                        else:
                            raise Error(
                                "un(deep)copyable object of type %s" % cls)
                y = _reconstruct(x, rv, 1, memo)

    memo[d] = y
    _keep_alive(x, memo) # Make sure x lives at least as long as d
    return y

またdeepcopyの処理速度について調べていたらこんな記事が
python - copy.deepcopy vs pickle - Stack Overflow

cPickleをつかってpickle化したオブジェクトを再びloadすることでcopy.deepcopy()を使うより高速に ディープコピーすることができるんだが、copy.deepcopy()を使う利点てあるの??

という質問なんだけど、そもそもpickle化したオブジェクトをloadすることでコピーするというテクニックを初めて知った。

で、回答としては

Pickleによるコピーがdeepcopyに比べて速度が速いが、deepcopyより一般性に欠ける手法である

とのこと。

確かにpickle化可能な型には制限があるし、(11.1. pickle — Python オブジェクトの直列化 — Python 2.7.x ドキュメント) copy対象のクラスが改変されたらバグになりうるから、使う場合はassertいれるとかコメント入れるとか、とにかくきちんと対応したほうがよさそう。 pickle、unpickleだけでコピーできる手軽さは魅力的ではあるが。

まとめ

コピー対象が比較的単純な場合はcPickleつかう。複雑なオブジェクトを正確にコピーしたい場合はcopy.deepcopyを使う。速度が必要な場合は専用にdeepcopyを作るのがよさそう。