เมื่อใดที่“ i + = x” แตกต่างจาก“ i = i + x” ใน Python


212

ผมก็บอกว่าสามารถมีผลกระทบที่แตกต่างกันกว่าสัญกรณ์มาตรฐาน+= i = i +มีกรณีที่i += 1จะแตกต่างจากi = i + 1?


7
+=ทำหน้าที่เหมือนextend()ในกรณีของรายการ
Ashwini Chaudhary

12
@AshwiniChaudhary นั่นคือความแตกต่างที่ลึกซึ้งสวยพิจารณาว่าเป็นi=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6] Trueนักพัฒนาหลายคนอาจไม่สังเกตว่าid(i)การเปลี่ยนแปลงสำหรับการดำเนินการหนึ่ง แต่ไม่เปลี่ยน
kojiro

1
@kojiro - ในขณะที่มันเป็นความแตกต่างที่ลึกซึ้งฉันคิดว่ามันเป็นสิ่งที่สำคัญ
mgilson

@ mgilson เป็นสิ่งสำคัญและฉันรู้สึกว่ามันต้องการคำอธิบาย :)
kojiro

1
คำถามที่เกี่ยวข้องเกี่ยวกับความแตกต่างระหว่างสองใน Java: stackoverflow.com/a/7456548/245966
jakub.g

คำตอบ:


317

ขึ้นอยู่กับวัตถุiทั้งหมด

+=เรียก__iadd__วิธีการ (ถ้ามี - ตกกลับบน__add__ถ้ามันไม่ได้อยู่) ในขณะที่+โทร__add__วิธีที่ 1หรือ__radd__วิธีการในไม่กี่กรณีที่ 2

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

i = 1
i += 1

iดูเหมือนว่าจะเพิ่มขึ้น ในความเป็นจริงคุณจะได้รับจำนวนเต็มใหม่และกำหนดให้ "ด้านบนของ" i- สูญเสียการอ้างอิงเดียวกับจำนวนเต็มเก่า ในกรณีนี้เป็นเหมือนกับi += 1 i = i + 1แต่ด้วยวัตถุที่ไม่แน่นอนส่วนใหญ่มันเป็นเรื่องที่แตกต่าง:

เป็นตัวอย่างที่เป็นรูปธรรม:

a = [1, 2, 3]
b = a
b += [1, 2, 3]
print a  #[1, 2, 3, 1, 2, 3]
print b  #[1, 2, 3, 1, 2, 3]

เปรียบเทียบกับ:

a = [1, 2, 3]
b = a
b = b + [1, 2, 3]
print a #[1, 2, 3]
print b #[1, 2, 3, 1, 2, 3]

แจ้งให้ทราบว่าในตัวอย่างแรกตั้งแต่bและaอ้างอิงวัตถุเดียวกันเมื่อฉันใช้+=ในbก็จริงการเปลี่ยนแปลงb(และaเห็นการเปลี่ยนแปลงที่มากเกินไป - หลังจากที่ทุกคนก็อ้างอิงรายการเดียวกัน) ในกรณีที่สอง แต่เมื่อฉันทำb = b + [1, 2, 3]นี้จะใช้เวลารายการที่มีการอ้างอิงและเชื่อมกับรายการใหม่b [1, 2, 3]จากนั้นจะเก็บรายการที่ตัดแบ่งในเนมสเปซปัจจุบันเป็นb- โดยไม่คำนึงถึงสิ่งที่bเป็นบรรทัดก่อน


1ในนิพจน์x + yหากx.__add__ไม่ได้ใช้งานหรือหากx.__add__(y)ส่งคืนNotImplemented และ xและyมีประเภทที่แตกต่างกันแล้วx + yy.__radd__(x)พยายามที่จะเรียกร้อง ดังนั้นในกรณีที่คุณมี

foo_instance += bar_instance

หากFooไม่ได้ใช้งาน__add__หรือ__iadd__ผลลัพธ์ที่นี่เหมือนกับ

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2ในการแสดงออกfoo_instance + bar_instance, bar_instance.__radd__จะพยายามก่อนfoo_instance.__add__ ถ้าประเภทของการbar_instanceเป็น subclass ของประเภทของfoo_instance(เช่นissubclass(Bar, Foo)) เหตุผลสำหรับสิ่งนี้เป็นเพราะBarในบางแง่วัตถุ "ระดับสูง" กว่าFooนั้นBarควรได้รับตัวเลือกในการเอาชนะFooพฤติกรรมของ


18
ดี+=เรียก__iadd__ ว่ามันมีอยู่และตกกลับไปที่การเพิ่มและ rebinding มิฉะนั้น นั่นเป็นเหตุผลที่การทำงานแม้ว่าจะไม่มีi = 1; i += 1 int.__iadd__แต่นอกจากคำอธิบายเล็กน้อยแล้ว
abarnert

4
@abarnert - ฉันมักจะสันนิษฐานว่าเรียกว่าเพียงแค่int.__iadd__ __add__ฉันดีใจที่ได้เรียนรู้สิ่งใหม่วันนี้ :)
mgilson

@abarnert - ผมคิดว่าอาจจะเป็นที่สมบูรณ์ , x + yสายy.__radd__(x)ถ้าx.__add__ไม่ได้อยู่ (หรือผลตอบแทนNotImplementedและxและyเป็นประเภทที่แตกต่างกัน)
mgilson

หากคุณต้องการเป็นผู้สมบรูณ์แบบจริงๆคุณต้องพูดถึงว่าบิต "ถ้ามี" จะต้องผ่านกลไก getattr ตามปกติยกเว้นสำหรับนิสัยแปลก ๆ บางอย่างที่มีคลาสคลาสสิกnb_inplace_addหรือsq_inplace_concatและฟังก์ชั่น C API เหล่านั้นมีข้อกำหนดที่เข้มงวดกว่าวิธี Python dunder และ… แต่ฉันไม่คิดว่ามันเกี่ยวข้องกับคำตอบ ความแตกต่างหลักคือ+=พยายามเพิ่มในสถานที่ก่อนที่จะกลับไปทำหน้าที่เหมือน+ที่ฉันคิดว่าคุณได้อธิบายแล้ว
abarnert

ใช่ฉันคิดว่าคุณกำลังขวา ... ถึงแม้ว่าผมเพิ่งจะถอยกลับในท่าทางที่ C API ไม่ได้เป็นส่วนหนึ่งของงูหลาม เป็นส่วนหนึ่งของCpython :-P
mgilson

67

ภายใต้ฝาครอบi += 1ทำสิ่งนี้:

try:
    i = i.__iadd__(1)
except AttributeError:
    i = i.__add__(1)

ในขณะที่i = i + 1ทำสิ่งนี้:

i = i.__add__(1)

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

ความตั้งใจก็คือประเภทที่เปลี่ยนแปลงไม่ได้เช่นlistจะกลายพันธุ์ในตัวเอง__iadd__(แล้วกลับมาselfเว้นแต่ว่าคุณกำลังทำอะไรที่ยุ่งยากมาก) ในขณะที่ประเภทที่ไม่เปลี่ยนรูปเช่นintจะไม่ใช้มัน

ตัวอย่างเช่น:

>>> l1 = []
>>> l2 = l1
>>> l1 += [3]
>>> l2
[3]

เพราะl2เป็นวัตถุเดียวกับl1และคุณกลายพันธุ์คุณยังกลายพันธุ์l1l2

แต่:

>>> l1 = []
>>> l2 = l1
>>> l1 = l1 + [3]
>>> l2
[]

ที่นี่คุณไม่ได้กลายพันธุ์l1; แต่คุณสร้างรายการใหม่l1 + [3]และรีบาวด์ชื่อl1ให้l2ชี้ไปที่เดิมโดยชี้ไปที่รายการเดิม

(ใน+=เวอร์ชั่นนี้คุณกำลังทำการ rebinding l1มันก็เป็นเช่นนั้นในกรณีที่คุณกำลัง rebinding มันให้เป็นแบบเดียวกันกับที่listมีอยู่แล้วดังนั้นคุณมักจะไม่สนใจส่วนนั้น)


ไม่__iadd__โทรจริง__add__ในกรณีของAttributeError?
mgilson

ดีi.__iadd__ไม่โทร__add__; มันเป็นสายที่i += 1 __add__
abarnert

เอ่อ ... ใช่นั่นคือสิ่งที่ฉันหมายถึง น่าสนใจ ฉันไม่รู้ว่ามันทำโดยอัตโนมัติ
mgilson

3
ความพยายามครั้งแรกเป็นจริงi = i.__iadd__(1)- iadd สามารถปรับเปลี่ยนวัตถุในสถานที่ แต่ไม่จำเป็นต้องและคาดว่าจะส่งกลับผลลัพธ์ในทั้งสองกรณี
lvc

โปรดทราบว่านี้หมายถึงว่าoperator.iaddการโทร__add__บนAttributeErrorแต่ก็ไม่สามารถ rebind ผล ... ดังนั้นi=1; operator.iadd(i, 1)ผลตอบแทนที่ 2 และใบตั้งค่าให้i 1ซึ่งค่อนข้างสับสน
abarnert

6

นี่คือตัวอย่างที่เปรียบเทียบโดยตรงi += xกับi = i + x:

def foo(x):
  x = x + [42]

def bar(x):
  x += [42]

c = [27]
foo(c); # c is not changed
bar(c); # c is changed to [27, 42]
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.