รายการการเปลี่ยนแปลงที่แสดงในรายการย่อยโดยไม่คาดคิด


645

ฉันต้องการสร้างรายการของรายการใน Python ดังนั้นฉันจึงพิมพ์รายการต่อไปนี้:

myList = [[1] * 4] * 3

รายการมีลักษณะดังนี้:

[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]  

จากนั้นฉันเปลี่ยนหนึ่งในค่าในสุด:

myList[0][0] = 5

ตอนนี้รายการของฉันมีลักษณะเช่นนี้:

[[5, 1, 1, 1], [5, 1, 1, 1], [5, 1, 1, 1]]  

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

คำตอบ:


560

เมื่อคุณเขียนที่คุณได้รับเป็นหลักรายการ[x]*3 [x, x, x]นั่นคือรายการที่มีการอ้างอิงถึง 3 xเดียวกัน เมื่อคุณปรับเปลี่ยนซิงเกิ้ลนี้xจะสามารถมองเห็นได้ผ่านการอ้างอิงทั้งสาม:

x = [1] * 4
l = [x] * 3
print(f"id(x): {id(x)}")
# id(x): 140560897920048
print(
    f"id(l[0]): {id(l[0])}\n"
    f"id(l[1]): {id(l[1])}\n"
    f"id(l[2]): {id(l[2])}"
)
# id(l[0]): 140560897920048
# id(l[1]): 140560897920048
# id(l[2]): 140560897920048

x[0] = 42
print(f"x: {x}")
# x: [42, 1, 1, 1]
print(f"l: {l}")
# l: [[42, 1, 1, 1], [42, 1, 1, 1], [42, 1, 1, 1]]

หากต้องการแก้ไขคุณต้องตรวจสอบให้แน่ใจว่าคุณได้สร้างรายการใหม่ในแต่ละตำแหน่ง วิธีหนึ่งที่จะทำคือ

[[1]*4 for _ in range(3)]

ซึ่งจะประเมินใหม่[1]*4ทุกครั้งแทนที่จะประเมินเพียงครั้งเดียวและทำการอ้างอิง 3 รายการต่อ 1 รายการ


คุณอาจสงสัยว่าทำไม*ไม่สามารถสร้างวัตถุอิสระในแบบที่รายการเข้าใจได้ นั่นเป็นเพราะตัว*ดำเนินการคูณทำงานบนวัตถุโดยไม่เห็นนิพจน์ เมื่อคุณใช้*คูณ[[1] * 4]ด้วย 3 *จะเห็นรายการ 1 องค์ประกอบ[[1] * 4]ประเมินเท่านั้นไม่ใช่[[1] * 4ข้อความนิพจน์ *ไม่มีความคิดในการทำสำเนาขององค์ประกอบนั้นไม่มีความคิดว่าจะประเมินใหม่[[1] * 4]และไม่คิดว่าคุณต้องการคัดลอกและโดยทั่วไปอาจไม่มีวิธีในการคัดลอกองค์ประกอบ

ตัวเลือกเดียว*คือทำการอ้างอิงใหม่ไปยังรายการย่อยที่มีอยู่แทนที่จะพยายามสร้างรายการย่อยใหม่ สิ่งอื่นใดจะไม่สอดคล้องกันหรือต้องการการออกแบบที่สำคัญในการตัดสินใจออกแบบภาษาพื้นฐาน

ในทางตรงกันข้ามความเข้าใจในรายการจะประเมินค่าการแสดงออกองค์ประกอบในทุกการวนซ้ำ [[1] * 4 for n in range(3)]ประเมินค่าใหม่[1] * 4ทุกครั้งด้วยเหตุผลเดียวกัน[x**2 for x in range(3)]ประเมินค่าใหม่x**2ทุกครั้ง การประเมินทุก[1] * 4รายการสร้างรายการใหม่ดังนั้นความเข้าใจในรายการจึงเป็นสิ่งที่คุณต้องการ

อนึ่ง[1] * 4ก็ไม่ได้คัดลอกองค์ประกอบของ[1]แต่ไม่สำคัญเนื่องจากจำนวนเต็มไม่เปลี่ยนรูป คุณไม่สามารถทำสิ่งที่ชอบ1.value = 2และเปลี่ยน 1 เป็น 2


24
ฉันแปลกใจที่ไม่มีร่างกายชี้ให้เห็นว่าคำตอบที่นี่ทำให้เข้าใจผิด [x]*3การอ้างอิง 3 ร้านค้าเช่น[x, x, x]ถูกต้องxก็ต่อเมื่อไม่แน่นอน งานนี้กวนสำหรับเช่นa=[4]*3ที่หลังa[0]=5,a=[5,4,4].
Allanqunzi

42
ในทางเทคนิคมันยังคงถูกต้อง เป็นหลักเทียบเท่ากับ[4]*3 x = 4; [x, x, x]แม้ว่ามันจะเป็นความจริง แต่สิ่งนี้จะไม่ทำให้เกิดปัญหาใด ๆเนื่องจาก4จะไม่เปลี่ยนรูป นอกจากนี้ตัวอย่างอื่น ๆ ของคุณก็ไม่ได้เป็นกรณีที่แตกต่างกันจริงๆ a = [x]*3; a[0] = 5จะไม่ทำให้เกิดปัญหามากถ้าxเป็นไม่แน่นอนเนื่องจากคุณไม่ได้ปรับเปลี่ยนเพียงการปรับเปลี่ยนx aฉันจะไม่อธิบายคำตอบของฉันว่าทำให้เข้าใจผิดหรือไม่ถูกต้อง - คุณไม่สามารถยิงเท้าของคุณเองถ้าคุณกำลังรับมือกับวัตถุที่ไม่เปลี่ยนรูป
CAdaker

19
@Allanqunzi คุณผิด Do ->x = 1000; lst = [x]*2; lst[0] is lst[1] TruePython ไม่แยกความแตกต่างระหว่างวัตถุที่เปลี่ยนแปลงได้และไม่เปลี่ยนรูปได้ที่นี่
timgeb

129
size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for i in range(size)]

เฟรมและวัตถุ

Live Python Tutor เห็นภาพ


ดังนั้นถ้าเราเขียนเมทริกซ์ = [[x] * 2] ไม่ได้ทำ 2 elemnts สำหรับวัตถุเดียวกันเหมือนตัวอย่างที่คุณอธิบายดูเหมือนว่าจะเป็นแนวคิดเดียวกันฉันหายไปอะไร
อาเหม็ดโมฮาเหม็ด

@ AhmedMohamed แน่นอนว่ามันสร้างรายการที่มีสององค์ประกอบของวัตถุเดียวกันแน่นอนที่xอ้างถึง หากคุณสร้างวัตถุที่ไม่ซ้ำกันทั่วโลกด้วย x = object()แล้วทำให้matrix = [[x] * 2]สิ่งเหล่านี้เป็นจริง:matrix[0][0] is matrix[0][1]
nadrimajstor

@nadrimajstor ดังนั้นทำไมการเปลี่ยนแปลงในเมทริกซ์ [0] จึงไม่ส่งผลกระทบต่อเมทริกซ์ [1] เหมือนตัวอย่างด้านบนด้วยเมทริกซ์ 2 มิติ
Ahmed Mohamed

@ AhmedMohamed Surprise มาเมื่อคุณสร้าง "คัดลอก" ของลำดับที่ไม่แน่นอน (ในตัวอย่างของเรามันคือ a list) ดังนั้นหาก a row = [x] * 2มากกว่าmatrix = [row] * 2ที่ซึ่งทั้งสองแถวเป็นวัตถุเดียวกันและตอนนี้เปลี่ยนเป็นหนึ่งแถวmatrix[0][0] = yก็สะท้อนให้เห็นในอีกหนึ่ง(matrix[0][0] is matrix[1][0]) == True
nadrimajstor

@AhmedMohamed ดูที่Ned Batchelder - ข้อเท็จจริงและตำนานเกี่ยวกับชื่อและค่า Pythonเนื่องจากอาจมีคำอธิบายที่ดีกว่า :)
nadrimajstor

52

จริงๆแล้วนี่คือสิ่งที่คุณคาดหวัง เรามาสลายสิ่งที่เกิดขึ้นที่นี่:

ที่คุณเขียน

lst = [[1] * 4] * 3

นี่เทียบเท่ากับ:

lst1 = [1]*4
lst = [lst1]*3

วิธีนี้lstเป็นรายการที่มี 3 lst1องค์ประกอบชี้ทั้งหมดไปยัง นี่หมายความว่าทั้งสองบรรทัดต่อไปนี้เทียบเท่ากัน:

lst[0][0] = 5
lst1[0] = 5

ในฐานะที่เป็นlst[0]คืออะไร lst1แต่

ในการรับพฤติกรรมที่ต้องการคุณสามารถใช้ list comprehension:

lst = [ [1]*4 for n in range(3) ] #python 3
lst = [ [1]*4 for n in xrange(3) ] #python 2

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


เพียงเล็กน้อยนอกจากนี้เพื่อคำตอบที่ดีที่นี่: เห็นได้ชัดว่าคุณกำลังจัดการกับวัตถุเดียวกันถ้าคุณทำid(lst[0][0])และid(lst[1][0])หรือแม้กระทั่งid(lst[0])และid(lst[1])
Sergiy Kolodyazhnyy

36
[[1] * 4] * 3

หรือแม้กระทั่ง:

[[1, 1, 1, 1]] * 3

สร้างรายการที่อ้างอิงภายใน[1,1,1,1]3 ครั้ง - ไม่ใช่สำเนาภายในของสามรายการดังนั้นเมื่อใดก็ตามที่คุณแก้ไขรายการ (ในตำแหน่งใด ๆ ) คุณจะเห็นการเปลี่ยนแปลงสามครั้ง

มันเหมือนกับตัวอย่างนี้:

>>> inner = [1,1,1,1]
>>> outer = [inner]*3
>>> outer
[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]
>>> inner[0] = 5
>>> outer
[[5, 1, 1, 1], [5, 1, 1, 1], [5, 1, 1, 1]]

ที่มันอาจจะแปลกใจเล็กน้อย


3
คุณสามารถใช้โอเปอเรเตอร์ "is" เพื่อค้นหาสิ่งนี้ ls [0] คือ ls [1] ผลตอบแทนจริง
mipadi

9

นอกเหนือจากคำตอบที่ได้รับการยอมรับซึ่งอธิบายปัญหาได้อย่างถูกต้องภายในรายการความเข้าใจของคุณหากคุณใช้ python-2.x การใช้xrange()ที่ส่งกลับตัวกำเนิดที่มีประสิทธิภาพมากขึ้น ( range()ใน python 3 ทำงานเหมือนกัน) _แทนตัวแปร throwaway n:

[[1]*4 for _ in xrange(3)]      # and in python3 [[1]*4 for _ in range(3)]

นอกจากนี้ยังเป็นวิธีการPythonicอื่น ๆ อีกมากมายที่คุณสามารถใช้itertools.repeat()เพื่อสร้างวัตถุตัววนซ้ำขององค์ประกอบที่ทำซ้ำ:

>>> a=list(repeat(1,4))
[1, 1, 1, 1]
>>> a[0]=5
>>> a
[5, 1, 1, 1]

ป.ล. ใช้ numpy ถ้าคุณเพียงต้องการสร้างอาร์เรย์ของคนหรือศูนย์คุณสามารถใช้np.onesและnp.zerosและ / หรือใช้ตัวเลขอื่น ๆnp.repeat():

In [1]: import numpy as np

In [2]: 

In [2]: np.ones(4)
Out[2]: array([ 1.,  1.,  1.,  1.])

In [3]: np.ones((4, 2))
Out[3]: 
array([[ 1.,  1.],
       [ 1.,  1.],
       [ 1.,  1.],
       [ 1.,  1.]])

In [4]: np.zeros((4, 2))
Out[4]: 
array([[ 0.,  0.],
       [ 0.,  0.],
       [ 0.,  0.],
       [ 0.,  0.]])

In [5]: np.repeat([7], 10)
Out[5]: array([7, 7, 7, 7, 7, 7, 7, 7, 7, 7])

6

ภาชนะหลามประกอบด้วยการอ้างอิงไปยังวัตถุอื่น ดูตัวอย่างนี้:

>>> a = []
>>> b = [a]
>>> b
[[]]
>>> a.append(1)
>>> b
[[1]]

ในการนี้เป็นรายการที่มีรายการหนึ่งที่มีการอ้างอิงถึงรายการb aรายการaไม่แน่นอน

การคูณรายการด้วยจำนวนเต็มเทียบเท่ากับการเพิ่มรายการเข้ากับตัวเองหลาย ๆ ครั้ง (ดูการดำเนินการตามลำดับทั่วไป ) ดังนั้นดำเนินการต่อด้วยตัวอย่าง:

>>> c = b + b
>>> c
[[1], [1]]
>>>
>>> a[0] = 2
>>> c
[[2], [2]]

เราจะเห็นว่ารายการcในขณะนี้มีการอ้างอิงถึงสองรายการซึ่งเทียบเท่ากับac = b * 2

Python คำถามที่พบบ่อยประกอบด้วยคำอธิบายเกี่ยวกับพฤติกรรมนี้: ฉันจะสร้างรายการหลายมิติได้อย่างไร


6

myList = [[1]*4] * 3สร้างวัตถุรายการหนึ่ง[1,1,1,1]ในหน่วยความจำและคัดลอกการอ้างอิงของมัน 3 ครั้ง obj = [1,1,1,1]; myList = [obj]*3นี้จะเทียบเท่ากับ การแก้ไขใด ๆ ที่objจะปรากฏในสามแห่งไม่ว่าobjจะมีการอ้างอิงที่ใดในรายการ ข้อความที่ถูกต้องจะเป็น:

myList = [[1]*4 for _ in range(3)]

หรือ

myList = [[1 for __ in range(4)] for _ in range(3)]

สิ่งที่สำคัญที่จะต้องทราบที่นี่คือการที่*ผู้ประกอบการเป็นส่วนใหญ่ใช้ในการสร้างรายชื่อของตัวอักษร แม้ว่า1จะไม่เปลี่ยนรูปobj =[1]*4จะยังคงสร้างรายการของ1ซ้ำ 4 [1,1,1,1]ครั้งไปยังรูปแบบ แต่ถ้ามีการอ้างอิงถึงวัตถุที่ไม่เปลี่ยนรูปแบบวัตถุนั้นจะถูกเขียนทับด้วยวัตถุใหม่

ซึ่งหมายความว่าถ้าเราทำobj[1]=42แล้วobjจะ[1,42,1,1] ไม่ [42,42,42,42]เป็นอย่างที่บางคนคิด สิ่งนี้สามารถตรวจสอบได้:

>>> myList = [1]*4
>>> myList
[1, 1, 1, 1]

>>> id(myList[0])
4522139440
>>> id(myList[1]) # Same as myList[0]
4522139440

>>> myList[1] = 42 # Since myList[1] is immutable, this operation overwrites myList[1] with a new object changing its id.
>>> myList
[1, 42, 1, 1]

>>> id(myList[0])
4522139440
>>> id(myList[1]) # id changed
4522140752
>>> id(myList[2]) # id still same as myList[0], still referring to value `1`.
4522139440

2
มันไม่เกี่ยวกับตัวอักษร obj[2] = 42 แทนที่การอ้างอิงที่ดัชนี2เมื่อเทียบกับการกลายพันธุ์วัตถุที่อ้างอิงโดยดัชนีนั้นซึ่งเป็นสิ่งที่myList[2][0] = ...ทำ ( myList[2]เป็นรายการและการประเมินการเปลี่ยนแปลงการอ้างอิงที่ดัชนี 0 ในรายการ tha) แน่นอนว่าจำนวนเต็มไม่สามารถเปลี่ยนแปลงได้ แต่มีชนิดของวัตถุจำนวนมาก และโปรดทราบว่า[....]สัญกรณ์การแสดงรายการยังเป็นรูปแบบของตัวอักษร! อย่าสับสนสารประกอบ (เช่นรายการ) และวัตถุสเกลาร์ (เช่นจำนวนเต็ม) กับวัตถุที่เปลี่ยนแปลงไม่ได้และไม่เปลี่ยนรูป
Martijn Pieters

5

คำง่ายๆนี้เกิดขึ้นเพราะใน python ทุกอย่างทำงานได้โดยการอ้างอิงดังนั้นเมื่อคุณสร้างรายการของรายการที่คุณมักจะจบลงด้วยปัญหาดังกล่าว

ในการแก้ปัญหาของคุณคุณสามารถเลือกทำอย่างใดอย่างหนึ่งดังต่อไปนี้: 1. ใช้เอกสารอาร์เรย์ numpy สำหรับ numpy.empty 2. ผนวกรายการตามที่คุณได้รับในรายการ 3. คุณสามารถใช้พจนานุกรมได้หากต้องการ


2

ให้เราเขียนรหัสของคุณใหม่ด้วยวิธีต่อไปนี้:

x = 1
y = [x]
z = y * 4

myList = [z] * 3

จากนั้นให้ใช้รหัสต่อไปนี้เพื่อทำให้ทุกอย่างชัดเจนยิ่งขึ้น สิ่งที่รหัสไม่เป็นพื้นพิมพ์ids ของวัตถุที่ได้รับซึ่ง

ส่งคืน“ เอกลักษณ์” ของวัตถุ

และจะช่วยเราระบุและวิเคราะห์สิ่งที่เกิดขึ้น:

print("myList:")
for i, subList in enumerate(myList):
    print("\t[{}]: {}".format(i, id(subList)))
    for j, elem in enumerate(subList):
        print("\t\t[{}]: {}".format(j, id(elem)))

และคุณจะได้รับผลลัพธ์ต่อไปนี้:

x: 1
y: [1]
z: [1, 1, 1, 1]
myList:
    [0]: 4300763792
        [0]: 4298171528
        [1]: 4298171528
        [2]: 4298171528
        [3]: 4298171528
    [1]: 4300763792
        [0]: 4298171528
        [1]: 4298171528
        [2]: 4298171528
        [3]: 4298171528
    [2]: 4300763792
        [0]: 4298171528
        [1]: 4298171528
        [2]: 4298171528
        [3]: 4298171528

ดังนั้นให้เราไปทีละขั้นตอน คุณมีxซึ่งเป็น1และรายการองค์ประกอบหนึ่งที่มีy xขั้นตอนแรกของคุณคือการy * 4ที่คุณจะได้รับรายการใหม่zซึ่งโดยพื้นฐาน[x, x, x, x]แล้วคือมันจะสร้างรายการใหม่ซึ่งจะมี 4 องค์ประกอบซึ่งเป็นการอ้างอิงถึงxวัตถุเริ่มต้น ขั้นตอนสุทธิคล้ายกันมาก โดยทั่วไปคุณทำz * 3ซึ่งก็คือ[[x, x, x, x]] * 3และส่งคืน[[x, x, x, x], [x, x, x, x], [x, x, x, x]]ด้วยเหตุผลเดียวกับขั้นตอนแรก


2

ฉันเดาว่าทุกคนจะอธิบายสิ่งที่เกิดขึ้น ฉันขอแนะนำวิธีหนึ่งในการแก้ไข:

myList = [[1 for i in range(4)] for j in range(3)]

myList[0][0] = 5

print myList

แล้วคุณมี:

[[5, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]

2

พยายามอธิบายให้ละเอียดยิ่งขึ้น

การดำเนินงาน 1:

x = [[0, 0], [0, 0]]
print(type(x)) # <class 'list'>
print(x) # [[0, 0], [0, 0]]

x[0][0] = 1
print(x) # [[1, 0], [0, 0]]

การดำเนินงาน 2:

y = [[0] * 2] * 2
print(type(y)) # <class 'list'>
print(y) # [[0, 0], [0, 0]]

y[0][0] = 1
print(y) # [[1, 0], [1, 0]]

สังเกตว่าทำไมไม่แก้ไของค์ประกอบแรกของรายการแรกไม่ได้แก้ไของค์ประกอบที่สองของแต่ละรายการ นั่นเป็นเพราะ[0] * 2รายการของตัวเลขสองตัวจริงๆและการอ้างอิงถึง 0 ไม่สามารถแก้ไขได้

หากคุณต้องการสร้างสำเนาโคลนลองใช้งาน 3:

import copy
y = [0] * 2   
print(y)   # [0, 0]

y = [y, copy.deepcopy(y)]  
print(y) # [[0, 0], [0, 0]]

y[0][0] = 1
print(y) # [[1, 0], [0, 0]]

อีกวิธีที่น่าสนใจในการสร้างสำเนาโคลนปฏิบัติการ 4:

import copy
y = [0] * 2
print(y) # [0, 0]

y = [copy.deepcopy(y) for num in range(1,5)]
print(y) # [[0, 0], [0, 0], [0, 0], [0, 0]]

y[0][0] = 5
print(y) # [[5, 0], [0, 0], [0, 0], [0, 0]]

2

@spelchekr จากการทวีคูณรายการ Python: [[... ]] * 3 ทำ 3 รายการซึ่งสะท้อนซึ่งกันและกันเมื่อแก้ไขและฉันมีคำถามเดียวกันเกี่ยวกับ "เหตุใดภายนอก * 3 จึงสร้างการอ้างอิงเพิ่มเติมในขณะที่รายการภายในไม่ได้ ทำไมมันไม่ใช่ทั้งหมด

li = [0] * 3
print([id(v) for v in li]) # [140724141863728, 140724141863728, 140724141863728]
li[0] = 1
print([id(v) for v in li]) # [140724141863760, 140724141863728, 140724141863728]
print(id(0)) # 140724141863728
print(id(1)) # 140724141863760
print(li) # [1, 0, 0]

ma = [[0]*3] * 3 # mainly discuss inner & outer *3 here
print([id(li) for li in ma]) # [1987013355080, 1987013355080, 1987013355080]
ma[0][0] = 1
print([id(li) for li in ma]) # [1987013355080, 1987013355080, 1987013355080]
print(ma) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

นี่คือคำอธิบายของฉันหลังจากลองใช้โค้ดด้านบน:

  • ภายใน*3ยังสร้างการอ้างอิง แต่อ้างอิงมันจะไม่เปลี่ยนรูปสิ่งที่ต้องการ[&0, &0, &0]จากนั้นเมื่อมีการเปลี่ยนแปลงli[0]ที่คุณไม่สามารถเปลี่ยนการอ้างอิงพื้นฐานของ int const 0ดังนั้นคุณก็สามารถเปลี่ยนที่อยู่อ้างอิงเข้ามาใหม่&1;
  • ในขณะที่ma=[&li, &li, &li]และliไม่แน่นอนดังนั้นเมื่อคุณโทรหาma[0][0]=1แม่ [0] [0] เป็นอย่างเท่าเทียมกัน&li[0]ดังนั้นทุก&liกรณีจะมีการเปลี่ยนแปลงที่อยู่ที่ 1 &1เข้าไปใน

1

โดยใช้ฟังก์ชั่นรายการ inbuilt คุณสามารถทำเช่นนี้

a
out:[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]
#Displaying the list

a.remove(a[0])
out:[[1, 1, 1, 1], [1, 1, 1, 1]]
# Removed the first element of the list in which you want altered number

a.append([5,1,1,1])
out:[[1, 1, 1, 1], [1, 1, 1, 1], [5, 1, 1, 1]]
# append the element in the list but the appended element as you can see is appended in last but you want that in starting

a.reverse()
out:[[5, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]
#So at last reverse the whole list to get the desired list

1
หมายเหตุขั้นตอนที่สี่สามารถลดลงได้หากคุณทำขั้นตอนที่สอง:a.insert(0,[5,1,1,1])
U10- ส่งต่อ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.