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

Oct 27 2008

Tôi cần tạo một danh sách các danh sách bằng Python, vì vậy tôi đã nhập 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 để giải thích nó?

Trả lời

614 CAdaker Oct 27 2008 at 22:03

[x]*3Về cơ bản, khi bạn viết, bạn nhận được danh sách [x, x, x]. Đó là, một danh sách có 3 tham chiếu đến giống nhau x. Sau đó, 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 ở 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á một lần và tạo 3 tham chiếu cho 1 danh sách.


Bạn có thể thắc mắc tại sao *không thể tạo các đối tượng độc lập theo cách mà cách hiểu danh sách làm. Đó là bởi vì toán tử nhân *hoạt động trên các đối tượng, mà không nhìn thấy biểu thức. Khi bạn sử dụng *để nhân [[1] * 4]với 3, *chỉ thấy danh sách 1 phần tử được [[1] * 4]đánh giá là chứ không phải [[1] * 4văn bản biểu thức. *không biết cách tạo bản sao của phần tử đó, không biết cách đánh giá lại [[1] * 4]và không biết 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ử đó.

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

Ngược lại, khả năng hiểu 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ần 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 lần đánh giá [1] * 4sẽ tạo ra một danh sách mới, do đó, 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 phần 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 điều gì đó giống như 1.value = 2và biến 1 thành 2.

140 nadrimajstor Aug 27 2013 at 06:17
size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for i in range(size)]

Trực quan hóa gia sư Python trực tiếp

56 PierreBdR Oct 27 2008 at 22:07

Trên thực tế, đây chính xác là 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

Đây có nghĩa lstlà một danh sách có 3 phần tử đều trỏ đến 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 tính nă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.

38 BlairConrad Oct 27 2008 at 22:02
[[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 kỳ 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 một chút.

10 Kasravnd Jun 18 2015 at 00:08

Cùng với câu trả lời được chấp nhận đã giải thích sự cố một cách chính xác, trong khả năng hiểu danh sách của bạn, nếu Bạn đang sử dụng sử dụng python-2.x xrange()trả về trình tạo hiệu quả hơn ( range()trong python 3 thực hiện công việc tương tự) _thay vì biến số 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 khác mà bạn có thể sử dụng itertools.repeat()để tạo một đối tượng trình 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])
7 ZbyněkWinkler Apr 06 2016 at 20:40

Vù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 là tham chiếu đến danh sách a. Danh sách acó thể thay đổi.

Việc nhân một danh sách với một số nguyên tương đương với việc cộng danh sách với chính nó nhiều lần (xem các phép toán dãy 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 cbây giờ 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?

7 jerrymouse Apr 06 2017 at 12:36

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. Mọi sửa đổi đối với objsẽ được phản ánh ở ba nơi, bất cứ nơi nào objđược tham chiếu trong danh sách. Câu lệnh đú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 danh sách các ký tự . Mặc dù 1là bất biến, obj =[1]*4vẫn sẽ tạo ra một danh sách 1lặp đi lặp lại 4 lần để tạo thành [1,1,1,1]. Nhưng nếu bất kỳ tham chiếu nào đến một đối tượng không thay đổi được thực hiện, đố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 vậy obj[1]=42, thì objsẽ [1,42,1,1] không như một số người có thể nghĩ. Điều này cũng có thể được xác minh: [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
5 bagrat Jun 10 2015 at 21:38

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ó điều này, hãy chạy đoạn mã sau để làm cho mọi thứ rõ ràng hơn. Những gì mã làm về cơ bản là in các idđối tượng thu được,

Trả lại "danh tính" của 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ì sẽ 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 kết quả 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 một. Bạn có xcái đó 1và một danh sách phần tử duy nhất ycó chứa x. Bước đầu tiên của bạn là y * 4sẽ tạo cho bạn một danh sách mới z, về cơ bản [x, x, x, x], tức là nó tạo 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 là khá giống nhau. Về cơ bản z * 3, bạn làm , đó là [[x, x, x, x]] * 3và trả về [[x, x, x, x], [x, x, x, x], [x, x, x, x]], với cùng lý do như đối với bước đầu tiên.

5 NeerajKomuravalli Jun 14 2016 at 13:36

Nói một cách đơn giản, điều này đang xảy ra bởi vì trong python mọi thứ đều hoạt động theo tham chiếu , vì vậy khi bạn tạo một danh sách theo cách đó về 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 hai cách sau: 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

4 awulll Apr 24 2016 at 20:31

Tôi đoán mọi người giải thích điều gì đang xảy ra. Tôi đề xuất 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]]
4 ouxiaogu Aug 09 2019 at 15:37

@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 được sửa đổi và tôi đã có cùng câu hỏi về "Tại sao chỉ có bên ngoài * 3 tạo nhiều tham chiếu hơn trong khi bên trong thì không ? Tại sao tất cả không phải là 1s? "

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, đại loại là [&0, &0, &0], khi nào thì 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;
  • while ma=[&li, &li, &li]licó thể thay đổi, vì vậy khi bạn gọi ma[0][0]=1, ma [0] [0] là như nhau &li[0], vì vậy tất cả các &liphiên bản sẽ thay đổi địa chỉ đầu tiên của nó thành &1.
3 AdilAbbasi Aug 10 2016 at 14:09

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à không thể sửa đổi tham chiếu đến 0.

Nếu bạn muốn tạo các bản sao chép, hãy thử Thao tác 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 nhân bản, Hoạt động 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 AnandTripathi Jul 15 2016 at 20:48

Bằng cách sử dụng chức năng danh sách có sẵn, 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
2 DeepakPatankar Jun 21 2020 at 18:34

Những câu hỏi này có rất nhiều câu trả lời, tôi đang thêm câu trả lời của mình để giải thích theo sơ đồ tương tự.

Cách bạn tạo 2D, tạo một danh sách nông

    arr = [[0]*cols]*row

Thay vào đó, nếu bạn muốn cập nhật các phần tử của danh sách, bạn nên sử dụng

   rows, cols = (5, 5) 
   arr = [[0 for i in range(cols)] for j in range(rows)] 

Giải thích :

Người ta có thể tạo một danh sách bằng cách sử dụng:

   arr = [0]*N 

hoặc là

   arr = [0 for i in range(N)] 

Trong trường hợp đầu tiên, tất cả các chỉ số của mảng đều trỏ đến cùng một đối tượng số nguyên

và khi bạn gán một giá trị cho một chỉ mục cụ thể, một đối tượng int mới sẽ được tạo, ví dụ: arr[4] = 5tạo

Bây giờ chúng ta hãy xem điều gì sẽ xảy ra khi chúng ta tạo một danh sách, trong trường hợp này, tất cả các phần tử của danh sách hàng đầu của chúng ta sẽ trỏ đến cùng một danh sách

Và nếu bạn cập nhật giá trị của bất kỳ chỉ mục nào, một đối tượng int mới sẽ được tạo. Nhưng vì tất cả các chỉ mục danh sách cấp cao nhất đều trỏ đến cùng một danh sách, nên tất cả các hàng sẽ trông giống nhau. Và bạn sẽ có cảm giác rằng cập nhật một phần tử là cập nhật tất cả các phần tử trong cột đó.

Tín dụng: Cảm ơn Pranav Devarakonda về lời giải thích dễ hiểu ở đây

Brian Oct 23 2020 at 02:57

Tôi đến đây vì tôi đang muốn xem làm cách nào để tôi có thể lồng một số lượng danh sách tùy ý. Ở trên có rất nhiều giải thích và ví dụ cụ thể, tuy nhiên bạn có thể khái quát N chiều danh sách danh sách các danh sách của ... bằng hàm đệ quy sau:

import copy

def list_ndim(dim, el=None, init=None):
    if init is None:
        init = el

    if len(dim)> 1:
        return list_ndim(dim[0:-1], None, [copy.copy(init) for x in range(dim[-1])])

    return [copy.deepcopy(init) for x in range(dim[0])]

Bạn thực hiện cuộc gọi đầu tiên của mình đến hàm như sau:

dim = (3,5,2)
el = 1.0
l = list_ndim(dim, el)

đâu (3,5,2)là một bộ kích thước của cấu trúc (tương tự như shapeđối số numpy ) và 1.0là phần tử bạn muốn cấu trúc được khởi tạo (cũng hoạt động với Không có). Lưu ý rằng initđối số chỉ được cung cấp bởi lệnh gọi đệ quy để chuyển tiếp các danh sách con lồng nhau

đầu ra của trên:

[[[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]],
 [[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]],
 [[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]]]

đặt các yếu tố cụ thể:

l[1][3][1] = 56
l[2][2][0] = 36.0+0.0j
l[0][1][0] = 'abc'

kết quả đầu ra:

[[[1.0, 1.0], ['abc', 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]],
 [[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 56.0], [1.0, 1.0]],
 [[1.0, 1.0], [1.0, 1.0], [(36+0j), 1.0], [1.0, 1.0], [1.0, 1.0]]]

bản chất không được đánh máy của danh sách được trình bày ở trên

mishsx Nov 23 2020 at 02:45

Lưu ý rằng các mục trong chuỗi không được sao chép; chúng được tham chiếu nhiều lần . Điều này thường ám ảnh các lập trình viên Python mới; xem xét:

>>> lists = [[]] * 3
>>> lists
[[], [], []]
>>> lists[0].append(3)
>>> lists
[[3], [3], [3]]

Điều đã xảy ra là [[]]danh sách một phần tử chứa danh sách trống, vì vậy cả ba phần tử [[]] * 3đều là tham chiếu đến danh sách trống duy nhất này. Việc sửa đổi bất kỳ phần tử nào của danh sách sẽ sửa đổi danh sách đơn này.

Một ví dụ khác để giải thích điều này là sử dụng mảng nhiều chiều .

Có thể bạn đã cố gắng tạo một mảng đa chiều như thế này:

>>> A = [[**None**] * 2] * 3

Điều này có vẻ chính xác nếu bạn in nó:

>>> A
[[None, None], [None, None], [None, None]]

Nhưng khi bạn chỉ định một giá trị, nó sẽ hiển thị ở nhiều nơi:

>>> A[0][0] = 5
>>> A
[[5, None], [5, None], [5, None]]

Lý do là vì sao chép một danh sách với  * không tạo ra các bản sao, nó chỉ tạo các tham chiếu đến các đối tượng hiện có. 3 tạo một danh sách chứa 3 tham chiếu đến cùng một danh sách có độ dài là hai. Các thay đổi đối với một hàng sẽ hiển thị trong tất cả các hàng, điều này gần như chắc chắn không phải là những gì bạn muốn.