Điều gì gây ra [* a] để tổng thể?


136

Rõ ràng list(a)không phải là tổng thể, [x for x in a]tổng thể tại một số điểm, và [*a]tổng thể mọi lúc ?

Kích thước lên tới n = 100

Dưới đây là kích thước n từ 0 đến 12 và kích thước kết quả tính bằng byte cho ba phương thức:

0 56 56 56
1 64 88 88
2 72 88 96
3 80 88 104
4 88 88 112
5 96 120 120
6 104 120 128
7 112 120 136
8 120 120 152
9 128 184 184
10 136 184 192
11 144 184 200
12 152 184 208

Được tính toán như thế này, có thể tái tạo tại repl.it , sử dụng Python 3. 8 :

from sys import getsizeof

for n in range(13):
    a = [None] * n
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]))

Vì vậy, làm thế nào điều này làm việc? Làm thế nào để [*a]tổng thể? Trên thực tế, nó sử dụng cơ chế nào để tạo danh sách kết quả từ đầu vào đã cho? Nó sử dụng một iterator hơn avà sử dụng một cái gì đó như thế list.appendnào? Mã nguồn ở đâu?

( Colab với dữ liệu và mã tạo ra hình ảnh.)

Phóng to đến n nhỏ hơn:

Kích thước lên tới n = 40

Thu nhỏ đến n lớn hơn:

Kích thước lên tới n = 1000


1
Fwiw, mở rộng các trường hợp thử nghiệm của bạn, có vẻ như việc hiểu danh sách hoạt động như viết một vòng lặp và nối thêm từng mục vào danh sách, trong khi [*a]dường như hoạt động như sử dụng extendtrong danh sách trống.
ngày

4
Nó có thể giúp xem xét mã byte được tạo cho mỗi. list(a)hoạt động hoàn toàn trong C; nó có thể phân bổ nút đệm nội bộ theo nút khi nó lặp lại a. [x for x in a]chỉ sử dụng LIST_APPENDrất nhiều, vì vậy nó tuân theo mô hình "tổng thể một chút, phân bổ lại khi cần thiết" của một danh sách bình thường. [*a]sử dụng BUILD_LIST_UNPACK, mà ... tôi không biết nó làm gì, ngoài việc phân bổ quá mức mọi lúc :)
chepner

2
Ngoài ra, trong Python 3.7, nó xuất hiện list(a)[*a]giống hệt nhau và cả hai tổng thể so với [x for x in a], vì vậy ... sys.getsizeofcó thể không phải là công cụ phù hợp để sử dụng ở đây.
chepner

7
@chepner Tôi nghĩ sys.getsizeoflà công cụ phù hợp, nó chỉ hiển thị list(a)được sử dụng để tổng thể. Trên thực tế Có gì mới trong Python 3.8 đề cập đến nó: "Trình xây dựng danh sách không tổng thể [...]" .
Stefan Pochmann

5
@chepner: Đó là một lỗi đã được sửa trong 3,8 ; các nhà xây dựng không phải là tổng thể.
ShadowRanger

Câu trả lời:


81

[*a] là nội bộ làm tương đương C của :

  1. Làm mới, trống list
  2. Gọi newlist.extend(a)
  3. Trả về list.

Vì vậy, nếu bạn mở rộng bài kiểm tra của mình sang:

from sys import getsizeof

for n in range(13):
    a = [None] * n
    l = []
    l.extend(a)
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]),
             getsizeof(l))

Hãy thử trực tuyến!

bạn sẽ thấy kết quả cho getsizeof([*a])l = []; l.extend(a); getsizeof(l)giống nhau.

Đây thường là điều đúng đắn để làm; khi extendbạn thường mong đợi để bổ sung thêm sau này và tương tự cho việc giải nén tổng quát, nó giả định rằng nhiều thứ sẽ được thêm lần lượt. [*a]không phải là trường hợp bình thường; Python giả định có nhiều mục hoặc iterables được thêm vào list( [*a, b, c, *d]), do đó, tổng thể tiết kiệm công việc trong trường hợp phổ biến.

Ngược lại, một listcấu trúc từ một lần lặp, được quy định trước (với list()) có thể không phát triển hoặc co lại trong quá trình sử dụng, và tổng thể là sớm cho đến khi được chứng minh khác đi; Python gần đây đã sửa một lỗi khiến tổng thể hàm tạo ngay cả đối với các đầu vào có kích thước đã biết .

Về phần listhiểu, chúng thực sự tương đương với các lần lặp lại append, vì vậy bạn đang thấy kết quả cuối cùng của mô hình tăng trưởng tổng thể bình thường khi thêm một yếu tố tại một thời điểm.

Để rõ ràng, không ai trong số này là một đảm bảo ngôn ngữ. Đó chỉ là cách CPython thực hiện nó. Thông số ngôn ngữ Python nói chung không liên quan đến các mô hình tăng trưởng cụ thể trong list(ngoài việc đảm bảo các khoản khấu hao O(1) appendpops từ cuối). Như đã lưu ý trong các ý kiến, việc thực hiện cụ thể thay đổi một lần nữa trong 3.9; mặc dù nó sẽ không ảnh hưởng [*a], nhưng nó có thể ảnh hưởng đến các trường hợp khác, nơi từng là "xây dựng tạm thời tuplecác mục riêng lẻ và sau đó extendvới tuple" bây giờ trở thành nhiều ứng dụng LIST_APPEND, có thể thay đổi khi tổng thể xảy ra và số nào đi vào tính toán.


4
@StefanPochmann: Tôi đã đọc mã trước đó (đó là lý do tại sao tôi biết điều này rồi). Đây là trình xử lý mã byte choBUILD_LIST_UNPACK , nó sử dụng _PyList_Extendnhư C tương đương với cuộc gọi extend(chỉ trực tiếp, chứ không phải bằng cách tra cứu phương thức). Họ đã kết hợp nó với các đường dẫn để xây dựng một tuplegiải nén; tupleKhông tổng thể độc đáo cho việc xây dựng từng phần, vì vậy họ luôn giải nén thành một list(để hưởng lợi từ tổng thể) và chuyển đổi sang tuplecuối khi đó là những gì được yêu cầu.
ShadowRanger

4
Lưu ý rằng điều này dường như thay đổi trong 3.9 , nơi xây dựng được thực hiện với bytecode riêng biệt ( BUILD_LIST, LIST_EXTENDđối với từng điều cần giải nén, LIST_APPENDcho các hạng mục duy nhất), thay vì tải tất cả mọi thứ trên stack trước khi xây dựng toàn bộ listbằng một lệnh mã byte duy nhất (nó cho phép biên dịch để thực hiện tối ưu mà các hướng dẫn tất cả-trong-một đã không cho phép, như thực hiện [*a, b, *c]như LIST_EXTEND, LIST_APPEND, LIST_EXTENDw / o cần phải quấn btrong một one- tupleđể đáp ứng các yêu cầu của BUILD_LIST_UNPACK).
ShadowRanger

18

Bức tranh đầy đủ về những gì xảy ra, dựa trên các câu trả lời và nhận xét khác (đặc biệt là câu trả lời của ShadowRanger , cũng giải thích lý do tại sao nó được thực hiện như vậy).

Chương trình tháo gỡ BUILD_LIST_UNPACKđược sử dụng:

>>> import dis
>>> dis.dis('[*a]')
  1           0 LOAD_NAME                0 (a)
              2 BUILD_LIST_UNPACK        1
              4 RETURN_VALUE

Điều đó được xử lý trongceval.c đó xây dựng một danh sách trống và mở rộng nó (với a):

        case TARGET(BUILD_LIST_UNPACK): {
            ...
            PyObject *sum = PyList_New(0);
              ...
                none_val = _PyList_Extend((PyListObject *)sum, PEEK(i));

_PyList_Extend sử dụng list_extend :

_PyList_Extend(PyListObject *self, PyObject *iterable)
{
    return list_extend(self, iterable);
}

Cuộc gọilist_resize nào có tổng kích thước :

list_extend(PyListObject *self, PyObject *iterable)
    ...
        n = PySequence_Fast_GET_SIZE(iterable);
        ...
        m = Py_SIZE(self);
        ...
        if (list_resize(self, m + n) < 0) {

tổng thể như sau:

list_resize(PyListObject *self, Py_ssize_t newsize)
{
  ...
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Hãy kiểm tra xem. Tính số lượng điểm dự kiến ​​với công thức ở trên và tính kích thước byte dự kiến ​​bằng cách nhân nó với 8 (như tôi đang sử dụng Python 64 bit ở đây) và thêm kích thước byte của danh sách trống (nghĩa là chi phí không đổi của đối tượng danh sách) :

from sys import getsizeof
for n in range(13):
    a = [None] * n
    expected_spots = n + (n >> 3) + (3 if n < 9 else 6)
    expected_bytesize = getsizeof([]) + expected_spots * 8
    real_bytesize = getsizeof([*a])
    print(n,
          expected_bytesize,
          real_bytesize,
          real_bytesize == expected_bytesize)

Đầu ra:

0 80 56 False
1 88 88 True
2 96 96 True
3 104 104 True
4 112 112 True
5 120 120 True
6 128 128 True
7 136 136 True
8 152 152 True
9 184 184 True
10 192 192 True
11 200 200 True
12 208 208 True

Các trận đấu ngoại trừ n = 0, list_extendthực sự là các phím tắt , vì vậy thực sự cũng khớp:

        if (n == 0) {
            ...
            Py_RETURN_NONE;
        }
        ...
        if (list_resize(self, m + n) < 0) {

8

Đây sẽ là chi tiết triển khai của trình thông dịch CPython và do đó có thể không nhất quán giữa các trình thông dịch khác.

Điều đó nói rằng, bạn có thể thấy nơi hiểu và list(a)hành vi đến đây:

https://github.com/python/cpython/blob/master/Objects/listobject.c#L36

Cụ thể cho việc hiểu:

 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
...

new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Ngay bên dưới những dòng đó, có list_preallocate_exactsử dụng khi gọi list(a).


1
[*a]không nối từng phần tử một. Nó có mã byte chuyên dụng riêng, có chức năng chèn số lượng lớn qua extend.
ShadowRanger

Gotcha - Tôi đoán tôi đã không đào đủ về điều đó. Đã xóa phần trên[*a]
Randy
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.