ทำไมไพ ธ อนจึงทำสำเนาองค์ประกอบเฉพาะเมื่อทำซ้ำรายการ?


31

ฉันเพิ่งรู้ว่าในงูใหญ่ถ้ามีคนเขียน

for i in a:
    i += 1

องค์ประกอบของรายการเดิมaจริงจะไม่ได้ส่งผลกระทบต่อที่ทุกคนตั้งแต่ตัวแปรจะออกมาเพียงแค่เป็นสำเนาขององค์ประกอบเดิมในia

ในการปรับเปลี่ยนองค์ประกอบดั้งเดิม

for index, i in enumerate(a):
    a[index] += 1

จะต้อง

ฉันรู้สึกประหลาดใจจริงๆกับพฤติกรรมนี้ ดูเหมือนว่าจะเป็นแบบที่ใช้ง่ายมากดูเหมือนจะแตกต่างจากภาษาอื่นและทำให้เกิดข้อผิดพลาดในรหัสของฉันที่ฉันต้องแก้ไขในขณะนี้

ฉันเคยอ่าน Python Tutorial มาก่อน เพียงเพื่อให้แน่ใจว่าฉันตรวจสอบหนังสืออีกครั้งในขณะนี้และมันไม่ได้พูดถึงพฤติกรรมนี้เลย

เหตุผลที่อยู่เบื้องหลังการออกแบบนี้คืออะไร? เป็นที่คาดหวังหรือไม่ว่าเป็นแบบฝึกหัดมาตรฐานในหลายภาษาเพื่อที่ผู้สอนจะเชื่อว่าผู้อ่านควรได้รับมันอย่างเป็นธรรมชาติหรือไม่? ในภาษาอื่นมีพฤติกรรมแบบเดียวกันในการทำซ้ำที่ฉันควรให้ความสนใจในอนาคตหรือไม่


19
นั่นเป็นความจริงเฉพาะในกรณีที่iไม่เปลี่ยนรูปหรือคุณกำลังดำเนินการไม่เปลี่ยนแปลง รายการที่ซ้อนfor i in a: a.append(1)จะมีพฤติกรรมที่แตกต่างกัน Python ไม่ได้คัดลอกรายการที่ซ้อนกัน อย่างไรก็ตามจำนวนเต็มไม่เปลี่ยนรูปและการเพิ่มส่งคืนวัตถุใหม่จะไม่เปลี่ยนวัตถุเก่า
jonrsharpe

10
ไม่น่าแปลกใจเลย ฉันไม่สามารถนึกถึงภาษาที่ไม่เหมือนกันสำหรับอาร์เรย์ประเภทพื้นฐานเช่นจำนวนเต็ม ยกตัวอย่างเช่นลองใน a=[1,2,3];a.forEach(i => i+=1);alert(a)JavaScript เหมือนกันใน C #
edc65

7
คุณคาดว่าi = i + 1จะส่งผลกระทบต่อa?
deltab

7
โปรดทราบว่าพฤติกรรมนี้ไม่แตกต่างกันในภาษาอื่น C, Javascript, Java ฯลฯ ทำงานในลักษณะนี้
slebetman

1
@jonrsharpe สำหรับรายการ "+ =" เปลี่ยนรายการเก่าในขณะที่ "+" สร้างรายการใหม่
Vasily Alexeev

คำตอบ:


68

ฉันตอบคำถามที่คล้ายกันเมื่อเร็ว ๆ นี้และเป็นสิ่งสำคัญมากที่ต้องตระหนักว่า+=อาจมีความหมายต่างกัน:

  • หากประเภทข้อมูลใช้การเพิ่มแบบแทนที่ (เช่นมี__iadd__ฟังก์ชั่นการทำงานที่ถูกต้อง) ข้อมูลที่iอ้างถึงจะได้รับการอัปเดต (ไม่สำคัญว่าจะอยู่ในรายการหรือที่อื่น)

  • ถ้าชนิดข้อมูลไม่ได้ดำเนินการ__iadd__วิธีการที่i += xคำสั่งเป็นเพียงน้ำตาลประโยคสำหรับดังนั้นค่าใหม่ถูกสร้างขึ้นและได้รับมอบหมายให้ชื่อตัวแปรi = i + xi

  • ถ้าชนิดข้อมูลใช้__iadd__แต่ทำสิ่งที่แปลก อาจเป็นไปได้ว่ามีการอัปเดต ... หรือไม่ - ขึ้นอยู่กับสิ่งที่นำไปใช้งานที่นั่น

Pythons จำนวนเต็มลอยสตริงไม่ได้ใช้__iadd__ดังนั้นสิ่งเหล่านี้จะไม่ได้รับการปรับปรุงในสถานที่ อย่างไรก็ตามประเภทข้อมูลอื่น ๆ เช่นnumpy.arrayหรือlistใช้มันและจะทำงานเหมือนที่คุณคาดหวัง ดังนั้นจึงไม่ใช่เรื่องของการคัดลอกหรือไม่มีการคัดลอกเมื่อทำซ้ำ (โดยปกติจะไม่ทำสำเนาสำหรับlists และtuples - แต่นั่นก็ขึ้นอยู่กับการใช้งานภาชนะ__iter__และ__getitem__วิธีการ!) - มันเป็นเรื่องของประเภทข้อมูลมากกว่า aคุณเก็บไว้ในของคุณ


2
นี่คือคำอธิบายที่ถูกต้องสำหรับพฤติกรรมที่อธิบายไว้ในคำถาม
pabouk

19

ชี้แจง - คำศัพท์

งูหลามไม่แยกระหว่างแนวความคิดของการอ้างอิงและตัวชี้ พวกเขามักจะใช้การอ้างอิงคำแต่ถ้าคุณเปรียบเทียบกับภาษาเช่น C ++ ที่มีความแตกต่างนั้น - มันใกล้ชิดกับตัวชี้มาก

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

  • ค่า : ข้อมูลจริงที่อยู่ในหน่วยความจำ void foo(int x);เป็นลายเซ็นของฟังก์ชั่นที่ได้รับเป็นจำนวนเต็มโดยค่า
  • ตัวชี้ : ที่อยู่หน่วยความจำถือว่าเป็นค่า สามารถเลื่อนเวลาในการเข้าถึงหน่วยความจำที่ชี้ไป void foo(int* x);เป็นลายเซ็นของฟังก์ชั่นที่ได้รับเป็นจำนวนเต็มโดยชี้
  • การอ้างอิง : น้ำตาลรอบตัวชี้ มีตัวชี้อยู่เบื้องหลัง แต่คุณสามารถเข้าถึงค่าที่เลื่อนออกไปและไม่สามารถเปลี่ยนที่อยู่ที่ชี้ไป void foo(int& x);เป็นลายเซ็นของฟังก์ชั่นที่ได้รับเป็นจำนวนเต็มโดยการอ้างอิง

คุณหมายถึง "แตกต่างจากภาษาอื่น" ภาษาส่วนใหญ่ที่ฉันรู้ว่าการรองรับลูปแต่ละอันกำลังคัดลอกองค์ประกอบเว้นแต่จะได้รับคำแนะนำเป็นพิเศษ

โดยเฉพาะสำหรับ Python (แม้ว่าหลายเหตุผลเหล่านี้อาจนำไปใช้กับภาษาอื่นที่มีแนวคิดทางสถาปัตยกรรมหรือปรัชญาที่คล้ายกัน):

  1. พฤติกรรมนี้อาจก่อให้เกิดข้อผิดพลาดสำหรับคนที่ไม่รู้จัก แต่พฤติกรรมทางเลือกที่อาจก่อให้เกิดข้อผิดพลาดไม่ได้สำหรับผู้ที่มีความตระหนักของมัน เมื่อคุณกำหนดตัวแปร ( i) คุณมักจะไม่หยุดและพิจารณาตัวแปรอื่น ๆ ทั้งหมดที่จะเปลี่ยนแปลงเนื่องจากมัน ( a) การ จำกัด ขอบเขตที่คุณกำลังทำอยู่เป็นปัจจัยสำคัญในการป้องกันรหัสสปาเก็ตตี้ดังนั้นการทำซ้ำโดยการคัดลอกจึงเป็นค่าเริ่มต้นแม้แต่ในภาษาที่สนับสนุนการทำซ้ำโดยการอ้างอิง

  2. ตัวแปร Python เป็นตัวชี้เดียวเสมอดังนั้นจึงมีราคาถูกที่จะทำซ้ำโดยการคัดลอก - ราคาถูกกว่าการวนซ้ำตามการอ้างอิงซึ่งจะต้องมีการเลื่อนพิเศษในแต่ละครั้งที่คุณเข้าถึงค่า

  3. Python ไม่มีแนวคิดของตัวแปรอ้างอิงเช่น - C ++ นั่นคือตัวแปรทั้งหมดใน Python เป็นการอ้างอิงจริง ๆ แต่ในแง่ที่ว่ามันเป็นตัวชี้ - ไม่ใช่การอ้างอิง constat แบบเบื้องหลังฉากเช่นtype& nameอาร์กิวเมนต์C ++ เนื่องจากแนวคิดนี้ไม่มีอยู่ใน Python การใช้การวนซ้ำโดยอ้างอิง - นับประสาทำให้เป็นค่าเริ่มต้น! - จะต้องเพิ่มความซับซ้อนให้กับ bytecode

  4. forคำแถลงของ Python ไม่เพียงทำงานในอาร์เรย์เท่านั้น แต่ยังรวมถึงแนวคิดทั่วไปของเครื่องกำเนิดไฟฟ้าอีกด้วย เบื้องหลัง, Python เรียกiterอาร์เรย์ของคุณจะได้รับวัตถุที่ - เมื่อคุณเรียกnextมัน - ทั้งผลตอบแทนองค์ประกอบถัดไปหรือSAraise StopIterationมีหลายวิธีในการนำเครื่องมือกำเนิดไปใช้ใน Python และมันจะยากกว่ามากในการนำเครื่องมือกำเนิดมาใช้สำหรับการอ้างอิงซ้ำ


ขอบคุณสำหรับคำตอบ. ดูเหมือนว่าความเข้าใจของฉันเกี่ยวกับตัววนซ้ำยังคงไม่เพียงพอ ตัวทำซ้ำในการอ้างอิง C ++ เป็นค่าเริ่มต้นหรือไม่ หากคุณอ่านซ้ำตัววนซ้ำคุณสามารถเปลี่ยนค่าขององค์ประกอบของคอนเทนเนอร์ต้นฉบับได้ทันทีหรือไม่?
xji

4
Python ทำการวนซ้ำตามการอ้างอิง (โดยค่า แต่ค่าเป็นการอ้างอิง) การลองด้วยรายการของวัตถุที่ไม่แน่นอนจะแสดงให้เห็นอย่างรวดเร็วว่าไม่มีการคัดลอกเกิดขึ้น
jonrsharpe

Iterators ใน C ++ เป็นจริงวัตถุที่สามารถเลื่อนการเข้าถึงค่าในอาร์เรย์ ในการปรับเปลี่ยนองค์ประกอบดั้งเดิมคุณใช้*it = ...- แต่ไวยากรณ์ประเภทนี้ได้ระบุว่าคุณกำลังแก้ไขบางสิ่งบางอย่าง - ซึ่งทำให้เหตุผล # 1 น้อยกว่าปัญหา เหตุผล # 2 และ # 3 ไม่สามารถใช้ได้เช่นกันเพราะในการคัดลอก C ++ มีราคาแพงและแนวคิดของตัวแปรอ้างอิงมีอยู่ สำหรับเหตุผล # 4 - ความสามารถในการส่งคืนการอ้างอิงช่วยให้การนำไปใช้งานง่ายสำหรับทุกกรณี
Idan Arye

1
@jonrsharpe ใช่มันถูกเรียกโดยการอ้างอิง แต่ในภาษาใด ๆ ที่มีความแตกต่างระหว่างพอยน์เตอร์และการอ้างอิงการเรียงลำดับการทำซ้ำนี้จะเป็นการทำซ้ำโดยตัวชี้ (และเนื่องจากพอยเตอร์เป็นค่า - การทำซ้ำตามค่า) ฉันจะเพิ่มคำอธิบาย
Idan Arye

20
ย่อหน้าแรกของคุณแนะนำว่า Python เช่นเดียวกับภาษาอื่น ๆ เหล่านั้นคัดลอกองค์ประกอบในวงสำหรับ มันไม่ได้ ไม่ จำกัด ขอบเขตของการเปลี่ยนแปลงที่คุณทำกับองค์ประกอบนั้น OP เห็นเฉพาะพฤติกรรมนี้เนื่องจากองค์ประกอบไม่เปลี่ยนรูป คำตอบของคุณนั้นไม่สมบูรณ์และผิดพลาดอย่างที่สุด
jonrsharpe

11

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

สาเหตุหลักที่ไม่สามารถใช้งานได้ตามที่คุณคาดหวังเพราะใน Python เมื่อคุณเขียน:

i += 1

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

a = 0
print('ID of the first integer:', id(a))
a += 1
print('ID of the first integer +=1:', id(a))

ฟังก์ชัน idแสดงค่าที่ไม่ซ้ำใครและคงที่สำหรับวัตถุในช่วงอายุการใช้งาน โดยทั่วไปแล้วจะจับคู่กับที่อยู่หน่วยความจำใน C / C ++ ใช้รหัสข้างต้น:

ID of the first integer: 140444342529056
ID of the first integer +=1: 140444342529088

ซึ่งหมายความว่ารายการแรกaไม่เหมือนกันเป็นครั้งที่สองaเนื่องจากรหัสของพวกเขาแตกต่างกัน อย่างมีประสิทธิภาพพวกเขาอยู่ในสถานที่ที่แตกต่างกันในหน่วยความจำ

อย่างไรก็ตามด้วยวัตถุสิ่งต่าง ๆ ก็ทำงานต่างกัน ฉันเขียนทับ+=โอเปอเรเตอร์ที่นี่:

class CustomInt:
  def __iadd__(self, other):
    # Override += 1 for this class
    self.value = self.value + other.value
    return self

  def __init__(self, v):
    self.value = v

ints = []
for i in range(5):
  int = CustomInt(i)
  print('ID={}, value={}'.format(id(int), i))
  ints.append(int)


for i in ints:
  i += CustomInt(i.value)

print("######")
for i in ints:
  print('ID={}, value={}'.format(id(i), i.value))

การรันผลลัพธ์นี้ในเอาต์พุตต่อไปนี้:

ID=140444284275400, value=0
ID=140444284275120, value=1
ID=140444284275064, value=2
ID=140444284310752, value=3
ID=140444284310864, value=4
######
ID=140444284275400, value=0
ID=140444284275120, value=2
ID=140444284275064, value=4
ID=140444284310752, value=6
ID=140444284310864, value=8

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

เปรียบเทียบสิ่งนี้กับเมื่อคุณเรียกใช้แบบฝึกหัดเดียวกันกับวัตถุที่เปลี่ยนรูปไม่ได้:

ints_primitives = []
for i in range(5):
  int = i
  ints_primitives.append(int)
  print('ID={}, value={}'.format(id(int), i))

print("######")
for i in ints_primitives:
  i += 1
  print('ID={}, value={}'.format(id(int), i))


print("######")
for i in ints_primitives:
  print('ID={}, value={}'.format(id(i), i))

ผลลัพธ์นี้:

ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
######
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
ID=140023258889408, value=5
######
ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4

สิ่งเล็ก ๆ น้อย ๆ ที่นี่เพื่อแจ้งให้ทราบล่วงหน้า ก่อนอื่นในลูปที่มี+=, คุณจะไม่เพิ่มวัตถุต้นฉบับอีกต่อไป ในกรณีนี้เนื่องจาก int อยู่ในประเภทที่ไม่เปลี่ยนรูปแบบใน Python , python จึงใช้ id อื่น สิ่งที่น่าสนใจที่จะทราบว่า Python ใช้พื้นฐานที่เหมือนกันidสำหรับตัวแปรหลายตัวที่มีค่าไม่เปลี่ยนรูปแบบเดียวกัน:

a = 1999
b = 1999
c = 1999

print('id a:', id(a))
print('id b:', id(b))
print('id c:', id(c))

id a: 139846953372048
id b: 139846953372048
id c: 139846953372048

tl; dr - Python มีประเภทที่ไม่เปลี่ยนรูปจำนวนหนึ่งซึ่งทำให้เกิดพฤติกรรมที่คุณเห็น สำหรับประเภทที่ไม่แน่นอนทั้งหมดความคาดหวังของคุณถูกต้อง


6

@ Idan เป็นคำตอบที่ดีในการอธิบายว่าทำไมไพ ธ อนไม่ปฏิบัติกับตัวแปรลูปในฐานะตัวชี้ในแบบ C แต่มันก็คุ้มค่าที่จะอธิบายในเชิงลึกยิ่งขึ้นว่าโค้ด snippet คลายตัวอย่างไรใน Python บิตที่เรียบง่าย ของรหัสจะถูกเรียกไปยังวิธีการในตัว เพื่อนำตัวอย่างแรกของคุณ

for i in a:
    i += 1

มีสองสิ่งที่จะแตกไฟล์คือ: for _ in _:ไวยากรณ์และ_ += _ไวยากรณ์ ในการใช้ for for loop เช่นเดียวกับภาษาอื่น ๆ Python มีการfor-eachวนซ้ำที่มีไวยากรณ์เป็นน้ำตาลสำหรับรูปแบบตัววนซ้ำ ใน Python ตัววนซ้ำเป็นวัตถุที่กำหนด.__next__(self)วิธีการส่งคืนองค์ประกอบปัจจุบันในลำดับเลื่อนไปยังลำดับถัดไปและจะเพิ่ม a StopIterationเมื่อไม่มีรายการในลำดับ Iterableเป็นวัตถุที่กำหนดเป็นนักการ.__iter__(self)วิธีการที่ส่งกลับ iterator

(NB: an Iteratorยังเป็นIterableและส่งคืนตัวเองจาก.__iter__(self)วิธีการของมัน)

งูใหญ่มักจะมีฟังก์ชั่น inbuilt ที่มอบหมายให้กับวิธีการขีดเส้นใต้คู่ที่กำหนดเอง ดังนั้นจึงมีiter(o)ที่หายไปo.__iter__()และแก้ไขที่next(o) o.__next__()หมายเหตุฟังก์ชั่น inbuilt เหล่านี้มักจะพยายามกำหนดค่าเริ่มต้นที่เหมาะสมถ้าวิธีที่พวกเขาจะมอบหมายไม่ได้กำหนด ยกตัวอย่างเช่นlen(o)มักจะหายไปแต่ถ้าวิธีการที่ไม่ได้กำหนดไว้ก็จะพยายามo.__len__()iter(o).__len__()

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

for i in %EXPR%:
    %LOOP%

จะได้รับการแตกออกเป็นบางอย่างเช่น

_a_iter = iter(%EXPR%)
while True:
    try:
        i = next(_a_iter)
    except StopIteration:
        break
    %LOOP%

ดังนั้นในกรณีนี้

for i in a:
    i += 1

ได้รับการคลายการแพค

_a_iter = iter(a) # = a.__iter__()
while True:
    try: 
        i = next(_a_iter) # = _a_iter.__next__()
    except StopIteration:
        break
    i += 1

i += 1อีกครึ่งหนึ่งของที่นี่คือ โดยทั่วไปได้รับการห่อ%ASSIGN% += %EXPR% %ASSIGN% = %ASSIGN%.__iadd__(%EXPR%)ที่นี่__iadd__(self, other)จะเติม inplace และส่งคืนตัวเอง

(NB นี่เป็นอีกกรณีหนึ่งที่ Python จะเลือกทางเลือกหากไม่ได้กำหนดวิธีการหลักหากวัตถุไม่ได้ใช้__iadd__มันจะถอยกลับไป__add__จริง ๆ แล้วมันทำเช่นนี้ในกรณีนี้เช่นเดียวกับที่intไม่ได้ใช้__iadd__- ซึ่งสมเหตุสมผล ไม่เปลี่ยนแปลงและไม่สามารถแก้ไขได้)

ดังนั้นรหัสของคุณที่นี่ดูเหมือน

_a_iter = iter(a)
while True:
    try:
        i = next(_a_iter)
    except StopIteration:
        break
    i = iadd(i,1)

ที่เราสามารถกำหนด

def iadd(o, v):
    try:
        return o.__iadd__(v)
    except AttributeError:
        return o.__add__(v)

ยังมีอีกหน่อยในโค้ดที่สองของคุณ สองสิ่งใหม่ที่เราจำเป็นต้องรู้ที่%ARG%[%KEY%] = %VALUE%ได้รับแตกไป(%ARG%).__setitem__(%KEY%, %VALUE%)และได้รับการห่อ%ARG%[%KEY%] (%ARG%).__getitem__(%KEY%)การรวบรวมความรู้นี้เข้าด้วยกันเราจะได้รับการa[ix] += 1คลายออกa.__setitem__(ix, a.__getitem__(ix).__add__(1))(อีกครั้ง: __add__แทนที่จะเป็น__iadd__เพราะ__iadd__ไม่ได้ใช้งานโดย ints) รหัสสุดท้ายของเราดูเหมือนว่า:

_a_iter = iter(enumerate(a))
while True:
    try:
        index, i = next(_a_iter)
    except StopIteration:
        break
    a.__setitem__(index, iadd(a.__getitem__(index), 1))

ที่จริงการตอบคำถามของคุณว่าทำไมคนแรกที่ไม่ได้ปรับเปลี่ยนรายการในขณะที่สองไม่ในตัวอย่างแรกของเราเราจะได้รับiจากnext(_a_iter)ซึ่งหมายความว่าจะเป็นi intเนื่องจากintไม่สามารถแก้ไขได้i += 1จึงไม่ทำสิ่งใดในรายการ ในกรณีที่สองของเราที่เราเป็นอีกครั้งที่ไม่ได้ปรับเปลี่ยนแต่จะมีการปรับเปลี่ยนรายชื่อโดยการเรียกint__setitem__

เหตุผลของการออกกำลังกายที่ซับซ้อนนี้เป็นเพราะฉันคิดว่ามันสอนบทเรียนต่อไปนี้เกี่ยวกับ Python:

  1. ราคาที่สามารถอ่านได้ของไพ ธ อนคือการเรียกวิธีการทำคะแนนสองเท่าเหล่านี้ตลอดเวลา
  2. ดังนั้นเพื่อให้มีโอกาสที่จะเข้าใจรหัส Python อย่างแท้จริงคุณต้องเข้าใจการแปลเหล่านี้

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

แก้ไข : @deltab แก้ไขการใช้คำว่า "คอลเลกชัน" ของฉันอย่างเลอะเทอะ


2
"ตัววนซ้ำยังเป็นคอลเลกชัน" ไม่ถูกต้อง: มันซ้ำได้ แต่คอลเลกชันก็มี__len__และ__contains__
deltab

2

+=ทำงานตามที่แตกต่างกันกับว่ามูลค่าปัจจุบันคือไม่แน่นอนหรือไม่เปลี่ยนรูป นี่คือเหตุผลหลักที่ทำให้ดูใช้เวลานานในการนำไปใช้กับ Python เนื่องจากผู้พัฒนา Python กลัวว่าจะทำให้สับสน

ถ้าiเป็น int แล้วจะไม่สามารถเปลี่ยนแปลงได้เนื่องจาก ints ไม่เปลี่ยนรูปและดังนั้นหากมูลค่าของiการเปลี่ยนแปลงจะต้องชี้ไปที่วัตถุอื่น:

>>> i=3
>>> id(i)
14336296
>>> i+=1
>>> id(i)
14336272   # Other object

อย่างไรก็ตามหากด้านซ้ายมือนั้นไม่แน่นอนค่า + = สามารถเปลี่ยนได้จริง เช่นถ้ามันเป็นรายการ:

>>> i=[]
>>> id(i)
140257231883944
>>> i+=[1]
>>> id(i)
140257231883944  # Still the same object!

ในลูป for ของคุณiอ้างถึงแต่ละองค์ประกอบของการaเปิด ถ้าสิ่งเหล่านั้นเป็นจำนวนเต็มดังนั้นกรณีแรกจะใช้และผลลัพธ์ของi += 1ต้องเป็นว่ามันหมายถึงวัตถุจำนวนเต็มอื่น รายการaของหลักสูตรยังคงมีองค์ประกอบเดียวกันกับที่เคยมีมา


ฉันไม่เข้าใจความแตกต่างระหว่างวัตถุที่ไม่แน่นอนและไม่เปลี่ยนรูปแบบนี้: ถ้าi = 1ตั้งค่าiเป็นวัตถุจำนวนเต็มไม่เปลี่ยนรูปแล้วi = []ควรตั้งค่าiเป็นรายการวัตถุที่ไม่เปลี่ยนรูป กล่าวอีกนัยหนึ่งทำไมวัตถุจำนวนเต็มไม่เปลี่ยนรูปและรายการวัตถุไม่แน่นอน ฉันไม่เห็นเหตุผลใด ๆ ที่อยู่เบื้องหลังสิ่งนี้
Giorgio

@Giorgio: วัตถุมาจากคลาสที่แตกต่างกันlistใช้วิธีการที่เปลี่ยนแปลงเนื้อหาintไม่ได้ [] เป็นวัตถุรายการที่ไม่แน่นอนและi = []ช่วยให้iอ้างถึงวัตถุนั้น
RemcoGerlich

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

@RemcoGerlich: ฉันเข้าใจว่าชั้นเรียนที่แตกต่างกันมีการทำงานที่แตกต่างกันฉันไม่เข้าใจว่าทำไมพวกเขาถึงมีการใช้งานในลักษณะนี้เช่นฉันไม่เข้าใจตรรกะเบื้องหลังตัวเลือกนี้ ฉันจะใช้ตัว+=ดำเนินการ / วิธีการทำงานในทำนองเดียวกัน (หลักการของความประหลาดใจน้อยที่สุด) สำหรับทั้งสองประเภท: เปลี่ยนวัตถุต้นฉบับหรือส่งคืนสำเนาที่แก้ไขสำหรับทั้งจำนวนเต็มและรายการ
Giorgio

1
@Giorgio: มันเป็นความจริงอย่างแน่นอนที่+=น่าแปลกใจใน Python แต่ก็รู้สึกว่าตัวเลือกอื่น ๆ ที่คุณพูดถึงนั้นน่าแปลกใจหรืออย่างน้อยก็ในทางปฏิบัติน้อยกว่า (การเปลี่ยนวัตถุดั้งเดิมไม่สามารถทำได้ด้วยมูลค่าที่พบบ่อยที่สุด คุณใช้ + = กับ, ints และการคัดลอกรายการทั้งหมดมีราคาแพงกว่าการกลายพันธุ์ Python ไม่ได้คัดลอกสิ่งต่าง ๆ เช่นรายการและพจนานุกรมเว้นแต่จะบอกไว้อย่างชัดเจน) มันเป็นการโต้วาทีครั้งใหญ่
RemcoGerlich

1

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

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

ความหมายของ Python สำหรับการมอบหมายแผนที่โดยตรงไปยัง C's (ไม่น่าแปลกใจที่ได้รับจากตัวชี้ PyObject * ของ CPython) โดยมีข้อแม้เพียงอย่างเดียวที่ทุกอย่างเป็นตัวชี้และคุณไม่ได้รับอนุญาตให้มีตัวชี้สองเท่า พิจารณารหัสต่อไปนี้:

a = 1
b = a
b += 1
print(a)

เกิดอะไรขึ้น? 1มันพิมพ์ ทำไม? จริงๆแล้วมันเทียบเท่ากับรหัส C ต่อไปนี้:

i64* a = malloc(sizeof(i64));
*a = 1;
i64* b = a;
i64* tmp = malloc(sizeof(i64));
tmp = *b + 1;
b = tmp;
printf("%d\n", *a);

ในรหัส C เป็นที่ชัดเจนว่าค่าของaไม่ได้รับผลกระทบอย่างสมบูรณ์

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

x = 1
a = [x]
print(a[0] is x)
a[0] += 1
print(a[0] is x)

แต่นี่ไม่ใช่พิเศษสำหรับรายการ แทนที่a[0]ด้วยรหัสนั้นด้วยyและคุณจะได้ผลลัพธ์เหมือนกันทุกประการ

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