Tại sao + = hoạt động bất ngờ trên danh sách?


118

Các +=nhà điều hành trong python dường như được hoạt động bất ngờ trong danh sách. Bất cứ ai có thể cho tôi biết những gì đang xảy ra ở đây?

class foo:  
     bar = []
     def __init__(self,x):
         self.bar += [x]


class foo2:
     bar = []
     def __init__(self,x):
          self.bar = self.bar + [x]

f = foo(1)
g = foo(2)
print f.bar
print g.bar 

f.bar += [3]
print f.bar
print g.bar

f.bar = f.bar + [4]
print f.bar
print g.bar

f = foo2(1)
g = foo2(2)
print f.bar 
print g.bar 

ĐẦU RA

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]

foo += bardường như ảnh hưởng đến mọi trường hợp của lớp, trong khi foo = foo + bardường như hành xử theo cách mà tôi mong đợi mọi thứ sẽ hoạt động.

Các +=nhà điều hành được gọi là "toán tử gán ghép".


cũng thấy sự khác biệt giữa 'mở rộng' và 'nối thêm' trên danh sách
N 1.1

3
Tôi không nghĩ rằng điều này cho thấy điều gì đó sai với Python. Hầu hết các ngôn ngữ thậm chí sẽ không cho phép bạn sử dụng +toán tử trên các mảng. Tôi nghĩ rằng nó có ý nghĩa hoàn hảo trong trường hợp này +=sẽ thêm vào.
Skilldrick

4
Nó chính thức được gọi là 'nhiệm vụ tăng cường'.
Martijn Pieters

Câu trả lời:


138

Câu trả lời chung là +=cố gắng gọi __iadd__phương thức đặc biệt và nếu phương thức đó không khả dụng, nó sẽ cố gắng sử dụng __add__thay thế. Vì vậy, vấn đề là với sự khác biệt giữa các phương pháp đặc biệt này.

Các __iadd__phương pháp đặc biệt là dành cho một sự bổ sung tại chỗ, có nghĩa là nó đột biến đối tượng mà nó hoạt động trên. Các __add__phương pháp đặc biệt trả về một đối tượng mới và cũng được sử dụng cho các tiêu chuẩn +vận hành.

Vì vậy, khi +=toán tử được sử dụng trên một đối tượng đã __iadd__được xác định, đối tượng sẽ được sửa đổi tại chỗ. Nếu không, thay vào đó, nó sẽ cố gắng sử dụng đơn giản __add__và trả về một đối tượng mới.

Đó là lý do tại sao đối với các loại có thể thay đổi như danh sách +=thay đổi giá trị của đối tượng, trong khi đối với các loại bất biến như bộ giá trị, chuỗi và số nguyên, một đối tượng mới được trả về thay thế ( a += btương đương vớia = a + b ).

Đối với các loại hỗ trợ cả hai __iadd____add__do đó bạn phải cẩn thận sử dụng loại nào. a += bsẽ gọi __iadd__và biến đổi a, trong khia = a + b sẽ tạo một đối tượng mới và gán nó cho a. Chúng không phải là hoạt động giống nhau!

>>> a1 = a2 = [1, 2]
>>> b1 = b2 = [1, 2]
>>> a1 += [3]          # Uses __iadd__, modifies a1 in-place
>>> b1 = b1 + [3]      # Uses __add__, creates new list, assigns it to b1
>>> a2
[1, 2, 3]              # a1 and a2 are still the same list
>>> b2
[1, 2]                 # whereas only b1 was changed

Đối với các kiểu bất biến (nơi bạn không có __iadd__) a += ba = a + btương đương. Đây là những gì cho phép bạn sử dụng +=trên các kiểu bất biến, điều này có vẻ là một quyết định thiết kế kỳ lạ cho đến khi bạn cân nhắc rằng nếu không thì bạn không thể sử dụng +=trên các kiểu bất biến như số!


4
Cũng có một __radd__phương thức đôi khi có thể được gọi (nó phù hợp với các biểu thức chủ yếu liên quan đến các lớp con).
jfs 27/02/10

2
Trong quan điểm: + = rất hữu ích nếu bộ nhớ và tốc độ là quan trọng
Norfeldt

3
Biết rằng điều đó +=thực sự mở rộng một danh sách, điều này giải thích tại sao x = []; x = x + {}cho một TypeErrorthời gian x = []; x += {}chỉ trả về [].
zezollo

96

Đối với trường hợp chung, hãy xem câu trả lời của Scott Griffith . Tuy nhiên, khi xử lý các danh sách giống như bạn, +=toán tử là cách viết tắt của someListObject.extend(iterableObject). Xem tài liệu về extension () .

Các extendchức năng sẽ nối tất cả các yếu tố của tham số vào danh sách.

Khi thực hiện, foo += somethingbạn đang sửa đổi danh sách footại chỗ, do đó bạn không thay đổi tham chiếu mà tên footrỏ đến, nhưng bạn đang thay đổi trực tiếp đối tượng danh sách. Vớifoo = foo + something , bạn đang thực sự tạo một danh sách mới .

Mã ví dụ này sẽ giải thích nó:

>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216

Lưu ý cách tham chiếu thay đổi khi bạn gán lại danh sách mới l.

Như barlà một biến lớp thay vì một biến cá thể, việc sửa đổi tại chỗ sẽ ảnh hưởng đến tất cả các thể hiện của lớp đó. Nhưng khi định nghĩa lại self.bar, instance sẽ có một biến instance riêng self.barmà không ảnh hưởng đến các instance của lớp khác.


7
Điều này không phải lúc nào cũng đúng: a = 1; a + = 1; là Python hợp lệ, nhưng int không có bất kỳ phương thức "extension ()" nào. Bạn không thể khái quát điều này.
e-thoả mãn

2
Hoàn thành một số thử nghiệm, Scott Griffiths đã hiểu đúng, vì vậy -1 cho bạn.
e-thoả mãn

11
@ e-Statistics: OP đã nói rõ ràng về danh sách, và tôi đã tuyên bố rõ ràng rằng tôi cũng đang nói về danh sách. Tôi không khái quát bất cứ điều gì.
AndiDog

Loại bỏ -1, câu trả lời là đúng. Tôi vẫn nghĩ câu trả lời của Griffiths tốt hơn.
e-thoả mãn

Lúc đầu, cảm thấy kỳ lạ khi nghĩ rằng a += bnó khác a = a + bvới hai danh sách ab. Nhưng nó có lý; extendthường là mục đích dự định làm với các danh sách hơn là tạo một bản sao mới của toàn bộ danh sách sẽ có độ phức tạp về thời gian cao hơn. Nếu các nhà phát triển cần phải cẩn thận rằng họ không sửa đổi các danh sách ban đầu tại chỗ, thì các bộ giá trị là một lựa chọn tốt hơn là các đối tượng bất biến. +=với bộ giá trị không thể sửa đổi bộ giá trị ban đầu.
Pranjal Mittal

22

Vấn đề ở đây là, barđược định nghĩa là một thuộc tính lớp, không phải là một biến thể hiện.

Trong foo, thuộc tính lớp được sửa đổi trong initphương thức, đó là lý do tại sao tất cả các phiên bản đều bị ảnh hưởng.

Trong foo2, một biến cá thể được xác định bằng cách sử dụng thuộc tính lớp (trống) và mọi cá thể đều cóbar .

Cách triển khai "đúng" sẽ là:

class foo:
    def __init__(self, x):
        self.bar = [x]

Tất nhiên, các thuộc tính của lớp là hoàn toàn hợp pháp. Trên thực tế, bạn có thể truy cập và sửa đổi chúng mà không cần tạo một thể hiện của lớp như thế này:

class foo:
    bar = []

foo.bar = [x]

8

Có hai điều liên quan ở đây:

1. class attributes and instance attributes
2. difference between the operators + and += for lists

+nhà điều hành gọi __add__phương thức trên một danh sách. Nó lấy tất cả các phần tử từ các toán hạng của nó và tạo một danh sách mới chứa các phần tử đó để duy trì thứ tự của chúng.

+= cuộc gọi của nhà điều hành __iadd__phương thức trong danh sách. Nó cần một tệp có thể lặp lại và nối tất cả các phần tử của có thể lặp lại vào danh sách tại chỗ. Nó không tạo một đối tượng danh sách mới.

Trong lớp foo, câu lệnh self.bar += [x]không phải là câu lệnh gán mà thực sự được dịch sang

self.bar.__iadd__([x])  # modifies the class attribute  

sửa đổi danh sách tại chỗ và hoạt động giống như phương thức danh sách extend.

foo2Ngược lại, trong lớp , câu lệnh gán trong initphương thức

self.bar = self.bar + [x]  

có thể được giải cấu trúc thành:
Cá thể không có thuộc tính bar(mặc dù có thuộc tính lớp cùng tên) vì vậy nó truy cập thuộc tính lớp barvà tạo một danh sách mới bằng cách thêm xvào nó. Câu lệnh được dịch thành:

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 

Sau đó, nó tạo ra một thuộc tính instance barvà gán danh sách mới tạo cho nó. Lưu ý rằng bartrên rhs của bài tập khác vớibar trên lhs.

Đối với các thể hiện của lớp foo, barlà một thuộc tính lớp chứ không phải thuộc tính cá thể. Do đó, bất kỳ thay đổi nào đối với thuộc tính lớp barsẽ được phản ánh cho tất cả các trường hợp.

Ngược lại, mỗi cá thể của lớp foo2có thuộc tính cá thể riêng của nó barkhác với thuộc tính lớp cùng tên bar.

f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]  
print f.__class__.bar # accessing the class attribute. prints []  

Hy vọng điều này rõ ràng mọi thứ.


5

Mặc dù thời gian đã trôi qua và nhiều điều đúng đã được nói ra, nhưng không có câu trả lời nào bao hàm cả hai tác dụng.

Bạn có 2 hiệu ứng:

  1. một hành vi "đặc biệt", có thể không được chú ý của danh sách với +=(như Scott Griffiths đã nêu )
  2. thực tế là các thuộc tính lớp cũng như các thuộc tính cá thể có liên quan (như Can Berk Büder đã nêu )

Trong lớp foo, __init__phương thức sửa đổi thuộc tính lớp. Đó là bởi vì self.bar += [x]dịch sang self.bar = self.bar.__iadd__([x]). __iadd__()là để sửa đổi tại chỗ, vì vậy nó sửa đổi danh sách và trả về một tham chiếu đến nó.

Lưu ý rằng instance dict được sửa đổi mặc dù điều này thường không cần thiết vì lớp dict đã chứa cùng một phép gán. Vì vậy, chi tiết này hầu như không được chú ý - ngoại trừ nếu bạn làm foo.bar = []sau đó. Đây là phiên bản củabar vẫn giống nhau nhờ vào thực tế đã nói.

Trong lớp foo2, tuy nhiên, của lớp barđược sử dụng, nhưng không được động đến. Thay vào đó, a [x]được thêm vào nó, tạo thành một đối tượng mới, như self.bar.__add__([x])được gọi ở đây, không sửa đổi đối tượng. Sau đó, kết quả được đưa vào instance dict, cung cấp cho instance đó danh sách mới dưới dạng dict, trong khi thuộc tính của lớp vẫn được sửa đổi.

Sự khác biệt giữa ... = ... + ...... += ...ảnh hưởng cũng như các nhiệm vụ sau đó:

f = foo(1) # adds 1 to the class's bar and assigns f.bar to this as well.
g = foo(2) # adds 2 to the class's bar and assigns g.bar to this as well.
# Here, foo.bar, f.bar and g.bar refer to the same object.
print f.bar # [1, 2]
print g.bar # [1, 2]

f.bar += [3] # adds 3 to this object
print f.bar # As these still refer to the same object,
print g.bar # the output is the same.

f.bar = f.bar + [4] # Construct a new list with the values of the old ones, 4 appended.
print f.bar # Print the new one
print g.bar # Print the old one.

f = foo2(1) # Here a new list is created on every call.
g = foo2(2)
print f.bar # So these all obly have one element.
print g.bar 

Bạn có thể xác minh danh tính của các đối tượng bằng print id(foo), id(f), id(g)(đừng quên bổ sung() s nếu bạn đang sử dụng Python3).

BTW: Người +=điều hành được gọi là "chỉ định tăng cường" và nói chung là nhằm thực hiện các sửa đổi tại chỗ càng nhiều càng tốt.


5

Các câu trả lời khác dường như đã được đề cập khá nhiều, mặc dù nó có vẻ đáng để trích dẫn và tham khảo PEP 203 của Bài tập tăng cường :

Chúng [các toán tử gán tăng cường] triển khai cùng một toán tử như dạng nhị phân thông thường của chúng, ngoại trừ thao tác được thực hiện 'tại chỗ' khi đối tượng bên trái hỗ trợ nó và bên trái chỉ được đánh giá một lần.

...

Ý tưởng đằng sau phép gán tăng cường trong Python là nó không chỉ là cách dễ dàng hơn để viết thông lệ lưu trữ kết quả của một phép toán nhị phân trong toán hạng bên trái của nó, mà còn là một cách để toán hạng bên trái được đề cập biết rằng nó sẽ tự hoạt động, thay vì tạo ra một bản sao sửa đổi của chính nó.


1
>>> elements=[[1],[2],[3]]
>>> subset=[]
>>> subset+=elements[0:1]
>>> subset
[[1]]
>>> elements
[[1], [2], [3]]
>>> subset[0][0]='change'
>>> elements
[['change'], [2], [3]]

>>> a=[1,2,3,4]
>>> b=a
>>> a+=[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
>>> a=[1,2,3,4]
>>> b=a
>>> a=a+[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4])

0
>>> a = 89
>>> id(a)
4434330504
>>> a = 89 + 1
>>> print(a)
90
>>> id(a)
4430689552  # this is different from before!

>>> test = [1, 2, 3]
>>> id(test)
48638344L
>>> test2 = test
>>> id(test)
48638344L
>>> test2 += [4]
>>> id(test)
48638344L
>>> print(test, test2)  # [1, 2, 3, 4] [1, 2, 3, 4]```
([1, 2, 3, 4], [1, 2, 3, 4])
>>> id(test2)
48638344L # ID is different here

Chúng ta thấy rằng khi chúng ta cố gắng sửa đổi một đối tượng không thay đổi (số nguyên trong trường hợp này), Python chỉ đơn giản là cung cấp cho chúng ta một đối tượng khác. Mặt khác, chúng ta có thể thực hiện thay đổi đối với một đối tượng có thể thay đổi (một danh sách) và giữ nguyên đối tượng đó trong suốt.

ref: https://medium.com/@ty Thảmheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

Cũng tham khảo url bên dưới để hiểu về phương pháp soi cạn và soi sâu

https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/


# ID giống nhau đối với Danh sách
roshan ok
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.