Note: see the link below for the English version of this article.
Việc tạo deep copy cho object là tương đối phức tạp. Thật may là Python có hàm deepcopy
trong module copy
để giúp ta đơn giản hóa quá trình này. Trong bài hôm nay, chúng ta sẽ phân tích hàm deepcopy
và làm rõ cách thức hoạt động của nó.
Giới thiệu hàm deepcopy
Từ cái tên, ta có thể đoán được hàm này có thể tạo deep copy cho object. Có nghĩa là nếu trong object của ta lại chứa các object khác thì deepcopy
sẽ tạo bản sao của các object con thay vì chỉ copy reference tới các object con đó.
from copy import deepcopy
inner = [1, 2, 3]
outer = [inner, 'a', 'b']
outer_copy = deepcopy(outer)
outer_copy[0] is outer[0] # False
Tuy nhiên, nếu nhiều object con có cùng reference tới một object thì deepcopy
cũng chỉ tạo một bản sao.
inner = [1, 2, 3]
outer = [inner, inner]
outer[0] is outer[1] # True
outer_copy = deepcopy(outer)
outer_copy[0] is outer[0] # False
outer_copy[0] is outer_copy[1] # True
Và hàm này cũng đủ thông minh để xử lý các object chứa reference về lại chính nó.
a = [1,2,3]
b = [1, a]
a[0] = b
a[0][1] is a # True
a_copy = deepcopy(a) # Không bị lỗi
a_copy[0][1] is a_copy # True
Vậy deepcopy
đã làm những điều này bằng cách nào? Chúng ta sẽ tìm câu trả lời bằng cách đọc mã nguồn.
Phương thức hoạt động của hàm deepcopy
Các bước để tạo một bản sao
Từ docstring của module copy
, ta phần nào đoán được cách deepcopy
xử lý các object có chứa reference về chính nó.
(tạm dịch)
Trong quá trình tạo deepcopy, Python tránh các vấn đề trên bằng cách sau:
a) lưu lại các object đã được copy trước đó
b) cho phép người dùng tự định nghĩa lại bước copy trong các lớp
họ tự tạo
Để hiểu rõ hơn các điểm trên, ta cần xem phần code của hàm deepcopy
tại đây. Tổng quan các bước của quá trình copy là như sau.
- Tạo một
dict
để làm cache lưu các object đã được copy. - Dùng hàm
id
để tính identity của object cần copy rồi tìm ID đó trong cache. Nếu trong cache đã chứa bản sao tương ứng thì ta trả lại bản sao đó. Còn nếu không thì ta thực hiện tiếp các bước dưới.d = id(x) y = memo.get(d, _nil)
- Tìm trong một
dict
khác gọi là_deepcopy_dispatch
để xem ta đã biết cách copy kiểu object hiện tại chưa. Nếu đã biết rồi thì ta trả lại hàm copy tương ứng. Còn nếu không thì ta thực hiện tiếp các bước dưới.cls = type(x) copier = _deepcopy_dispatch.get(cls)
- Nếu object cần copy có định nghĩa hàm
deepcopy
thì ta dùng nó để tạo bản sao (đây là cách ta định nghĩa lại quá trình copy). Còn nếu không thấy hàm đó thì ta thực hiện tiếp các bước dưới.copier = getattr(x, "__deepcopy__", None)
- Nếu tất cả các bước trên đều thất bại thì ta sử dụng logic của quá trình pickling/unpickling để tạo bản sao.
- Bước cuối cùng là lưu bản sao vào
cache
.if y is not x: memo[d] = y
Cách copy các kiểu dữ liệu mặc định
Từ đoạn này, ta có thể thấy value type (và một vài reference type đặc biệt) có thể được copy bằng hàm _deepcopy_atomic
. Và hàm này chỉ đơn giản là trả lại đúng giá trị của object gốc. Đó là vì với value type, ta không thể phân biệt 2 object cùng giá trị. Và việc thay đổi một object hoàn toàn không ảnh hưởng đến các object khác cùng giá trị.
def _deepcopy_atomic(x, memo):
return x
Với kiểu list/tuple/dictionary
, ta dùng hàm _deepcopy_list, _deepcopy_tuple, hoặc _deepcopy_dict. Mặc dù có một vài khác biệt nhỏ giữa các hàm này, nhưng nói chung là chúng tạo collection rỗng, gọi hàm deepcopy
trên từng thành phần của object ban đầu, rồi nhét các bản sao vào trong collection.
Ta còn thấy một trường hợp thú vị khác, đó là việc copy hàm. Như ta đã biết, trong Python hàm cũng là một object. Tức là ta cũng có thể chứa hàm trong list/tuple/dictionary
… Vậy khi ta copy một hàm thì bản sao đó sẽ được gắn với object nào? Câu trả lời là nó sẽ được gắn với một bản sao của object gốc, và bản sao đó cũng được tạo bởi chính deepcopy
.
def _deepcopy_method(x, memo): # Copy instance methods
return type(x)(x.__func__, deepcopy(x.__self__, memo))
Ở đây, ta có thể thấy deepcopy
sử dụng types.MethodType
. Đây là một tính năng ít dùng trong Python.
Nếu ta không tìm được hàm copy nào phù hợp thì sao?
Nếu kiểu object cần copy là do người dùng tự định nghĩa thì sao? Lúc này deepcopy
sẽ tạo bản sao bằng cách tái sử dụng quá trình serialize/deserialize như trong module pickle
. Tùy theo object cần copy và phiên bản Python đang được sử dụng, ta sẽ dùng pickle protocol phiên bản 1 hoặc phiên bản 4. Nhưng phiên bản 4 được ưu tiên hơn.
Giả sử ta có object dưới đây và nó hỗ trợ pickle protocol phiên bản 4, ta sẽ copy nó như thế nào?
class Person:
def __init__(self, name):
self.name = name
def introduce(self):
print(f'{id(self)}: My name is {self.name}')
p = Person('John Doe')
Bước đầu tiên là gọi hàm __reduce_ex__
của object đó.
func, args, state, _, _ = d.__reduce_ex__(4)
Các bạn có thể tham khảo ý nghĩa của từng giá trị trả về tại đây (đường link trên là của hàm __reduce__
, nhưng hàm __reduce_ex__
hầu như tương tự). Vì lớp Person
không kế thừa list
hay dict
, ta chỉ cần quan tâm tới 3 giá trị đầu tiên.
func
<function __newobj__ at 0x000002392D419280>
args
(<class '__main__.Person'>,)
state
{'name': 'John Doe'}
Ta dùng func
và args
để tạo một object mới với kiểu là Person
.
p1 = func(*args)
Rồi ta dùng state
để phục hồi lại giá trị cho các attribute trong bản copy. Vì ta đang thực hiện deep copy nên ta phải tạo bản sao cho chính state
.
state = deepcopy(state, memo) # Chú ý ta gọi deepcopy một cách đệ quy tại đây
p1.__dict__.update(state)
Tất cả các bước ở trên được thực hiện trong một hàm private của module copy
với tên gọi là _reconstruct.
Thử một vài tính năng của hàm deepcopy
Truyền memo khi sử dụng hàm deepcopy
Ta sẽ tạo một ví dụ để xem chuyện gì xảy ra nếu ta truyền vào tham số memo
khi gọi hàm deepcopy
.
first_inner = [1, 2, 3]
second_inner = [4, 5, 6]
outer = [first_inner, second_inner]
memo = { id(second_inner): second_inner }
outer_copy = deepcopy(outer, memo)
Cần nhớ là đầu tiên deepcopy
sẽ tìm bản sao của object cần copy trong memo
. Vì trong tham số memo
ta đã định nghĩa sẵn một entry cho second_inner
, deepcopy
không cần copy list này nữa.
outer[0] is outer_copy[0] # False
outer[1] is outer_copy[1] # True do sử dụng luôn giá trị trong memo
outer[1].append(1989)
print(outer_copy[1]) # [4, 5, 6, 1989]
Tự định nghĩa lại quá trình deep copy
Như đã nói ở trên, nếu object cần copy đã định nghĩa sẵn hàm __deepcopy__
thì Python sẽ sử dụng luôn hàm đó. Ta sẽ tạo một object mà người khác không copy được.
class Uncopyable:
def __init__(self, attributes):
self.attribute = attributes
def __deepcopy__(self, memo):
raise NotImplementedError('Không được copy object này.')
def show(self):
return self.attribute
Ta có thể tạo shallow copy cho Uncopyable
, nhưng ta không thể tạo deep copy.
from copy import copy
u = Uncopyable([1,2,3,4,5])
u_shallow = copy(u) # OK
u_deep = deepcopy(u) # Exception has occurred: NotImplementedError: Không được copy object này.
Kết thúc
Tôi tin rằng việc đọc mã nguồn của các module hệ thống là cách hiệu quả để hiểu rõ hơn về một ngôn ngữ lập trình. Ở đây, tôi không ngờ rằng khi nghiên cứu về deepcopy
tôi lại biết thêm một số điều thú vị về module pickle
.
One Thought on “Deep copy trong Python hoạt động như thế nào?”