เหตุใด + = จึงทำงานโดยไม่คาดคิดในรายการ


118

+=ผู้ประกอบการในหลามดูเหมือนว่าจะมีการดำเนินงานอย่างไม่คาดคิดในรายการ ใครช่วยบอกทีว่าเกิดอะไรขึ้นที่นี่?

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 

เอาท์พุท

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

foo += barดูเหมือนว่าจะส่งผลกระทบต่อทุกตัวอย่างของชั้นเรียนในขณะที่foo = foo + barดูเหมือนว่าจะประพฤติในแบบที่ฉันคาดหวังให้สิ่งต่าง ๆ ประพฤติ

ตัว+=ดำเนินการนี้เรียกว่า "ตัวดำเนินการกำหนดสารประกอบ"


ดูความแตกต่างระหว่าง 'ขยาย' และ 'ผนวก' ในรายการด้วย
N 1.1

3
ฉันไม่คิดว่านี่แสดงสิ่งผิดปกติกับ Python ภาษาส่วนใหญ่ไม่อนุญาตให้คุณใช้+โอเปอเรเตอร์ในอาร์เรย์ด้วยซ้ำ ฉันคิดว่ามันสมเหตุสมผลดีในกรณีนี้ที่+=จะต่อท้าย
Skilldrick

4
เรียกว่า 'การมอบหมายงานเสริม' อย่างเป็นทางการ
Martijn Pieters

คำตอบ:


138

คำตอบทั่วไปคือ+=พยายามเรียกใช้__iadd__เมธอดพิเศษและหากไม่สามารถใช้งานได้ก็จะพยายามใช้__add__แทน ดังนั้นปัญหาคือความแตกต่างระหว่างวิธีพิเศษเหล่านี้

__iadd__วิธีพิเศษสำหรับการนอกจากนี้ในสถานที่ที่เป็นมันแปรรูปวัตถุที่จะทำหน้าที่ในการ __add__วิธีพิเศษส่งกลับวัตถุใหม่และยังใช้สำหรับมาตรฐาน+ผู้ประกอบการ

ดังนั้นเมื่อใช้+=ตัวดำเนินการกับวัตถุที่มีการ__iadd__กำหนดวัตถุจะถูกแก้ไขในสถานที่ มิฉะนั้นจะพยายามใช้แบบธรรมดา__add__และส่งคืนวัตถุใหม่แทน

นั่นคือเหตุผลที่ประเภทที่เปลี่ยนแปลงได้เช่นรายการจะ+=เปลี่ยนค่าของอ็อบเจ็กต์ในขณะที่สำหรับประเภทที่ไม่เปลี่ยนรูปเช่นสิ่งทอสตริงและจำนวนเต็มจะส่งคืนอ็อบเจ็กต์ใหม่แทน ( a += bกลายเป็นเทียบเท่าa = a + b)

สำหรับประเภทที่รองรับทั้งสองอย่าง__iadd__และ__add__คุณต้องระวังว่าคุณใช้อันไหน a += bจะเรียก__iadd__และกลายพันธุ์aในขณะที่จะสร้างวัตถุใหม่และกำหนดให้a = a + b aไม่ใช่การดำเนินการเดียวกัน!

>>> 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

สำหรับประเภทที่ไม่เปลี่ยนรูป (โดยที่คุณไม่มี__iadd__) a += bและa = a + bเทียบเท่า นี่คือสิ่งที่ช่วยให้คุณใช้+=กับประเภทที่ไม่เปลี่ยนรูปได้ซึ่งอาจดูเป็นการตัดสินใจในการออกแบบที่แปลกจนกว่าคุณจะพิจารณาว่ามิฉะนั้นคุณจะไม่สามารถใช้+=กับประเภทที่ไม่เปลี่ยนรูปได้เช่นตัวเลข!


4
นอกจากนี้ยังมี__radd__วิธีการที่อาจถูกเรียกใช้ในบางครั้ง (เกี่ยวข้องกับนิพจน์ที่เกี่ยวข้องกับคลาสย่อยเป็นส่วนใหญ่)
jfs

2
ในมุมมอง: + = มีประโยชน์หากหน่วยความจำและความเร็วมีความสำคัญ
Norfeldt

3
รู้ว่า+=จริง ๆ แล้วขยายรายการนี้จะอธิบายถึงเหตุผลที่x = []; x = x + {}ให้TypeErrorในขณะที่เพียงผลตอบแทนx = []; x += {} []
zezollo

96

สำหรับกรณีทั่วไปดูคำตอบของสกอตต์กริฟฟิ เมื่อจัดการกับรายการเหมือนคุณอยู่แม้ว่าผู้ประกอบการจดชวเลขเป็น+= someListObject.extend(iterableObject)ดูเอกสารขยาย ()

extendฟังก์ชั่นจะผนวกองค์ประกอบทั้งหมดของพารามิเตอร์ในรายการ

เมื่อfoo += somethingคุณกำลังแก้ไขรายการfooอยู่ดังนั้นคุณจะไม่เปลี่ยนการอ้างอิงที่ชื่อfooชี้ไป แต่คุณกำลังเปลี่ยนวัตถุรายการโดยตรง กับfoo = foo + somethingคุณจริงสร้างใหม่รายการ

โค้ดตัวอย่างนี้จะอธิบาย:

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

lหมายเหตุวิธีการอ้างอิงการเปลี่ยนแปลงเมื่อท่านโอนรายการใหม่ที่จะ

เนื่องจากbarเป็นตัวแปรคลาสแทนที่จะเป็นตัวแปรอินสแตนซ์การปรับเปลี่ยนในตำแหน่งจะส่งผลต่ออินสแตนซ์ทั้งหมดของคลาสนั้น แต่เมื่อกำหนดใหม่self.barอินสแตนซ์จะมีตัวแปรอินสแตนซ์แยกต่างหากself.barโดยไม่ส่งผลกระทบต่ออินสแตนซ์คลาสอื่น


7
สิ่งนี้ไม่เป็นความจริงเสมอไป: a = 1; a + = 1; เป็น Python ที่ถูกต้อง แต่ ints ไม่มีเมธอด "expand ()" คุณไม่สามารถสรุปสิ่งนี้ได้
e-satis

2
ทำการทดสอบบางอย่าง Scott Griffiths ทำถูกต้องดังนั้น -1 สำหรับคุณ
e-satis

11
@ e-statis: OP พูดถึงรายการอย่างชัดเจนและฉันก็บอกชัดเจนว่าฉันกำลังพูดถึงรายการด้วย ฉันไม่ได้สรุปอะไรเลย
AndiDog

ลบ -1 ออกคำตอบนั้นดีพอ ฉันยังคิดว่าคำตอบของ Griffiths ดีกว่า
e-satis

ตอนแรกก็รู้สึกแปลกที่จะคิดว่าa += bจะแตกต่างจากa = a + bสองรายการและa bแต่มันก็สมเหตุสมผล extendมักจะเป็นสิ่งที่ตั้งใจจะทำกับรายการมากกว่าสร้างสำเนาใหม่ของรายการทั้งหมดซึ่งจะมีความซับซ้อนของเวลาที่สูงขึ้น หากนักพัฒนาจำเป็นต้องระวังว่าพวกเขาไม่ได้ปรับเปลี่ยนรายการต้นฉบับให้เข้าที่สิ่งเหล่านี้เป็นตัวเลือกที่ดีกว่าในการเป็นวัตถุที่ไม่เปลี่ยนรูป +=กับทูเพิลไม่สามารถแก้ไขทูเพิลดั้งเดิมได้
Pranjal Mittal

22

ปัญหาที่นี่คือbarกำหนดเป็นแอตทริบิวต์คลาสไม่ใช่ตัวแปรอินสแตนซ์

ในfooแอตทริบิวต์คลาสถูกแก้ไขในinitเมธอดนั่นคือสาเหตุที่อินสแตนซ์ทั้งหมดได้รับผลกระทบ

ในfoo2การตัวแปรเช่นมีการกำหนดใช้ (ว่าง) barแอตทริบิวต์ระดับและทุกกรณีที่ได้รับของตัวเอง

การใช้งานที่ "ถูกต้อง" คือ:

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

แน่นอนว่าแอตทริบิวต์ของคลาสนั้นถูกกฎหมายอย่างสมบูรณ์ ในความเป็นจริงคุณสามารถเข้าถึงและแก้ไขได้โดยไม่ต้องสร้างอินสแตนซ์ของคลาสเช่นนี้:

class foo:
    bar = []

foo.bar = [x]

8

มีสองสิ่งที่เกี่ยวข้องที่นี่:

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

+ตัวดำเนินการเรียกใช้__add__เมธอดในรายการ ใช้องค์ประกอบทั้งหมดจากตัวถูกดำเนินการและสร้างรายการใหม่ที่มีองค์ประกอบเหล่านั้นรักษาลำดับของพวกเขา

+= ผู้ให้บริการโทร __iadd__วิธีการการในรายการ ต้องใช้เวลาในการทำซ้ำและผนวกองค์ประกอบทั้งหมดของรายการที่ทำซ้ำได้ในรายการ ไม่สร้างวัตถุรายการใหม่

ในชั้นเรียนfooคำสั่ง self.bar += [x]ไม่ใช่คำสั่งมอบหมาย แต่แปลเป็นจริง

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

ซึ่งแก้ไขรายการในสถานที่และทำหน้าที่เหมือนวิธีรายการ extendซึ่งปรับเปลี่ยนรายการในสถานที่และวิธีการทำหน้าที่เหมือนรายการ

ในชั้นเรียนfoo2ตรงกันข้ามคำสั่งมอบหมายในinitวิธีการ

self.bar = self.bar + [x]  

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

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

จากนั้นจะสร้างแอตทริบิวต์อินสแตนซ์barและกำหนดรายการที่สร้างขึ้นใหม่ให้ โปรดทราบว่าbarใน rhs ของงานนั้นแตกต่างจากไฟล์barบน lhs

สำหรับกรณีของชั้นfoo, barเป็นคุณลักษณะการเรียนและไม่แอตทริบิวต์อินสแตนซ์ ดังนั้นการเปลี่ยนแปลงแอตทริบิวต์คลาสbarจะมีผลกับอินสแตนซ์ทั้งหมด

ในทางตรงกันข้ามตัวอย่างของการเรียนในแต่ละfoo2มีแอตทริบิวต์อินสแตนซ์ของตัวเองที่แตกต่างจากแอตทริบิวต์คลาสที่มีชื่อเดียวกันbarbar

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

หวังว่านี่จะเคลียร์สิ่งต่างๆ


5

แม้ว่าเวลาจะผ่านไปนานแค่ไหนและมีการพูดถึงสิ่งที่ถูกต้องมากมาย แต่ก็ไม่มีคำตอบว่าเอฟเฟกต์ทั้งสองแบบใด

คุณมี 2 เอฟเฟกต์:

  1. "พิเศษ" ซึ่งอาจไม่มีใครสังเกตเห็นพฤติกรรมของรายการที่มี+=(ตามที่ระบุโดยScott Griffiths )
  2. ข้อเท็จจริงที่ว่าแอตทริบิวต์คลาสและแอตทริบิวต์อินสแตนซ์มีส่วนเกี่ยวข้อง (ตามที่ระบุโดยCan Berk Büder )

ในชั้นเรียนfooที่__init__วิธีการปรับเปลี่ยนแอตทริบิวต์ชั้นเรียน เป็นเพราะself.bar += [x]แปลself.bar = self.bar.__iadd__([x])ว่า __iadd__()มีไว้สำหรับการแก้ไขแบบแทนที่ดังนั้นมันจึงแก้ไขรายการและส่งคืนการอ้างอิงไปยังรายการนั้น

โปรดทราบว่าอินสแตนซ์ dict ถูกแก้ไขแม้ว่าโดยปกติจะไม่จำเป็นเนื่องจาก class dict มีงานที่มอบหมายเดียวกันอยู่แล้ว ดังนั้นรายละเอียดนี้จึงแทบไม่มีใครสังเกตเห็น - ยกเว้นว่าคุณจะทำfoo.bar = []หลังจากนั้น นี่คืออินสแตนซ์barจะยังคงเหมือนเดิมเนื่องจากข้อเท็จจริงดังกล่าว

ในชั้นเรียนfoo2แต่ชั้นก็barคือการใช้ แต่ไม่ได้สัมผัส แต่[x]จะมีการเพิ่มa เข้าไปสร้างวัตถุใหม่ดังที่self.bar.__add__([x])เรียกว่าที่นี่ซึ่งไม่ได้แก้ไขวัตถุ ผลลัพธ์จะถูกใส่ลงในอินสแตนซ์ dict จากนั้นให้อินสแตนซ์รายการใหม่เป็นคำสั่งในขณะที่แอตทริบิวต์ของคลาสยังคงถูกแก้ไข

ความแตกต่างระหว่าง... = ... + ...และ... += ...มีผลต่อการมอบหมายงานในภายหลัง:

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 

คุณสามารถยืนยันตัวตนของวัตถุได้ด้วยprint id(foo), id(f), id(g)(อย่าลืมไฟล์() sถ้าคุณอยู่บน Python3)

BTW: +=โอเปอเรเตอร์นี้เรียกว่า "การมอบหมายงานที่เพิ่มขึ้น" และโดยทั่วไปมีจุดมุ่งหมายเพื่อทำการแก้ไขภายในเท่าที่จะทำได้


5

คำตอบอื่น ๆ ดูเหมือนจะครอบคลุมมากแม้ว่ามันจะคุ้มค่าที่จะอ้างถึงและอ้างถึงAugmented Assignments PEP 203 :

พวกเขา[ตัวดำเนินการกำหนดค่าเสริม]ใช้ตัวดำเนินการเดียวกันกับรูปแบบไบนารีปกติยกเว้นว่าการดำเนินการจะเสร็จสิ้นเมื่อวัตถุด้านซ้ายมือรองรับและประเมินด้านซ้ายมือเพียงครั้งเดียว

...

แนวคิดเบื้องหลังการมอบหมายงานเสริมใน Python ไม่ใช่แค่วิธีที่ง่ายกว่าในการเขียนแนวทางปฏิบัติทั่วไปในการจัดเก็บผลลัพธ์ของการดำเนินการไบนารีในตัวถูกดำเนินการด้านซ้าย แต่ยังเป็นวิธีสำหรับตัวถูกดำเนินการทางซ้ายที่เป็นปัญหาในการ รู้ว่าควรดำเนินการด้วยตัวเองแทนที่จะสร้างสำเนาที่แก้ไขเอง


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

เราจะเห็นว่าเมื่อเราพยายามแก้ไขวัตถุที่ไม่เปลี่ยนรูป (จำนวนเต็มในกรณีนี้) Python จะให้วัตถุอื่นมาแทน ในทางกลับกันเราสามารถทำการเปลี่ยนแปลงกับวัตถุที่เปลี่ยนแปลงได้ (รายการ) และทำให้วัตถุนั้นยังคงเป็นวัตถุเดิมตลอดไป

อ้างอิง: https://medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

โปรดดู url ด้านล่างเพื่อทำความเข้าใจตื้นและสำเนาลึก

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


# ID เหมือนกันสำหรับ Lists
roshan ok
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.