Danh sách của Python được triển khai như thế nào?


182

Có phải là một danh sách liên kết, một mảng? Tôi tìm kiếm xung quanh và chỉ thấy mọi người đoán. Kiến thức C của tôi không đủ tốt để xem mã nguồn.

Câu trả lời:


57

Đó là một mảng động . Bằng chứng thực tế: Việc lập chỉ mục mất (tất nhiên là có sự khác biệt cực kỳ nhỏ (0,0013 Cách sử dụng!)) Cùng một lúc bất kể chỉ số:

...>python -m timeit --setup="x = [None]*1000" "x[500]"
10000000 loops, best of 3: 0.0579 usec per loop

...>python -m timeit --setup="x = [None]*1000" "x[0]"
10000000 loops, best of 3: 0.0566 usec per loop

Tôi sẽ ngạc nhiên nếu IronPython hoặc Jython sử dụng danh sách được liên kết - chúng sẽ phá hỏng hiệu suất của nhiều thư viện được sử dụng rộng rãi được xây dựng dựa trên giả định rằng danh sách là mảng động.


1
@Ralf: Tôi biết CPU của tôi (hầu hết các phần cứng khác, đối với vấn đề đó) đã cũ và chó chậm - về mặt sáng sủa, tôi có thể cho rằng mã chạy đủ nhanh đối với tôi là đủ nhanh cho tất cả người dùng: D

88
@delnan: -1 "bằng chứng thực tế" của bạn là vô nghĩa, cũng như 6 upvote. Khoảng 98% thời gian được thực hiện x=[None]*1000, để lại sự đo lường của bất kỳ sự khác biệt truy cập danh sách có thể thay vì không chính xác. Bạn cần tách riêng phần khởi tạo:-s "x=[None]*100" "x[0]"
John Machin

26
Cho thấy rằng đó không phải là một triển khai ngây thơ của một danh sách liên kết. Không dứt khoát cho thấy rằng đó là một mảng.
Michael Mior

6
Bạn có thể đọc về nó ở đây: docs.python.org/2/faq/design.html#how-are-lists-implemented
CCoder

3
Có nhiều cấu trúc hơn nhiều so với chỉ danh sách và mảng được liên kết, thời gian không được sử dụng thực tế để quyết định giữa chúng.
Ross Hemsley

236

Mã C khá đơn giản, thực sự. Mở rộng một macro và cắt tỉa một số ý kiến ​​không liên quan, cấu trúc cơ bản nằm trong listobject.hđó xác định danh sách là:

typedef struct {
    PyObject_HEAD
    Py_ssize_t ob_size;

    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     */
    Py_ssize_t allocated;
} PyListObject;

PyObject_HEADchứa số tham chiếu và định danh loại. Vì vậy, nó là một vectơ / mảng tổng thể. Mã để thay đổi kích thước một mảng như vậy khi nó đầy listobject.c. Nó không thực sự tăng gấp đôi mảng, nhưng phát triển bằng cách phân bổ

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

đến công suất mỗi lần, newsizekích thước được yêu cầu ở đâu (không nhất thiết allocated + 1bởi vì bạn có thể extendbằng một số phần tử tùy ý thay vìappend 'từng cái một).

Xem thêm Câu hỏi thường gặp về Python .


6
Vì vậy, khi lặp qua danh sách python, nó chậm như danh sách được liên kết, bởi vì mọi mục nhập chỉ là một con trỏ, vì vậy mọi phần tử rất có thể sẽ gây ra lỗi bộ nhớ cache.
Kr0e

9
@ Kr0e: không phải nếu các phần tử tiếp theo thực sự là cùng một đối tượng :) Nhưng nếu bạn cần cấu trúc dữ liệu nhỏ hơn / thân thiện hơn với bộ đệm, arraymô-đun hoặc NumPy sẽ được ưu tiên.
Fred Foo

@ Kr0e Tôi sẽ không nói lặp đi lặp lại trong danh sách chậm như danh sách được liên kết, nhưng việc lặp lại các giá trị của danh sách được liên kết là chậm như một danh sách được liên kết, với lời cảnh báo mà Fred đã đề cập. Ví dụ, lặp lại một danh sách để sao chép nó sang một danh sách khác nên nhanh hơn một danh sách được liên kết.
Ganea Dan Andrei

35

Trong CPython, danh sách là mảng con trỏ. Các triển khai khác của Python có thể chọn lưu trữ chúng theo các cách khác nhau.


32

Điều này phụ thuộc vào việc triển khai, nhưng IIRC:

  • CPython sử dụng một loạt các con trỏ
  • Jython sử dụng một ArrayList
  • IronPython rõ ràng cũng sử dụng một mảng. Bạn có thể duyệt mã nguồn để tìm hiểu.

Do đó, tất cả họ đều có quyền truy cập ngẫu nhiên O (1).


1
Việc thực hiện phụ thuộc như trong một trình thông dịch python đã thực hiện các danh sách như các danh sách được liên kết sẽ là một triển khai hợp lệ của ngôn ngữ python? Nói cách khác: O (1) truy cập ngẫu nhiên vào danh sách không được đảm bảo? Không phải điều đó không thể viết mã hiệu quả mà không dựa vào chi tiết triển khai sao?
sepp2k

2
@sepp Tôi tin rằng các danh sách trong Python chỉ là các bộ sưu tập được đặt hàng; các yêu cầu thực hiện và / hoặc hiệu suất của việc thực hiện nói trên không được nêu rõ ràng
NullUserException

6
@ sppe2k: Vì Python không thực sự có thông số kỹ thuật tiêu chuẩn hoặc chính thức (mặc dù có một số tài liệu có nội dung "... được đảm bảo cho ..."), nên bạn không thể chắc chắn 100% như trong "điều này được đảm bảo bởi một số mảnh giấy ". Nhưng vì O(1)lập chỉ mục danh sách là một giả định khá phổ biến và hợp lệ, nên không có triển khai nào dám phá vỡ nó.

@Paul Nó không nói gì về cách thực hiện cơ bản của danh sách nên được thực hiện.
NullUserException

Nó chỉ không xảy ra để xác định thời gian chạy O lớn của mọi thứ. Đặc tả cú pháp ngôn ngữ không nhất thiết có nghĩa giống như chi tiết triển khai, nó thường xảy ra.
Paul McMillan

26

Tôi muốn đề xuất bài viết "Thực hiện danh sách Python" của Laurent Luce . Nó thực sự hữu ích cho tôi vì tác giả giải thích cách danh sách được triển khai trong CPython và sử dụng các sơ đồ tuyệt vời cho mục đích này.

Liệt kê cấu trúc đối tượng C

Một đối tượng danh sách trong CPython được biểu diễn bằng cấu trúc C sau. ob_itemlà một danh sách các con trỏ đến các thành phần danh sách. phân bổ là số lượng khe được phân bổ trong bộ nhớ.

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;

Điều quan trọng là nhận thấy sự khác biệt giữa các vị trí được phân bổ và kích thước của danh sách. Kích thước của một danh sách là giống như len(l). Số lượng vị trí được phân bổ là những gì đã được phân bổ trong bộ nhớ. Thông thường, bạn sẽ thấy rằng phân bổ có thể lớn hơn kích thước. Điều này là để tránh cần gọi reallocmỗi khi một yếu tố mới được thêm vào danh sách.

...

Nối

Chúng tôi nối một số nguyên vào danh sách : l.append(1). Chuyện gì xảy ra
nhập mô tả hình ảnh ở đây

Chúng tôi tiếp tục bằng cách thêm một yếu tố nữa : l.append(2). list_resizeđược gọi với n + 1 = 2 nhưng vì kích thước được phân bổ là 4 nên không cần phân bổ thêm bộ nhớ. Điều tương tự cũng xảy ra khi chúng ta bổ sung thêm 2 số nguyên: l.append(3), l.append(4). Sơ đồ sau đây cho thấy những gì chúng ta có cho đến nay.

nhập mô tả hình ảnh ở đây

...

Chèn

Hãy chèn một số nguyên mới (5) vào vị trí 1: l.insert(1,5)và xem xét những gì xảy ra trong nội bộ.nhập mô tả hình ảnh ở đây

...

Nhạc pop

Khi bạn bật phần tử cuối cùng : l.pop(), listpop()được gọi. list_resizeđược gọi bên trong listpop()và nếu kích thước mới nhỏ hơn một nửa kích thước được phân bổ thì danh sách bị thu hẹp.nhập mô tả hình ảnh ở đây

Bạn có thể quan sát rằng khe 4 vẫn trỏ đến số nguyên nhưng điều quan trọng là kích thước của danh sách hiện là 4. Hãy bật thêm một yếu tố. Tronglist_resize() , kích thước - 1 = 4 - 1 = 3 nhỏ hơn một nửa số vị trí được phân bổ để danh sách được thu nhỏ thành 6 vị trí và kích thước mới của danh sách hiện là 3.

Bạn có thể quan sát rằng khe 3 và 4 vẫn trỏ đến một số số nguyên nhưng điều quan trọng là kích thước của danh sách hiện là 3.nhập mô tả hình ảnh ở đây

...

Xóa đối tượng danh sách Python có một phương thức để loại bỏ một phần tử cụ thể : l.remove(5).nhập mô tả hình ảnh ở đây


Cảm ơn, tôi hiểu phần liên kết của danh sách nhiều hơn bây giờ. Danh sách Python là một aggregation, không composition. Tôi ước có một danh sách các thành phần quá.
shuva

22

Theo tài liệu ,

Danh sách của Python là các mảng thực sự có độ dài thay đổi, không phải là danh sách được liên kết theo kiểu Lisp.


5

Như những người khác đã nêu ở trên, các danh sách (khi đáng kể lớn) được thực hiện bằng cách phân bổ một lượng không gian cố định và, nếu không gian đó sẽ lấp đầy, hãy phân bổ một lượng không gian lớn hơn và sao chép qua các phần tử.

Để hiểu lý do tại sao phương thức được khấu hao O (1), không mất tính tổng quát, giả sử chúng ta đã chèn các phần tử a = 2 ^ n và bây giờ chúng ta phải tăng gấp đôi bảng của mình lên kích thước 2 ^ (n + 1). Điều đó có nghĩa là chúng tôi hiện đang thực hiện các hoạt động 2 ^ (n + 1). Bản sao cuối cùng, chúng tôi đã thực hiện 2 ^ n hoạt động. Trước đó, chúng tôi đã làm 2 ^ (n-1) ... tất cả đều giảm xuống còn 8.4,2,1. Bây giờ, nếu chúng ta thêm những thứ này lên, chúng ta sẽ nhận được 1 + 2 + 4 + 8 + ... + 2 ^ (n + 1) = 2 ^ (n + 2) - 1 <4 * 2 ^ n = O (2 ^ n) = O (a) tổng số lần chèn (tức là thời gian khấu hao O (1)). Ngoài ra, cần lưu ý rằng nếu bảng cho phép xóa, việc thu hẹp bảng phải được thực hiện ở một yếu tố khác (ví dụ 3x)


Theo tôi hiểu, không có bản sao của các yếu tố cũ. Nhiều không gian hơn được phân bổ, nhưng không gian mới không tiếp giáp với không gian đã được sử dụng và chỉ các phần tử mới hơn được chèn vào được sao chép vào không gian mới. Xin hãy sửa tôi nếu tôi sai.
Tushar Vazirani

1

Một danh sách trong Python là một cái gì đó giống như một mảng, nơi bạn có thể lưu trữ nhiều giá trị. Danh sách có thể thay đổi có nghĩa là bạn có thể thay đổi nó. Điều quan trọng hơn bạn nên biết, khi chúng tôi tạo một danh sách, Python sẽ tự động tạo một Reference_id cho biến danh sách đó. Nếu bạn thay đổi nó bằng cách gán biến khác, danh sách chính sẽ thay đổi. Hãy thử với một ví dụ:

list_one = [1,2,3,4]

my_list = list_one

#my_list: [1,2,3,4]

my_list.append("new")

#my_list: [1,2,3,4,'new']
#list_one: [1,2,3,4,'new']

Chúng tôi chắp thêm my_listnhưng danh sách chính của chúng tôi đã thay đổi. Danh sách đó có nghĩa là không được gán làm danh sách sao chép gán làm tham chiếu của nó.


0

Trong danh sách CPython được triển khai dưới dạng mảng động và do đó, khi chúng tôi nối vào thời điểm đó, không chỉ có một macro được thêm vào mà còn phân bổ thêm không gian để không cần thêm không gian mới.

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.