Bagaimana cara menimpa operasi copy / deepcopy untuk objek Python?

101

Saya memahami perbedaan antara copyvs. deepcopydalam modul salin. Saya telah menggunakan copy.copydan copy.deepcopysebelumnya berhasil, tetapi ini adalah pertama kalinya saya benar-benar kelebihan beban metode __copy__dan __deepcopy__. Aku sudah Googled sekitar dan melihat melalui built-in modul Python untuk mencari contoh dari __copy__dan __deepcopy__fungsi (misalnya sets.py, decimal.py, dan fractions.py), tapi aku masih belum 100% yakin aku punya itu benar.

Inilah skenario saya:

Saya memiliki objek konfigurasi. Awalnya, saya akan membuat contoh satu objek konfigurasi dengan satu set nilai default. Konfigurasi ini akan diserahkan ke beberapa objek lainnya (untuk memastikan semua objek dimulai dengan konfigurasi yang sama). Namun, begitu interaksi pengguna dimulai, setiap objek perlu menyesuaikan konfigurasinya secara independen tanpa mempengaruhi konfigurasi satu sama lain (yang mengatakan kepada saya bahwa saya perlu membuat salinan dalam dari konfigurasi awal saya untuk dibagikan).

Berikut contoh objeknya:

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 

Apa cara yang tepat untuk mengimplementasikan metode copydan deepcopypada objek ini untuk memastikan copy.copydan copy.deepcopymemberi saya perilaku yang tepat?

Brent Menulis Kode
sumber
Apakah itu bekerja? Apakah ada masalah?
Ned Batchelder
Saya pikir saya masih mendapatkan masalah dengan referensi bersama, tetapi sangat mungkin saya mengacaukan di tempat lain. Saya akan memeriksa ulang berdasarkan postingan @ MortenSiebuhr ketika saya mendapat kesempatan dan memperbarui hasilnya.
Brent Menulis Kode
Dari pemahaman saya yang terbatas saat ini, saya mengharapkan copy.deepcopy (ChartConfigInstance) mengembalikan contoh baru yang tidak akan memiliki referensi bersama dengan aslinya (tanpa menerapkan ulang deepcopy sendiri). Apakah ini salah?
emschorsch

Jawaban:

82

Rekomendasi untuk menyesuaikan ada di bagian paling akhir halaman dokumen :

Kelas dapat menggunakan antarmuka yang sama untuk mengontrol penyalinan yang mereka gunakan untuk mengontrol pengawetan. Lihat deskripsi acar modul untuk informasi tentang metode ini. Modul copy tidak menggunakan modul registrasi copy_reg.

Agar kelas dapat mendefinisikan implementasi salinnya sendiri, ia dapat mendefinisikan metode khusus __copy__()dan __deepcopy__(). Yang pertama dipanggil untuk mengimplementasikan operasi penyalinan dangkal; tidak ada argumen tambahan yang diberikan. Yang terakhir dipanggil untuk mengimplementasikan operasi deep copy; itu melewati satu argumen, kamus memo. Jika __deepcopy__() implementasi perlu membuat salinan mendalam dari sebuah komponen, itu harus memanggil deepcopy()fungsi dengan komponen sebagai argumen pertama dan kamus memo sebagai argumen kedua.

Karena Anda tampaknya tidak peduli tentang kustomisasi pengawetan, mendefinisikan __copy__dan __deepcopy__sepertinya cara yang tepat untuk Anda.

Secara khusus, __copy__(salinan dangkal) cukup mudah dalam kasus Anda ...:

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

__deepcopy__akan serupa (menerima memoargumen juga) tetapi sebelum mengembalikannya harus memanggil self.foo = deepcopy(self.foo, memo)atribut apa pun self.fooyang membutuhkan penyalinan mendalam (pada dasarnya atribut yang merupakan wadah - daftar, dicts, objek non-primitif yang menyimpan barang lain melalui __dict__s mereka ).

Alex Martelli
sumber
1
@kaizer, mereka baik-baik saja untuk menyesuaikan pengawetan / pembongkaran serta penyalinan, tetapi jika Anda tidak peduli tentang pengawetan, itu lebih sederhana dan lebih langsung untuk menggunakan __copy__/ __deepcopy__.
Alex Martelli
4
Itu sepertinya bukan terjemahan langsung dari copy / deepcopy. Baik copy maupun deepcopy tidak memanggil konstruktor dari objek yang sedang disalin. Perhatikan contoh ini. class Test1 (objek): 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
Saya pikir alih-alih type (self) (), Anda harus menggunakan cls = self .__ class__; cls .__ new __ (cls) menjadi tidak sensitif terhadap antarmuka konstruktor (terutama untuk subclassing). Namun tidak terlalu penting di sini.
Juh_
11
Kenapa self.foo = deepcopy(self.foo, memo)...? Tidakkah kamu benar-benar bermaksud newone.foo = ...?
Alois Mahdal
4
Komentar @ Juh_ tepat. Anda tidak ingin menelepon __init__. Bukan itu yang dilakukan salinan. Juga sangat sering terjadi kasus penggunaan di mana pengawetan dan penyalinan harus berbeda. Sebenarnya, saya bahkan tidak tahu mengapa copy mencoba menggunakan protokol pengawetan secara default. Menyalin adalah untuk manipulasi dalam memori, pengawetan adalah untuk persistensi lintas zaman; mereka adalah hal-hal yang sama sekali berbeda yang tidak memiliki banyak hubungan satu sama lain.
Nimrod
97

Dengan menggabungkan jawaban Alex Martelli dan komentar Rob Young, Anda mendapatkan kode berikut:

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

cetakan

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

di sini __deepcopy__mengisi memodict untuk menghindari penyalinan berlebih jika objek itu sendiri direferensikan dari anggotanya.

Antony Hatchkins
sumber
2
@bytestorm apa itu Transporter?
Antony Hatchkins
@AntonyHatchkins Transporteradalah nama kelas yang saya tulis. Untuk kelas itu saya ingin menimpa perilaku deepcopy.
bytestorm
1
@bytestorm apa isinya Transporter?
Antony Hatchkins
1
Saya pikir __deepcopy__harus menyertakan tes untuk menghindari rekursi tak terbatas: <! - language: lang-python -> d = id (self) result = memo.get (d, None) jika hasilnya bukan None: return result
Antonín Hoskovec
@AntonyHatchkins Tidak segera jelas dari posting Anda di mana memo[id(self)] sebenarnya digunakan untuk mencegah rekursi tak terbatas. Saya telah mengumpulkan contoh singkat yang menunjukkan bahwa secara copy.deepcopy()internal membatalkan panggilan ke suatu objek jika itu id()adalah kuncinya memo, benar? Perlu juga dicatat bahwa deepcopy()tampaknya melakukan ini sendiri secara default , yang membuatnya sulit untuk membayangkan kasus di mana mendefinisikan __deepcopy__secara manual benar-benar diperlukan ...
Jonathan H
14

Mengikuti jawaban Peter yang sangat baik , untuk mengimplementasikan sebuah custom deepcopy, dengan sedikit perubahan pada implementasi default (misalnya hanya memodifikasi sebuah field seperti yang saya butuhkan):

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
Eino Gourdin
sumber
1
adalah ini lebih suka menggunakan delattr(self, '__deepcopy__')kemudian setattr(self, '__deepcopy__', deepcopy_method)?
joel
Menurut jawaban ini , keduanya setara; tetapi setattr lebih berguna saat mengatur atribut yang namanya dinamis / tidak dikenal pada saat pengkodean.
Eino Gourdin
8

Tidak jelas dari masalah Anda mengapa Anda perlu mengganti metode ini, karena Anda tidak ingin melakukan penyesuaian apa pun pada metode penyalinan.

Bagaimanapun, jika Anda ingin menyesuaikan salinan dalam (misalnya dengan berbagi beberapa atribut dan menyalin yang lain), berikut solusinya:

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
Peter
sumber
Bukankah klon juga memerlukan __deepcopy__metode reset karena akan memiliki __deepcopy__= Tidak ada?
flutefreak7
2
Nggak. Jika __deepcopy__metode tidak ditemukan (atau obj.__deepcopy__mengembalikan Tidak ada), maka deepcopykembali ke fungsi penyalinan dalam standar. Ini dapat dilihat di sini
Peter
1
Tapi kemudian b tidak akan memiliki kemampuan untuk melakukan deepcopy dengan berbagi? c = deepcopy (a) akan berbeda dari d = deepcopy (b) karena d akan menjadi deepcopy default di mana c akan memiliki beberapa atribut bersama dengan a.
flutefreak7
1
Ah, sekarang saya mengerti apa yang Anda katakan. Poin yang bagus. Saya memperbaikinya, saya pikir, dengan menghapus __deepcopy__=Noneatribut palsu dari klon. Lihat kode baru.
Peter
1
mungkin jelas bagi ahli python: jika Anda menggunakan kode ini di python 3, ubah "untuk attr, val di shared_attributes.iteritems ():" dengan "untuk attr, val di shared_attributes.items ():"
complexM
6

Saya mungkin sedikit kurang spesifik, tapi ini dia;

Dari copydokumen ;

  • Salinan dangkal membangun objek gabungan baru dan kemudian (sejauh mungkin) menyisipkan referensi ke dalamnya ke objek yang ditemukan dalam aslinya.
  • Salinan dalam membuat objek gabungan baru dan kemudian, secara rekursif, menyisipkan salinan objek yang ditemukan di aslinya ke dalamnya.

Dengan kata lain: copy()akan menyalin hanya elemen atas dan meninggalkan sisanya sebagai penunjuk ke dalam struktur aslinya. deepcopy()akan menyalin semuanya secara rekursif.

Artinya, deepcopy()itulah yang Anda butuhkan.

Jika Anda perlu melakukan sesuatu yang sangat spesifik, Anda dapat menggantinya __copy__()atau __deepcopy__(), seperti yang dijelaskan di manual. Secara pribadi, saya mungkin akan mengimplementasikan fungsi biasa (misalnya config.copy_config()atau semacamnya) untuk membuatnya jelas bahwa itu bukan perilaku standar Python.

Morten Siebuhr
sumber
3
Agar kelas dapat menentukan implementasi salinnya sendiri, ia dapat menentukan metode khusus __copy__() dan __deepcopy__(). docs.python.org/library/copy.html
SilentGhost
Saya akan memeriksa ulang kode saya, terima kasih. Saya akan merasa bodoh jika ini adalah bug sederhana di tempat lain :-P
Brent Menulis Kode
@MortenSiebuhr Anda benar. Saya tidak sepenuhnya jelas bahwa copy / deepcopy akan melakukan apa pun secara default tanpa saya mengganti fungsi tersebut. Saya sedang mencari kode sebenarnya yang dapat saya ubah nanti (misalnya jika saya tidak ingin menyalin semua atribut), jadi saya memberi Anda suara-atas tetapi saya akan memilih jawaban @ AlexMartinelli. Terima kasih!
Brent Menulis Kode
2

The copymodul menggunakan akhirnya __getstate__()/ acar protokol , sehingga ini juga target yang valid untuk override.__setstate__()

Implementasi default hanya mengembalikan dan menyetel __dict__kelas, jadi Anda tidak perlu menelepon super()dan mengkhawatirkan trik pintar Eino Gourdin di atas .

ankostis
sumber
1

Membangun jawaban bersih Antony Hatchkins, inilah versi saya di mana kelas yang dimaksud berasal dari kelas khusus lain (yang perlu kita panggil 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
Boltzmann Otak
sumber