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ờ
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
[x]*3
Về 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, x
nó 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]*4
mỗ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] * 4
vă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] * 4
mọi lần vì cùng một lý do [x**2 for x in range(3)]
đánh giá lại x**2
mọi lần. Mỗi lần đánh giá [1] * 4
sẽ 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] * 4
cũ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 = 2
và biến 1 thành 2.
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
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 lst
là 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.
[[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.
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.ones
và np.zeros
và / 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])
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 b
là danh sách chứa một mục là tham chiếu đến danh sách a
. Danh sách a
có 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 c
bây giờ chứa hai tham chiếu đến danh sách a
tươ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?
myList = [[1]*4] * 3
tạ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 obj
sẽ đượ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 là *
toán tử chủ yếu được sử dụng để tạo danh sách các ký tự . Mặc dù 1
là bất biến, obj =[1]*4
vẫn sẽ tạo ra một danh sách 1
lặ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ì obj
sẽ [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
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ó x
cái đó 1
và một danh sách phần tử duy nhất y
có chứa x
. Bước đầu tiên của bạn là y * 4
sẽ 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]] * 3
và 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.
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
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]]
@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
*3
cũ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 đổili[0]
, bạn không thể thay đổi bất kỳ tham chiếu cơ bản nào của const int0
, 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]
vàli
có thể thay đổi, vì vậy khi bạn gọima[0][0]=1
, ma [0] [0] là như nhau&li[0]
, vì vậy tất cả các&li
phiên bản sẽ thay đổi địa chỉ đầu tiên của nó thành&1
.
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] * 2
thự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]]
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
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] = 5
tạ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
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.0
là 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
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.