Pythonオブジェクトのコピー/ディープコピー操作をオーバーライドする方法は?


100

私は違いを理解copydeepcopyコピーモジュール内を。私は以前は成功裏に使用copy.copycopy.deepcopyてきましたが、__copy__および__deepcopy__メソッドのオーバーロードについて実際に行ったのはこれが初めてです。私はすでにを通じて周りGoogleで検索して見てきたのインスタンスを探すためにPythonモジュールを内蔵__copy__し、__deepcopy__機能(例えばsets.pydecimal.py、およびfractions.py)が、私はまだ100%でないことを確認、私はそれを右に持っているんです。

これが私のシナリオです:

設定オブジェクトがあります。最初に、デフォルトの値のセットを使用して1つの構成オブジェクトをインスタンス化します。この構成は、他の複数のオブジェクトに渡されます(すべてのオブジェクトが同じ構成で開始されるようにするため)。ただし、ユーザーインタラクションが開始すると、各オブジェクトは、互いの構成に影響を与えずに、その構成を個別に微調整する必要があります(つまり、手渡しするために、初期構成のディープコピーを作成する必要があると言います)。

次にオブジェクトの例を示します。

class ChartConfig(object):

    def __init__(self):

        #Drawing properties (Booleans/strings)
        self.antialiased = None
        self.plot_style = None
        self.plot_title = None
        self.autoscale = None

        #X axis properties (strings/ints)
        self.xaxis_title = None
        self.xaxis_tick_rotation = None
        self.xaxis_tick_align = None

        #Y axis properties (strings/ints)
        self.yaxis_title = None
        self.yaxis_tick_rotation = None
        self.yaxis_tick_align = None

        #A list of non-primitive objects
        self.trace_configs = []

    def __copy__(self):
        pass

    def __deepcopy__(self, memo):
        pass 

このオブジェクトにcopyおよびdeepcopyメソッドを実装して適切な動作を確実にcopy.copycopy.deepcopyてくれる正しい方法は何ですか?


うまくいきますか?問題はありますか?
Ned Batchelder

共有参照でまだ問題が発生していると思いましたが、他の場所でめちゃくちゃになっている可能性があります。機会があれば@MortenSiebuhrの投稿に基づいて再確認し、結果を更新します。
ブレントがコードを作成

私の現在の限られた理解から、copy.deepcopy(ChartConfigInstance)は、元のオブジェクトとの共有参照がない新しいインスタンスを返します(ディープコピーを自分で再実装することなく)。これは間違っていますか?
emschorsch 2015

回答:


81

カスタマイズの推奨事項は、ドキュメントページの最後にあります

クラスは、酸洗いの制御に使用するのと同じインターフェースを使用して、コピーを制御できます。これらのメソッドの詳細については、モジュールpickleの説明を参照してください。コピーモジュールは、copy_reg登録モジュールを使用しません。

クラスが独自のコピー実装を定義するために、特別なメソッド__copy__()とを 定義できます__deepcopy__()。前者は、浅いコピー操作を実装するために呼び出されます。追加の引数は渡されません。後者は、ディープコピー操作を実装するために呼び出されます。1つの引数、メモ辞書が渡されます。__deepcopy__() 実装がコンポーネントのディープコピーを作成する必要がある場合はdeepcopy()、コンポーネントを最初の引数、メモディクショナリを2番目の引数として関数を呼び出す必要があります。

酸洗いのカスタマイズを気にしないように見えるので、定義__copy____deepcopy__間違いなくあなたのために行くための正しい方法のように思えます。

具体的には、__copy__(浅いコピー)はあなたの場合はかなり簡単です...:

def __copy__(self):
  newone = type(self)()
  newone.__dict__.update(self.__dict__)
  return newone

__deepcopy__同様であろう(受諾memoあまりにも引数を)が、復帰までにコールする必要がありますself.foo = deepcopy(self.foo, memo)任意の属性に対してself.fooニーズが深い(コンテナである本質的属性-彼らを介して他のものを保持するリスト、dicts、非プリミティブオブジェクトのコピーという__dict__複数可)。


1
@kaizer、ピクルス化/アンピクルス化、およびコピーをカスタマイズすることは問題ありませんが、ピクルス化を気にしない場合は、__copy__/ を使用するほうが簡単で直接的__deepcopy__です。
Alex Martelli、

4
これはコピー/ディープコピーを直接翻訳したものではないようです。copyもdeepcopyも、コピーされるオブジェクトのコンストラクターを呼び出しません。この例を考えてみましょう。class Test1(object):def init __(self):print "%s。%s"%(self .__ class .__ name__、 " init ")class Test2(Test1):def __copy __(self):new = type(self) ()return new t1 = Test1()copy.copy(t1)t2 = Test2()copy.copy(t2)
Rob Young

12
type(self)()の代わりに、cls = self .__ class__を使用する必要があると思います。cls .__ new __(cls)は、コンストラクターインターフェイスの影響を受けません(特にサブクラス化の場合)。ただし、ここではそれほど重要ではありません。
Juh_ 2013年

11
なぜself.foo = deepcopy(self.foo, memo)...?どういう意味newone.foo = ...ですか?
Alois Mahdal 2013

4
@Juh_のコメントはスポットです。電話をかけたくない__init__。それはコピーがすることではありません。また、酸洗いとコピーを異なるものにする必要があるユースケースもよくあります。実際、コピーがデフォルトで酸洗いプロトコルを使用しようとする理由もわかりません。コピーはメモリ内の操作用であり、酸洗いはクロスエポックの永続性用です。それらは互いにほとんど関係のないまったく異なるものです。
Nimrod

96

Alex Martelliの回答とRob Youngのコメントを組み合わせると、次のコードが得られます。

from copy import copy, deepcopy

class A(object):
    def __init__(self):
        print 'init'
        self.v = 10
        self.z = [2,3,4]

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        return result

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, deepcopy(v, memo))
        return result

a = A()
a.v = 11
b1, b2 = copy(a), deepcopy(a)
a.v = 12
a.z.append(5)
print b1.v, b1.z
print b2.v, b2.z

プリント

init
11 [2, 3, 4, 5]
11 [2, 3, 4]

ここ__deepcopy__ではmemo、オブジェクト自体がそのメンバーから参照されている場合に過剰なコピーを回避するために、dictを入力します。


2
@bytestormとはTransporter何ですか?
アントニーハッチキンズ

@AntonyHatchkins Transporterは、私が書いているクラスの名前です。そのクラスでは、deepcopyの動作をオーバーライドします。
バイトストーム

1
@bytestormの内容はTransporter何ですか?
アントニーハッチキンズ

1
私が思うに__deepcopy__、戻り結果:結果がNoneでない場合は、D = ID(自己)結果= memo.get(D、なし):< - - LANG-pythonの!言語:>無限の再帰を避けるためにテストを含める必要があります
アントニン・Hoskovec

@AntonyHatchkinsは、それはすぐにあなたのポストからはっきりしていないところ memo[id(self)]、実際に無限再帰を防ぐために使用されます。それがのキーである場合、オブジェクトへの呼び出しを内部的に中止することを示唆する短い例をまとめました、正しいですか?デフォルトでこれを単独で行うように見えることも注目に値します。手動で定義することが実際に必要な場合を想像するのは難しくなります...copy.deepcopy()id()memodeepcopy()__deepcopy__
Jonathan H

14

Peterの優れた答えに従って、デフォルトの実装への変更を最小限に抑えてカスタムディープコピーを実装します(たとえば、必要なフィールドを変更するだけです)。

class Foo(object):
    def __deepcopy__(self, memo):
        deepcopy_method = self.__deepcopy__
        self.__deepcopy__ = None
        cp = deepcopy(self, memo)
        self.__deepcopy__ = deepcopy_method
        cp.__deepcopy__ = deepcopy_method

        # custom treatments
        # for instance: cp.id = None

        return cp

1
これはdelattr(self, '__deepcopy__')then を使用するよりも好ましいsetattr(self, '__deepcopy__', deepcopy_method)ですか?
ジョエル

この回答によれば、どちらも同等です。ただし、名前が動的でコーディング時に不明な属性を設定する場合は、setattrの方が便利です。
Eino Gourdin

8

コピーメソッドをカスタマイズする必要がないため、これらのメソッドをオーバーライドする必要がある理由は問題から明らかではありません。

とにかく、ディープコピーをカスタマイズしたい場合(たとえば、一部の属性を共有し、他の属性をコピーするなど)、ここに解決策があります。

from copy import deepcopy


def deepcopy_with_sharing(obj, shared_attribute_names, memo=None):
    '''
    Deepcopy an object, except for a given list of attributes, which should
    be shared between the original object and its copy.

    obj is some object
    shared_attribute_names: A list of strings identifying the attributes that
        should be shared between the original and its copy.
    memo is the dictionary passed into __deepcopy__.  Ignore this argument if
        not calling from within __deepcopy__.
    '''
    assert isinstance(shared_attribute_names, (list, tuple))
    shared_attributes = {k: getattr(obj, k) for k in shared_attribute_names}

    if hasattr(obj, '__deepcopy__'):
        # Do hack to prevent infinite recursion in call to deepcopy
        deepcopy_method = obj.__deepcopy__
        obj.__deepcopy__ = None

    for attr in shared_attribute_names:
        del obj.__dict__[attr]

    clone = deepcopy(obj)

    for attr, val in shared_attributes.iteritems():
        setattr(obj, attr, val)
        setattr(clone, attr, val)

    if hasattr(obj, '__deepcopy__'):
        # Undo hack
        obj.__deepcopy__ = deepcopy_method
        del clone.__deepcopy__

    return clone



class A(object):

    def __init__(self):
        self.copy_me = []
        self.share_me = []

    def __deepcopy__(self, memo):
        return deepcopy_with_sharing(self, shared_attribute_names = ['share_me'], memo=memo)

a = A()
b = deepcopy(a)
assert a.copy_me is not b.copy_me
assert a.share_me is b.share_me

c = deepcopy(b)
assert c.copy_me is not b.copy_me
assert c.share_me is b.share_me

= None __deepcopy__になるので、クローンもメソッドをリセットする必要はあり__deepcopy__ませんか?
flutefreak7

2
いいえ。場合は__deepcopy__この方法が見つからない(あるいはされていないobj.__deepcopy__リターンなし)、その後、deepcopy標準のディープコピー機能にフォールバックします。これはここ
ピーター

1
しかし、その場合、bは共有でディープコピーすることができませんか?c = deepcopy(a)は、d = deepcopy(b)とは異なります。これは、dがデフォルトのディープコピーであり、cがaといくつかの共有属性を持つためです。
flutefreak7 2017

1
ああ、今あなたの言っていることがわかります。いい視点ね。__deepcopy__=Noneクローンから偽の属性を削除することで修正したと思います。新しいコードを参照してください。
Peter

1
おそらくPythonの専門家には明らかです:このコードをPython 3で使用する場合は、 "for attr、val in shared_attributes.iteritems():"を "for attr、val in shared_attributes.items():"に変更してください
complexM

6

細かい点では少し気が進まないかもしれませんが、ここにいきます

copyドキュメントから;

  • 浅いコピーは新しい複合オブジェクトを作成し、(可能な範囲で)元のオブジェクトにあるオブジェクトへの参照を挿入します。
  • ディープコピーは、新しい複合オブジェクトを作成し、再帰的に、元のオブジェクトにあるオブジェクトのコピーをそれに挿入します。

つまりcopy()、一番上の要素のみをコピーし、残りはポインタとして元の構造に残します。deepcopy()すべてを再帰的にコピーします。

つまり、deepcopy()必要なものです。

本当に特定のことを行う必要がある場合は、マニュアルで説明されているように、__copy__()またはをオーバーライドできます__deepcopy__()。個人的には、私はおそらくプレーンな関数(config.copy_config()など)を実装して、Pythonの標準的な動作ではないことを明らかにします。


3
クラスが独自のコピー実装を定義するために、特別なメソッド__copy__()およびを定義できます__deepcopy__() docs.python.org/library/copy.html
SilentGhost

おかげで、コードを再確認します。これが他の場所での単純なバグだったら、私は馬鹿げた感じになるでしょう:-P
ブレントがコードを

@MortenSiebuhrあなたは正しいです。これらの関数を上書きせずに、コピー/ディープコピーがデフォルトで何かを実行することを完全に明確にしていませんでした。後で微調整できる実際のコードを探していたので(たとえば、すべての属性をコピーしたくない場合)、賛成票を与えましたが、@ AlexMartinelliの答えを使用します。ありがとう!
ブレントがコード

2

copy最終的にモジュールが使用__getstate__()/ 酸洗プロトコル__setstate__() ので、これらはまた、オーバーライドへの有効な標的です。

デフォルトの実装__dict__はクラスのを返し、設定するだけなのでsuper()上記のEino Gourdinの巧妙なトリックを呼び出して心配する必要はありません。


1

Antony Hatchkinsの明確な答えに基づいて、問題のクラスが別のカスタムクラスから派生した私のバージョンを次に示します(呼び出しが必要なものsuper)。

class Foo(FooBase):
    def __init__(self, param1, param2):
        self._base_params = [param1, param2]
        super(Foo, result).__init__(*self._base_params)

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        super(Foo, result).__init__(*self._base_params)
        return result

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, copy.deepcopy(v, memo))
        super(Foo, result).__init__(*self._base_params)
        return result
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.