Làm thế nào để ghi đè các hoạt động sao chép / bản ghi sâu cho một đối tượng Python?


100

Tôi hiểu sự khác biệt giữa copyso với deepcopytrong mô-đun sao chép. Tôi đã sử dụng copy.copycopy.deepcopytrước đó thành công, nhưng đây là lần đầu tiên tôi thực sự đi về quá tải các phương thức __copy____deepcopy__. Tôi đã google xung quanh và nhìn qua được xây dựng trong module Python để tìm kiếm các trường hợp của __copy____deepcopy__chức năng (ví dụ sets.py, decimal.pyfractions.py), nhưng tôi vẫn không chắc chắn 100% tôi đã nhận nó đúng.

Đây là kịch bản của tôi:

Tôi có một đối tượng cấu hình. Ban đầu, tôi sẽ khởi tạo một đối tượng cấu hình với một bộ giá trị mặc định. Cấu hình này sẽ được chuyển giao cho nhiều đối tượng khác (để đảm bảo tất cả các đối tượng bắt đầu với cùng một cấu hình). Tuy nhiên, một khi tương tác của người dùng bắt đầu, mỗi đối tượng cần phải điều chỉnh cấu hình của nó một cách độc lập mà không ảnh hưởng đến cấu hình của nhau (điều này nói với tôi rằng tôi sẽ cần tạo deepcopys của cấu hình ban đầu để xử lý).

Đây là một đối tượng mẫu:

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 

Cách thực hiện đúng copydeepcopycác phương pháp trên đối tượng này là gì để đảm bảo copy.copycopy.deepcopycung cấp cho tôi các hành vi phù hợp?


Nó có hoạt động không? Có vấn đề gì không?
Ned Batchelder, 30/09/09

Tôi nghĩ rằng tôi vẫn gặp sự cố với các tài liệu tham khảo được chia sẻ, nhưng hoàn toàn có thể tôi đã nhầm lẫn ở nơi khác. Tôi sẽ kiểm tra lại dựa trên bài đăng của @ MortenSiebuhr khi tôi có cơ hội và cập nhật kết quả.
Brent viết mã vào

Từ hiểu biết hạn chế hiện tại của tôi, tôi hy vọng copy.deepcopy (ChartConfigInstance) sẽ trả về một phiên bản mới sẽ không có bất kỳ tham chiếu được chia sẻ nào với bản gốc (mà không cần tự thực hiện lại deepcopy). Điều này không chính xác?
emschorsch

Câu trả lời:


81

Các đề xuất để tùy chỉnh nằm ở cuối trang tài liệu :

Các lớp có thể sử dụng các giao diện giống nhau để kiểm soát việc sao chép mà chúng sử dụng để kiểm soát việc tẩy rửa. Xem mô tả của mô-đun dưa để biết thông tin về các phương pháp này. Mô-đun sao chép không sử dụng mô-đun đăng ký copy_reg.

Để một lớp xác định việc triển khai bản sao của chính nó, nó có thể định nghĩa các phương thức đặc biệt __copy__()__deepcopy__(). Cái trước được gọi để thực hiện hoạt động sao chép nông; không có đối số bổ sung nào được thông qua. Sau đó được gọi để thực hiện hoạt động sao chép sâu; nó được thông qua một đối số, từ điển ghi nhớ. Nếu việc __deepcopy__() triển khai cần tạo bản sao sâu của một thành phần, thì nó nên gọi deepcopy()hàm với thành phần là đối số đầu tiên và từ điển ghi nhớ làm đối số thứ hai.

Vì bạn dường như không quan tâm đến việc tùy chỉnh ngâm, xác định __copy____deepcopy__chắc chắn có vẻ như là cách phù hợp với bạn.

Cụ thể, __copy__(bản sao cạn) khá dễ dàng trong trường hợp của bạn ...:

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

__deepcopy__sẽ tương tự (chấp nhận cả một đối số memo) nhưng trước khi trả về, nó sẽ phải gọi self.foo = deepcopy(self.foo, memo)bất kỳ thuộc tính self.foonào cần sao chép sâu (về cơ bản các thuộc tính là vùng chứa - danh sách, dicts, các đối tượng không nguyên thủy chứa các thứ khác thông qua các thuộc tính của chúng __dict__).


1
@kaizer, họ có thể tùy chỉnh ngâm / giải nén cũng như sao chép, nhưng nếu bạn không quan tâm đến quá trình tẩy, thì việc sử dụng đơn giản và trực tiếp hơn __copy__/ __deepcopy__.
Alex Martelli

4
Đó dường như không phải là bản dịch trực tiếp của copy / deepcopy. Cả bản sao và bản ghi sâu đều không gọi hàm tạo của đối tượng được sao chép. Hãy xem xét ví dụ này. class Test1 (object): def init __ (self): print "% s.% s"% (self .__ class .__ name__, " init ") class Test2 (Test1): def __copy __ (self): new = type (self) () trả t1 mới = Test1 () copy.copy (t1) t2 = Test2 () copy.copy (t2)
Rob trẻ

12
Tôi nghĩ thay vì type (self) (), bạn nên sử dụng cls = self .__ class__; cls .__ new __ (cls) không nhạy cảm với giao diện hàm tạo (đặc biệt đối với phân lớp). Tuy nhiên, nó không thực sự quan trọng ở đây.
Juh_ Ngày

11
Tại sao self.foo = deepcopy(self.foo, memo)...? Bạn không thực sự muốn nói newone.foo = ...?
Alois Mahdal

4
Bình luận của @ Juh_ được đưa ra. Bạn không muốn gọi __init__. Đó không phải là những gì bản sao làm. Ngoài ra, thường có một trường hợp sử dụng mà việc ngâm và sao chép cần phải khác nhau. Trên thực tế, tôi thậm chí không biết tại sao bản sao lại cố gắng sử dụng giao thức ngâm theo mặc định. Sao chép là để thao tác trong bộ nhớ, tẩy là để tồn tại giữa các kỷ nguyên; chúng là những thứ hoàn toàn khác nhau và có rất ít mối quan hệ với nhau.
Nimrod

96

Kết hợp câu trả lời của Alex Martelli và nhận xét của Rob Young, bạn sẽ nhận được mã sau:

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

bản in

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

ở đây __deepcopy__điền vào memodict để tránh sao chép thừa trong trường hợp bản thân đối tượng được tham chiếu từ thành viên của nó.


2
@bytestorm là Transportergì?
Antony Hatchkins

@AntonyHatchkins Transporterlà tên của lớp tôi đang viết. Đối với lớp đó, tôi muốn ghi đè hành vi deepcopy.
bytestorm

1
@bytestorm nội dung của là Transportergì?
Antony Hatchkins

1
Tôi nghĩ __deepcopy__nên bao gồm một bài kiểm tra để tránh đệ quy vô hạn: <! - language: lang-python -> d = id (self) result = memo.get (d, None) nếu kết quả không phải là None: return result
Antonín Hoskovec

@AntonyHatchkins Nó không ngay lập tức rõ ràng từ bài viết của bạn memo[id(self)] thực sự được sử dụng để ngăn chặn đệ quy vô hạn. Tôi đã tổng hợp một ví dụ ngắn gọn gợi ý rằng copy.deepcopy()nội bộ hủy lệnh gọi đến một đối tượng nếu nó id()là khóa của memo, đúng không? Nó cũng đáng chú ý là deepcopy()dường như để làm điều này ngày của riêng mình bằng cách mặc định , mà làm cho nó khó có thể tưởng tượng một trường hợp xác định __deepcopy__bằng tay là thực sự cần thiết ...
Jonathan H

14

Sau câu trả lời tuyệt vời của Peter , để triển khai một bản deepcopy tùy chỉnh, với sự thay đổi tối thiểu đối với việc triển khai mặc định (ví dụ: chỉ cần sửa đổi một trường như tôi cần):

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
điều này có được ưu tiên sử dụng delattr(self, '__deepcopy__')sau đó setattr(self, '__deepcopy__', deepcopy_method)không?
joel

Theo câu trả lời này , cả hai đều tương đương; nhưng setattr hữu ích hơn khi thiết lập một thuộc tính có tên là động / không được biết tại thời điểm mã hóa.
Eino Gourdin

8

Vấn đề của bạn không rõ ràng là tại sao bạn cần ghi đè các phương thức này, vì bạn không muốn thực hiện bất kỳ tùy chỉnh nào đối với các phương thức sao chép.

Nhưng dù sao, nếu bạn muốn tùy chỉnh bản sao sâu (ví dụ: bằng cách chia sẻ một số thuộc tính và sao chép các thuộc tính khác), đây là một giải pháp:

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

Không phải bản sao cũng cần __deepcopy__thiết lập lại phương thức của nó vì nó sẽ có __deepcopy__= Không?
flanfreak7

2
Không. Nếu __deepcopy__phương thức không được tìm thấy (hoặc obj.__deepcopy__trả về Không có), thì hãy deepcopyquay lại hàm sao chép sâu tiêu chuẩn. Điều này có thể được nhìn thấy ở đây
Peter

1
Nhưng rồi b sẽ không có khả năng deepcopy với share? c = deepcopy (a) sẽ khác với d = deepcopy (b) vì d sẽ là deepcopy mặc định trong đó c sẽ có một số tệp đính kèm được chia sẻ với a.
sáofreak7

1
À, giờ tôi hiểu bạn đang nói gì. Điểm tốt. Tôi đã sửa nó, tôi nghĩ, bằng cách xóa __deepcopy__=Nonethuộc tính giả khỏi bản sao. Xem mã mới.
Peter

1
có thể rõ ràng với các chuyên gia về python: nếu bạn sử dụng mã này trong python 3, hãy thay đổi "cho attr, val trong shared_attributes.iteritems ():" với "cho attr, val trong shared_attributes.items ():"
complexM

6

Tôi có thể hơi thiếu về các chi tiết cụ thể, nhưng đây là;

Từ các copytài liệu ;

  • Một bản sao cạn xây dựng một đối tượng ghép mới và sau đó (trong phạm vi có thể) chèn các tham chiếu vào nó với các đối tượng được tìm thấy trong bản gốc.
  • Bản sao sâu tạo ra một đối tượng ghép mới và sau đó, một cách đệ quy, chèn các bản sao vào nó của các đối tượng được tìm thấy trong bản gốc.

Nói cách khác: copy()sẽ chỉ sao chép phần tử trên cùng và để phần còn lại dưới dạng con trỏ vào cấu trúc ban đầu. deepcopy()sẽ sao chép đệ quy mọi thứ.

Đó là, deepcopy()là những gì bạn cần.

Nếu bạn cần làm điều gì đó thực sự cụ thể, bạn có thể ghi đè __copy__()hoặc __deepcopy__(), như được mô tả trong sách hướng dẫn. Cá nhân, tôi có thể sẽ triển khai một hàm đơn giản (ví dụ config.copy_config()hoặc tương tự) để làm cho nó rõ ràng rằng nó không phải là hành vi chuẩn của Python.


3
Để một lớp xác định việc triển khai bản sao của chính nó, nó có thể định nghĩa các phương thức đặc biệt __copy__() và __deepcopy__(). docs.python.org/library/copy.html
SilentGhost 30/09/09

Tôi sẽ kiểm tra lại mã của mình, cảm ơn. Tôi sẽ cảm thấy ngớ ngẩn nếu điều này là một đơn giản :-P lỗi ở nơi khác
Brent Viết Mã

@MortenSiebuhr Bạn đã đúng. Tôi không hoàn toàn rõ ràng rằng copy / deepcopy sẽ làm bất cứ điều gì theo mặc định mà không cần tôi ghi đè các chức năng đó. Tôi đang tìm kiếm mã thực tế mặc dù tôi có thể chỉnh sửa sau (ví dụ: nếu tôi không muốn sao chép tất cả các thuộc tính), vì vậy tôi đã cho bạn một phiếu bầu nhưng tôi sẽ đi với câu trả lời của @ AlexMartinelli. Cảm ơn!
Brent viết mã

2

Các copyModule này sử dụng cuối cùng là __getstate__()/ tẩy giao thức , vì vậy đây cũng là mục tiêu hợp lệ để ghi đè.__setstate__()

Việc triển khai mặc định chỉ trả về và đặt __dict__lớp, vì vậy bạn không cần phải gọi super()và lo lắng về thủ thuật thông minh của Eino Gourdin ở trên .


1

Dựa trên câu trả lời rõ ràng của Antony Hatchkins, đây là phiên bản của tôi trong đó lớp được đề cập đến từ một lớp tùy chỉnh khác (chúng ta cần gọi 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.