จะลบล้างการดำเนินการคัดลอก / deepcopy สำหรับวัตถุ Python ได้อย่างไร


104

ฉันเข้าใจความแตกต่างระหว่างcopyเทียบกับdeepcopyในโมดูลการคัดลอก ฉันเคยใช้มาก่อนcopy.copyและcopy.deepcopyประสบความสำเร็จ แต่นี่เป็นครั้งแรกที่ฉันได้ใช้งาน__copy__และ__deepcopy__วิธีการมากเกินไป ฉัน Googled แล้วรอบ ๆ และมองผ่านโมดูลในตัวงูหลามที่จะมองหาอินสแตนซ์ของ__copy__และ__deepcopy__ฟังก์ชั่น (เช่นsets.py, decimal.pyและfractions.py) แต่ฉันยังคงไม่แน่ใจ 100% ผมเคยได้มันขวา

นี่คือสถานการณ์ของฉัน:

ฉันมีวัตถุการกำหนดค่า ในขั้นต้นฉันจะสร้างอินสแตนซ์วัตถุการกำหนดค่าหนึ่งรายการด้วยชุดค่าเริ่มต้น การกำหนดค่านี้จะถูกส่งต่อไปยังวัตถุอื่น ๆ หลายรายการ (เพื่อให้แน่ใจว่าวัตถุทั้งหมดเริ่มต้นด้วยการกำหนดค่าเดียวกัน) อย่างไรก็ตามเมื่อการโต้ตอบกับผู้ใช้เริ่มต้นขึ้นแต่ละออบเจ็กต์จะต้องปรับแต่งการกำหนดค่าโดยอิสระโดยไม่ส่งผลกระทบต่อการกำหนดค่าของกันและกัน (ซึ่งบอกว่าฉันจะต้องสร้าง deepcopys ของการกำหนดค่าเริ่มต้นของฉันเพื่อส่งต่อ)

นี่คือวัตถุตัวอย่าง:

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.copyและcopy.deepcopyให้พฤติกรรมที่เหมาะสมแก่ฉัน


ได้ผลหรือไม่? มีปัญหาหรือเปล่า
Ned Batchelder

ฉันคิดว่าฉันยังคงมีปัญหากับการอ้างอิงที่แชร์ แต่เป็นไปได้ทั้งหมดที่ฉันทำผิดที่อื่น ฉันจะตรวจสอบอีกครั้งตามโพสต์ของ @ MortenSiebuhr เมื่อฉันมีโอกาสและอัปเดตพร้อมกับผลลัพธ์
Brent Writes Code

จากความเข้าใจที่ จำกัด ของฉันในขณะนี้ฉันคาดว่า copy.deepcopy (ChartConfigInstance) จะส่งคืนอินสแตนซ์ใหม่ซึ่งจะไม่มีการอ้างอิงร่วมกับต้นฉบับ (โดยไม่ต้องใช้ Deepcopy ซ้ำด้วยตัวคุณเอง) สิ่งนี้ไม่ถูกต้องหรือไม่?
emschorsch

คำตอบ:


83

คำแนะนำสำหรับการปรับแต่งอยู่ที่ท้ายสุดของหน้าเอกสาร :

คลาสสามารถใช้อินเทอร์เฟซเดียวกันเพื่อควบคุมการคัดลอกที่ใช้ควบคุมการดอง ดูคำอธิบายของโมดูลดองสำหรับข้อมูลเกี่ยวกับวิธีการเหล่านี้ โมดูลการคัดลอกไม่ใช้โมดูลการลงทะเบียน copy_reg

ในการสั่งซื้อสำหรับการเรียนเพื่อกำหนดดำเนินงานสำเนาของตัวเองก็สามารถกำหนดวิธีการพิเศษและ__copy__() __deepcopy__()อดีตถูกเรียกให้ดำเนินการคัดลอกตื้น ไม่มีการส่งผ่านข้อโต้แย้งเพิ่มเติม หลังถูกเรียกให้ใช้การดำเนินการสำเนาลึก มันถูกส่งผ่านหนึ่งอาร์กิวเมนต์พจนานุกรมบันทึก หาก__deepcopy__() การนำไปใช้งานจำเป็นต้องสร้างสำเนาลึกของส่วนประกอบควรเรียกใช้deepcopy()ฟังก์ชันที่มีองค์ประกอบเป็นอาร์กิวเมนต์แรกและพจนานุกรมบันทึกเป็นอาร์กิวเมนต์ที่สอง

เนื่องจากคุณดูเหมือนจะไม่สนใจเกี่ยวกับการปรับแต่งการดองการกำหนด__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ที่ต้องการการคัดลอกแบบลึก (โดยพื้นฐานแล้วแอตทริบิวต์ที่เป็นคอนเทนเนอร์ - รายการคำสั่งวัตถุที่ไม่ใช่แบบดั้งเดิมซึ่งเก็บสิ่งอื่น ๆ ผ่านทาง__dict__s)


1
@kaizer พวกเขากำลังที่ดีในการปรับแต่งดอง / unpickling เช่นเดียวกับการคัดลอก แต่ถ้าคุณไม่สนใจเกี่ยวกับการดองก็ง่ายและตรงกับการใช้งาน/__copy__ __deepcopy__
Alex Martelli

4
ดูเหมือนจะไม่ใช่การแปลโดยตรงจาก copy / deepcopy ไม่ว่าจะคัดลอกหรือ deepcopy เรียกตัวสร้างของวัตถุที่กำลังคัดลอก ลองพิจารณาตัวอย่างนี้ class Test1 (object): def init __ (self): พิมพ์ "% 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
ฉันคิดว่าแทนที่จะเป็นประเภท (self) () คุณควรใช้ cls = self .__ class__; cls .__ new __ (cls) ที่จะไม่ไวต่ออินเทอร์เฟซตัวสร้าง (โดยเฉพาะสำหรับคลาสย่อย) อย่างไรก็ตามมันไม่สำคัญจริงๆที่นี่
Juh_

11
ทำไมself.foo = deepcopy(self.foo, memo)... ? คุณไม่ได้ตั้งใจจริงnewone.foo = ...เหรอ?
Alois Mahdal

4
ความคิดเห็นของ @ Juh_ เป็นจุดที่ คุณไม่ต้องการโทร__init__. นั่นไม่ใช่สิ่งที่ทำสำเนา นอกจากนี้มักจะมีกรณีการใช้งานที่การดองและการคัดลอกต้องแตกต่างกัน อันที่จริงฉันไม่รู้ด้วยซ้ำว่าทำไมสำเนาถึงพยายามใช้โปรโตคอลการดองโดยค่าเริ่มต้น การคัดลอกมีไว้สำหรับการจัดการในหน่วยความจำการดองมีไว้สำหรับการคงอยู่ข้ามยุค พวกเขาเป็นสิ่งที่แตกต่างกันโดยสิ้นเชิงที่มีความสัมพันธ์กันเล็กน้อย
Nimrod

102

การรวบรวมคำตอบของ 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เพื่อหลีกเลี่ยงการคัดลอกมากเกินไปในกรณีที่ออบเจ็กต์ถูกอ้างอิงจากสมาชิก


2
@bytestorm คือTransporterอะไร?
Antony Hatchkins

@AntonyHatchkins Transporterเป็นชื่อชั้นของฉันที่ฉันเขียน สำหรับคลาสนั้นฉันต้องการลบล้างลักษณะการทำงานของ Deepcopy
bytestorm

1
@bytestorm เนื้อหาTransporterคืออะไร?
Antony Hatchkins

1
ฉันคิดว่า__deepcopy__ควรรวมการทดสอบเพื่อหลีกเลี่ยงการเรียกซ้ำแบบไม่มีที่สิ้นสุด: <! - language: lang-python -> d = id (self) result = memo.get (d, None) หากผลลัพธ์ไม่ใช่ไม่มี: return result
Antonín Hoskovec

@AntonyHatchkins ยังไม่ชัดเจนในทันทีจากโพสต์ของคุณที่ memo[id(self)]ถูกนำมาใช้เพื่อป้องกันการเรียกซ้ำแบบไม่สิ้นสุด ฉันได้รวบรวมตัวอย่างสั้น ๆซึ่งชี้ให้เห็นว่าcopy.deepcopy()ภายในยกเลิกการเรียกไปยังวัตถุถ้ามันid()เป็นกุญแจของmemoถูกต้อง? นอกจากนี้ยังเป็นที่น่าสังเกตว่าdeepcopy()ดูเหมือนว่าจะทำสิ่งนี้ด้วยตัวเองโดยค่าเริ่มต้นซึ่งทำให้ยากที่จะจินตนาการถึงกรณีที่__deepcopy__จำเป็นต้องกำหนดด้วยตนเองจริงๆ ...
Jonathan H

15

ทำตามคำตอบที่ยอดเยี่ยมของ Peterเพื่อใช้งาน Deepcopy ที่กำหนดเองโดยมีการปรับเปลี่ยนการใช้งานเริ่มต้นเพียงเล็กน้อย (เช่นเพียงแค่แก้ไขฟิลด์ตามที่ฉันต้องการ):

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__')แล้วsetattr(self, '__deepcopy__', deepcopy_method)หรือไม่?
joel

ตามคำตอบนี้ทั้งสองเทียบเท่ากัน แต่ setattr มีประโยชน์มากกว่าเมื่อตั้งค่าแอตทริบิวต์ที่มีชื่อเป็นแบบไดนามิก / ไม่ทราบในเวลาเข้ารหัส
Eino Gourdin

1
นี่คือรายการโปรดส่วนตัวของฉันและฉันกำลังใช้มันในการผลิตที่วัตถุมีคนตัดไม้ซึ่งจะมีการล็อคด้ายซึ่งไม่สามารถดองได้ บันทึกคนตัดไม้ตั้งค่าเป็นNoneเรียกค่าเริ่มต้นสำหรับสิ่งอื่น ๆ จากนั้นใส่กลับ พิสูจน์อนาคตเพราะฉันไม่ต้องกังวลว่าจะลืมจัดการสนามและสืบทอดคลาส "แค่ทำงาน"
Aaron D. Marasco

BTW ฉันลองใช้delattr()แล้วและล้มเหลวใน Python2.7 ด้วยAttributeError. "ตั้งค่าเป็นNone" คือสิ่งที่ฉันเคยใช้
Aaron D. Marasco

9

ยังไม่ชัดเจนจากปัญหาของคุณว่าทำไมคุณถึงต้องแทนที่วิธีการเหล่านี้เนื่องจากคุณไม่ต้องการปรับแต่งวิธีการคัดลอกใด ๆ

อย่างไรก็ตามหากคุณต้องการปรับแต่งสำเนาลึก (เช่นโดยการแบ่งปันคุณสมบัติบางอย่างและคัดลอกผู้อื่น) นี่คือวิธีแก้ปัญหา:

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

โคลนไม่จำเป็นต้อง__deepcopy__รีเซ็ตวิธีการเนื่องจากจะมี__deepcopy__= ไม่มี?
flutefreak7

2
ไม่ หาก__deepcopy__ไม่พบวิธีการ (หรือobj.__deepcopy__คืนค่าไม่มี) ให้deepcopyกลับไปใช้ฟังก์ชันการคัดลอกแบบลึกมาตรฐาน สามารถดูได้ที่นี่
ปีเตอร์

1
แต่ b จะไม่มีความสามารถในการคัดลอกลึกด้วยการแบ่งปัน? c = deepcopy (a) จะแตกต่างจาก d = deepcopy (b) เนื่องจาก d จะเป็น deepcopy เริ่มต้นโดยที่ c จะมี Attrs ร่วมกับ a.
flutefreak7

1
อ่าตอนนี้ฉันเห็นสิ่งที่คุณกำลังพูด จุดดี. ฉันแก้ไขแล้วฉันคิดว่าโดยการลบ__deepcopy__=Noneแอตทริบิวต์ปลอมออกจากโคลน ดูรหัสใหม่
ปีเตอร์

1
อาจเป็นที่ชัดเจนสำหรับผู้เชี่ยวชาญ python: หากคุณใช้รหัสนี้ใน python 3 ให้เปลี่ยน "for attr, val ใน shared_attributes.iteritems ():" with "for attr, val ใน 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
Brent Writes Code

@MortenSiebuhr คุณถูกต้อง ฉันไม่ชัดเจนว่า copy / deepcopy จะทำทุกอย่างตามค่าเริ่มต้นโดยที่ฉันไม่ได้ลบล้างฟังก์ชันเหล่านั้น ฉันกำลังมองหารหัสจริงแม้ว่าฉันจะสามารถปรับแต่งได้ในภายหลัง (เช่นถ้าฉันไม่ต้องการคัดลอกแอตทริบิวต์ทั้งหมด) ดังนั้นฉันจึงให้คะแนนคุณ แต่ฉันจะตอบคำถามของ @ AlexMartinelli ขอบคุณ!
Brent Writes Code

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.