Danh sách các thay đổi danh sách được phản ánh trên các danh sách phụ bất ngờ


645

Tôi cần tạo một danh sách các danh sách trong Python, vì vậy tôi đã gõ như sau:

myList = [[1] * 4] * 3

Danh sách trông như thế này:

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

Sau đó, tôi đã thay đổi một trong những giá trị trong cùng:

myList[0][0] = 5

Bây giờ danh sách của tôi trông như thế này:

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

đó không phải là những gì tôi muốn hoặc mong đợi. Ai đó có thể vui lòng giải thích những gì đang xảy ra, và làm thế nào để khắc phục nó?

Câu trả lời:


560

Khi bạn viết [x]*3bạn nhận được, về cơ bản, danh sách [x, x, x]. Đó là, một danh sách với 3 tài liệu tham khảo giống nhau x. Khi bạn sửa đổi đĩa đơn này, xnó sẽ hiển thị thông qua cả ba tham chiếu đến nó:

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

Để khắc phục, bạn cần đảm bảo rằng bạn tạo một danh sách mới tại mỗi vị trí. Một cách để làm điều đó là

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

sẽ đánh giá lại [1]*4mỗi lần thay vì đánh giá nó một lần và thực hiện 3 tham chiếu đến 1 danh sách.


Bạn có thể tự hỏi tại sao *không thể tạo ra các đối tượng độc lập theo cách hiểu danh sách. Đó là bởi vì toán tử nhân *hoạt động trên các đối tượng, mà không thấy các biểu thức. Khi bạn sử dụng *để nhân [[1] * 4]3, *chỉ thấy danh sách 1 phần tử [[1] * 4]ước tính chứ không phải [[1] * 4văn bản biểu thức. *không biết làm thế nào để tạo các bản sao của phần tử đó, không biết làm thế nào để đánh giá lại [[1] * 4]và không có ý tưởng nào bạn thậm chí muốn bản sao và nói chung, thậm chí có thể không có cách nào để sao chép phần tử đó.

Tùy chọn duy nhất *có là tạo các tham chiếu mới cho danh sách phụ hiện có thay vì cố gắng tạo danh sách phụ mới. Bất cứ điều gì khác sẽ không nhất quán hoặc yêu cầu thiết kế lại chính các quyết định thiết kế ngôn ngữ cơ bản.

Ngược lại, một sự hiểu biết danh sách đánh giá lại biểu thức phần tử trên mỗi lần lặp. [[1] * 4 for n in range(3)]đánh giá lại [1] * 4mọi lúc vì cùng một lý do [x**2 for x in range(3)]đánh giá lại x**2mỗi lần. Mỗi đánh giá [1] * 4tạo ra một danh sách mới, vì vậy việc hiểu danh sách thực hiện những gì bạn muốn.

Ngẫu nhiên, [1] * 4cũng không sao chép các yếu tố của [1], nhưng điều đó không quan trọng, vì số nguyên là bất biến. Bạn không thể làm một cái gì đó như 1.value = 2và biến 1 thành 2.


24
Tôi ngạc nhiên rằng không có cơ thể chỉ ra rằng, câu trả lời ở đây là sai lệch. [x]*3lưu trữ 3 tài liệu tham khảo như thế [x, x, x]chỉ đúng khi xcó thể thay đổi. Điều này không hoạt động a=[4]*3, ví dụ , sau đó a[0]=5,a=[5,4,4].
Allanqunzi

42
Về mặt kỹ thuật, nó vẫn đúng. [4]*3về cơ bản là tương đương với x = 4; [x, x, x]. Tuy nhiên, sự thật là điều này sẽ không bao giờ gây ra bất kỳ vấn đề nào vì 4nó là bất biến. Ngoài ra, ví dụ khác của bạn không thực sự là một trường hợp khác. a = [x]*3; a[0] = 5sẽ không gây ra vấn đề ngay cả khi xcó thể thay đổi, vì bạn không sửa đổi x, chỉ sửa đổi a. Tôi sẽ không mô tả câu trả lời của tôi là sai lệch hoặc không chính xác - bạn chỉ không thể tự bắn vào chân mình nếu bạn đang đối phó với các vật thể bất biến.
CAdaker

19
@ ALLanqunzi bạn sai rồi. Làm x = 1000; lst = [x]*2; lst[0] is lst[1]-> True. Python không phân biệt giữa các đối tượng có thể thay đổi và bất biến ở đây.
hồ bấm giờ

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

Khung và đối tượng

Giáo viên dạy Python trực quan Trực quan hóa


Vậy, tại sao nếu chúng ta viết ma trận = [[x] * 2] không tạo ra 2 elemnt cho cùng một đối tượng như ví dụ bạn mô tả, thì nó dường như là cùng một khái niệm, tôi còn thiếu gì?
Ahmed Mohamed

@AhmedMohamed Thật vậy, nó tạo một danh sách với hai thành phần của cùng một đối tượng chính xác xđề cập đến. Nếu bạn tạo một vật thể độc nhất toàn cầu bằng x = object()và sau đó biến matrix = [[x] * 2]chúng thành sự thật:matrix[0][0] is matrix[0][1]
nadrimajstor

@nadrimajstor vậy tại sao sự thay đổi trong ma trận [0] không ảnh hưởng đến ma trận [1] như ví dụ trên với ma trận 2d.
Ahmed Mohamed

@AhmedMohamed Bất ngờ xuất hiện khi bạn tạo một "bản sao" của chuỗi có thể thay đổi (trong ví dụ của chúng tôi là a list) vì vậy nếu a row = [x] * 2hơn một hàng matrix = [row] * 2mà cả hai hàng chính xác là cùng một đối tượng, và bây giờ thay đổi thành một hàng matrix[0][0] = yđột nhiên phản ánh trong một hàng khác(matrix[0][0] is matrix[1][0]) == True
nadrimajstor

@AhmedMohamed Hãy xem Ned Batchelder - Sự thật và huyền thoại về tên và giá trị của Python vì nó có thể đưa ra một lời giải thích tốt hơn. :)
nadrimajstor

52

Trên thực tế, đây là chính xác những gì bạn mong đợi. Hãy phân tích những gì đang xảy ra ở đây:

Bạn viết

lst = [[1] * 4] * 3

Điều này tương đương với:

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

Điều này có nghĩa lstlà một danh sách với 3 yếu tố tất cả chỉ vào lst1. Điều này có nghĩa là hai dòng sau là tương đương:

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

Như lst[0]không có gì nhưng lst1.

Để có được hành vi mong muốn, bạn có thể sử dụng hiểu danh sách:

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

Trong trường hợp này, biểu thức được đánh giá lại cho mỗi n, dẫn đến một danh sách khác nhau.


Chỉ là một bổ sung nhỏ cho câu trả lời hay ở đây: hiển nhiên là bạn đang xử lý cùng một đối tượng nếu bạn làm id(lst[0][0])id(lst[1][0])thậm chí id(lst[0])id(lst[1])
Sergiy Kolodyazhnyy

36
[[1] * 4] * 3

hoặc thậm chí:

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

Tạo danh sách tham chiếu nội bộ [1,1,1,1]3 lần - không phải ba bản sao của danh sách bên trong, vì vậy bất cứ khi nào bạn sửa đổi danh sách (ở bất kỳ vị trí nào), bạn sẽ thấy thay đổi ba lần.

Nó giống như ví dụ này:

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

nơi có lẽ ít ngạc nhiên hơn.


3
Bạn có thể sử dụng toán tử "is" để khám phá điều này. ls [0] là ls [1] trả về True.
mipadi

9

Bên cạnh câu trả lời được chấp nhận đã giải thích chính xác vấn đề, trong phạm vi hiểu danh sách của bạn, nếu Bạn đang sử dụng python-2.x, sử dụng xrange()trả về một trình tạo hiệu quả hơn ( range()trong python 3 thực hiện cùng một công việc) _thay vì biến ném đi n:

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

Ngoài ra, như một cách Pythonic hơn nhiều, bạn có thể sử dụng itertools.repeat()để tạo một đối tượng lặp của các phần tử lặp lại:

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

PS Sử dụng NumPy, nếu bạn chỉ muốn tạo một mảng của những người thân hoặc zero bạn có thể sử dụng np.onesnp.zerosvà / hoặc sử dụng số khác 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

Các thùng chứa Python chứa các tham chiếu đến các đối tượng khác. Xem ví dụ này:

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

Trong đây blà danh sách chứa một mục có tham chiếu đến danh sách a. Danh sách anày là đột biến.

Phép nhân của một danh sách theo một số nguyên tương đương với việc thêm danh sách đó vào nhiều lần (xem các thao tác trình tự phổ biến ). Vì vậy, tiếp tục với ví dụ:

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

Chúng ta có thể thấy rằng danh sách chiện chứa hai tham chiếu đến danh sách atương đương với c = b * 2.

Câu hỏi thường gặp về Python cũng chứa giải thích về hành vi này: Làm cách nào để tạo danh sách đa chiều?


6

myList = [[1]*4] * 3tạo một đối tượng danh sách [1,1,1,1]trong bộ nhớ và sao chép tham chiếu của nó 3 lần. Điều này tương đương với obj = [1,1,1,1]; myList = [obj]*3. Bất kỳ sửa đổi nào objsẽ được phản ánh tại ba nơi, bất cứ nơi nào objđược tham chiếu trong danh sách. Tuyên bố đúng sẽ là:

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

hoặc là

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

Điều quan trọng cần lưu ý ở đây*toán tử chủ yếu được sử dụng để tạo một danh sách các chữ . Mặc dù 1là bất biến, obj =[1]*4nhưng vẫn sẽ tạo một danh sách 1lặp lại 4 lần để hình thành [1,1,1,1]. Nhưng nếu bất kỳ tham chiếu nào đến một đối tượng bất biến được tạo ra, đối tượng đó sẽ bị ghi đè bằng một đối tượng mới.

Điều này có nghĩa là nếu chúng ta làm obj[1]=42, sau đó objsẽ trở thành [1,42,1,1] không [42,42,42,42] như một số người có thể giả định. Điều này cũng có thể được xác minh:

>>> 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
Đó không phải là về nghĩa đen. obj[2] = 42 thay thế tham chiếu tại chỉ mục 2, trái ngược với việc làm biến đổi đối tượng được tham chiếu bởi chỉ mục đó, đó là những gìmyList[2][0] = ... ( myList[2]là một danh sách, và sự thay đổi làm thay đổi tham chiếu ở chỉ số 0 trong danh sách tha). Tất nhiên, số nguyên không thể thay đổi, nhưng nhiều loại đối tượng . Và lưu ý rằng [....]ký hiệu hiển thị danh sách cũng là một dạng cú pháp theo nghĩa đen! Đừng nhầm lẫn hợp chất (như danh sách) và các đối tượng vô hướng (như số nguyên), với các đối tượng có thể thay đổi so với bất biến.
Martijn Pieters

5

Nói một cách đơn giản, điều này xảy ra bởi vì trong python mọi thứ đều hoạt động bằng cách tham chiếu , vì vậy khi bạn tạo một danh sách danh sách theo cách cơ bản bạn sẽ gặp phải những vấn đề như vậy.

Để giải quyết vấn đề của bạn, bạn có thể thực hiện một trong số chúng: 1. Sử dụng tài liệu mảng numpy cho numpy.empty 2. Nối danh sách khi bạn vào danh sách. 3. Bạn cũng có thể sử dụng từ điển nếu bạn muốn


2

Hãy để chúng tôi viết lại mã của bạn theo cách sau:

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

myList = [z] * 3

Sau đó, có mã này, chạy mã sau đây để làm cho mọi thứ rõ ràng hơn. Những gì mã làm về cơ bản là in ids của các đối tượng thu được, mà

Trả về danh tính của người Viking về một đối tượng

và sẽ giúp chúng tôi xác định chúng và phân tích những gì xảy ra:

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

Và bạn sẽ nhận được đầu ra sau:

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

Vì vậy, bây giờ chúng ta hãy đi từng bước. Bạn có xđó là 1, và một danh sách thành phần duy nhất ycó chứa x. Bước đầu tiên của bạn là y * 4sẽ đưa bạn một danh sách mới z, về cơ bản [x, x, x, x], tức là nó tạo ra một danh sách mới sẽ có 4 phần tử, là các tham chiếu đến xđối tượng ban đầu . Bước net khá giống nhau. Về cơ bản z * 3, bạn làm [[x, x, x, x]] * 3và trả lại [[x, x, x, x], [x, x, x, x], [x, x, x, x]], với cùng lý do như bước đầu tiên.


2

Tôi đoán mọi người giải thích những gì đang xảy ra. Tôi đề nghị một cách để giải quyết nó:

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

myList[0][0] = 5

print myList

Và sau đó bạn có:

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

2

Cố gắng giải thích nó một cách mô tả hơn,

Hoạt động 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]]

Hoạt động 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]]

Nhận thấy tại sao không sửa đổi phần tử đầu tiên của danh sách đầu tiên không sửa đổi phần tử thứ hai của mỗi danh sách? Đó là bởi vì [0] * 2thực sự là một danh sách gồm hai số và tham chiếu đến 0 không thể được sửa đổi.

Nếu bạn muốn tạo bản sao, hãy thử Hoạt động 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]]

một cách thú vị khác để tạo bản sao, Chiến dịch 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 từ phép nhân danh sách Python: [[...]] * 3 tạo 3 danh sách phản chiếu lẫn nhau khi tôi sửa đổi và tôi có cùng một câu hỏi về "Tại sao chỉ có bên ngoài * 3 tạo thêm tham chiếu trong khi bên trong không Tại sao không phải là tất cả 1 giây? "

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

Đây là lời giải thích của tôi sau khi thử mã ở trên:

  • Bên trong *3cũng tạo ra các tham chiếu, nhưng các tham chiếu của nó là bất biến, giống như [&0, &0, &0]khi thay đổi li[0], bạn không thể thay đổi bất kỳ tham chiếu cơ bản nào của const int 0, vì vậy bạn chỉ có thể thay đổi địa chỉ tham chiếu thành địa chỉ mới &1;
  • trong khi ma=[&li, &li, &li]licó thể thay đổi, vì vậy khi bạn gọi ma[0][0]=1, ma [0] [0] bằng nhau &li[0], vì vậy tất cả các &litrường hợp sẽ thay đổi địa chỉ đầu tiên của nó thành &1.

1

Bằng cách sử dụng chức năng danh sách sẵn có, bạn có thể làm như thế này

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
Lưu ý, bước thứ tư có thể bị hủy nếu bạn thực hiện bước thứ hai:a.insert(0,[5,1,1,1])
U10-Chuyển tiếp
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.