Câu trả lời:
Dưới đây là tất cả mọi thứ về các câu lệnh Python mà tôi có thể kết hợp lại với nhau (có lẽ nhiều hơn bất kỳ ai muốn biết; nhưng câu trả lời là toàn diện).
dict
sử dụng địa chỉ mở để giải quyết các va chạm băm (giải thích bên dưới) (xem dictobject.c: 296-297 ).O(1)
tra cứu theo chỉ mục).Hình dưới đây là một biểu diễn logic của bảng băm Python. Trong hình bên dưới, 0, 1, ..., i, ...
bên trái là các chỉ số của các vị trí trong bảng băm (chúng chỉ nhằm mục đích minh họa và rõ ràng không được lưu trữ cùng với bảng!).
# Logical model of Python Hash table
-+-----------------+
0| <hash|key|value>|
-+-----------------+
1| ... |
-+-----------------+
.| ... |
-+-----------------+
i| ... |
-+-----------------+
.| ... |
-+-----------------+
n| ... |
-+-----------------+
Khi một lệnh mới được khởi tạo, nó bắt đầu với 8 vị trí . (xem dictobject.h: 49 )
i
dựa trên hàm băm của khóa. CPython ban đầu sử dụng i = hash(key) & mask
(ở đâu mask = PyDictMINSIZE - 1
, nhưng điều đó không thực sự quan trọng). Chỉ cần lưu ý rằng vị trí ban đầu, i
được kiểm tra phụ thuộc vào hàm băm của khóa.<hash|key|value>
). Nhưng nếu khe đó bị chiếm thì sao!? Rất có thể vì một mục khác có cùng hàm băm (va chạm băm!)==
so sánh không phải là is
so sánh) của mục nhập trong vị trí so với hàm băm và khóa của mục nhập hiện tại sẽ được chèn ( dictobject.c : 337.344-345 ) tương ứng. Nếu cả hai khớp nhau, thì nó nghĩ rằng mục nhập đã tồn tại, từ bỏ và chuyển sang mục tiếp theo sẽ được chèn. Nếu hàm băm hoặc khóa không khớp, nó sẽ bắt đầu thăm dò .i+1, i+2, ...
và sử dụng cái đầu tiên có sẵn (đó là thăm dò tuyến tính). Nhưng vì những lý do được giải thích rất hay trong các bình luận (xem dictobject.c: 33-126 ), CPython sử dụng phương pháp thăm dò ngẫu nhiên . Trong thăm dò ngẫu nhiên, vị trí tiếp theo được chọn theo thứ tự giả ngẫu nhiên. Các mục được thêm vào khe trống đầu tiên. Đối với cuộc thảo luận này, thuật toán thực tế được sử dụng để chọn vị trí tiếp theo không thực sự quan trọng (xem dictobject.c: 33-126 để biết thuật toán thăm dò). Điều quan trọng là các vị trí được thăm dò cho đến khi tìm thấy vị trí trống đầu tiên.dict
sẽ được thay đổi kích thước nếu nó đầy hai phần ba. Điều này tránh làm chậm việc tra cứu. (xem dictobject.h: 64-65 )LƯU Ý: Tôi đã thực hiện nghiên cứu về triển khai Python Dict để trả lời câu hỏi của riêng tôi về cách nhiều mục trong một dict có thể có cùng giá trị băm. Tôi đã đăng một phiên bản chỉnh sửa một chút của câu trả lời ở đây vì tất cả các nghiên cứu đều rất phù hợp với câu hỏi này.
Các từ điển tích hợp của Python được triển khai như thế nào?
Đây là khóa học ngắn hạn:
Khía cạnh được đặt hàng không chính thức như Python 3.6 (để cho các triển khai khác có cơ hội theo kịp), nhưng chính thức trong Python 3.7 .
Trong một thời gian dài, nó hoạt động chính xác như thế này. Python sẽ phân bổ 8 hàng trống và sử dụng hàm băm để xác định vị trí gắn cặp khóa-giá trị. Ví dụ: nếu hàm băm cho khóa kết thúc vào năm 001, nó sẽ gắn nó vào chỉ mục 1 (tức là thứ 2) (như ví dụ dưới đây.)
<hash> <key> <value>
null null null
...010001 ffeb678c 633241c4 # addresses of the keys and values
null null null
... ... ...
Mỗi hàng chiếm 24 byte trên kiến trúc 64 bit, 12 trên 32 bit. (Lưu ý rằng các tiêu đề cột chỉ là nhãn cho mục đích của chúng tôi ở đây - chúng không thực sự tồn tại trong bộ nhớ.)
Nếu hàm băm kết thúc giống như hàm băm của khóa có sẵn, đây là một xung đột và sau đó nó sẽ gắn cặp khóa-giá trị ở một vị trí khác.
Sau khi 5 giá trị khóa được lưu trữ, khi thêm một cặp khóa-giá trị khác, xác suất va chạm băm là quá lớn, do đó từ điển có kích thước gấp đôi. Trong quy trình 64 bit, trước khi thay đổi kích thước, chúng ta có 72 byte trống và sau đó, chúng ta đang lãng phí 240 byte do 10 hàng trống.
Điều này tốn rất nhiều không gian, nhưng thời gian tra cứu khá không đổi. Thuật toán so sánh khóa là tính toán hàm băm, đi đến vị trí dự kiến, so sánh id của khóa - nếu chúng là cùng một đối tượng, chúng bằng nhau. Nếu không thì so sánh các giá trị băm, nếu chúng không giống nhau, chúng không bằng nhau. Khác, cuối cùng chúng ta so sánh các khóa cho đẳng thức, và nếu chúng bằng nhau, trả về giá trị. So sánh cuối cùng cho sự bình đẳng có thể khá chậm, nhưng các kiểm tra trước đó thường rút ngắn so sánh cuối cùng, làm cho việc tra cứu rất nhanh.
Các va chạm làm chậm mọi thứ và về mặt lý thuyết, kẻ tấn công có thể sử dụng các va chạm băm để thực hiện tấn công từ chối dịch vụ, vì vậy chúng tôi đã ngẫu nhiên khởi tạo hàm băm sao cho nó tính toán các giá trị băm khác nhau cho mỗi quy trình Python mới.
Không gian lãng phí được mô tả ở trên đã khiến chúng tôi sửa đổi việc thực hiện từ điển, với một tính năng mới thú vị mà từ điển hiện được đặt hàng bằng cách chèn.
Thay vào đó, chúng tôi bắt đầu bằng cách sắp xếp một mảng cho chỉ mục của phần chèn.
Vì cặp khóa-giá trị đầu tiên của chúng tôi đi vào vị trí thứ hai, chúng tôi lập chỉ mục như sau:
[null, 0, null, null, null, null, null, null]
Và bảng của chúng tôi chỉ được điền theo thứ tự chèn:
<hash> <key> <value>
...010001 ffeb678c 633241c4
... ... ...
Vì vậy, khi chúng tôi tìm kiếm một khóa, chúng tôi sử dụng hàm băm để kiểm tra vị trí mà chúng tôi mong đợi (trong trường hợp này, chúng tôi đi thẳng đến chỉ mục 1 của mảng), sau đó đi đến chỉ mục đó trong bảng băm (ví dụ: chỉ mục 0 ), kiểm tra xem các khóa có bằng nhau không (sử dụng cùng một thuật toán được mô tả trước đó) và nếu vậy, trả về giá trị.
Chúng tôi duy trì thời gian tra cứu liên tục, với một số tổn thất tốc độ nhỏ trong một số trường hợp và tăng trong những trường hợp khác, với những mặt tích cực mà chúng tôi tiết kiệm được khá nhiều không gian so với việc triển khai trước đó và chúng tôi giữ lại thứ tự chèn. Không gian bị lãng phí duy nhất là các byte null trong mảng chỉ mục.
Raymond Hettinger đã giới thiệu điều này trên python-dev vào tháng 12 năm 2012. Cuối cùng, nó đã vào CPython trong Python 3.6 . Đặt hàng bằng cách chèn được coi là một chi tiết triển khai cho 3.6 để cho phép các triển khai khác của Python có cơ hội bắt kịp.
Một tối ưu hóa khác để tiết kiệm không gian là một triển khai chia sẻ các khóa. Do đó, thay vì có các từ điển dư thừa chiếm hết không gian đó, chúng tôi có các từ điển sử dụng lại các khóa băm và khóa chung. Bạn có thể nghĩ về nó như thế này:
hash key dict_0 dict_1 dict_2...
...010001 ffeb678c 633241c4 fffad420 ...
... ... ... ... ...
Đối với máy 64 bit, điều này có thể tiết kiệm tới 16 byte cho mỗi khóa cho mỗi từ điển phụ.
Các mã khóa chia sẻ này được dự định sẽ được sử dụng cho các đối tượng tùy chỉnh ' __dict__
. Để có được hành vi này, tôi tin rằng bạn cần hoàn thành việc điền vào __dict__
trước khi bạn khởi tạo đối tượng tiếp theo ( xem PEP 412 ). Điều này có nghĩa là bạn nên gán tất cả các thuộc tính của mình trong __init__
hoặc __new__
, nếu không bạn có thể không nhận được khoản tiết kiệm không gian của mình.
Tuy nhiên, nếu bạn biết tất cả các thuộc tính của mình tại thời điểm bạn __init__
được thực thi, bạn cũng có thể cung cấp __slots__
cho đối tượng của mình và đảm bảo rằng __dict__
nó hoàn toàn không được tạo (nếu không có sẵn ở cha mẹ) hoặc thậm chí cho phép __dict__
nhưng đảm bảo rằng các thuộc tính dự kiến của bạn là lưu trữ trong các khe dù sao. Để biết thêm về __slots__
, xem câu trả lời của tôi ở đây .
**kwargs
trong một hàm.find_empty_slot
: github.com/python/cpython/blob/master/Objects/dictobject.c # L969 - và bắt đầu trên dòng 134 có một số văn xuôi mô tả nó.
Từ điển Python sử dụng địa chỉ mở ( tham chiếu bên trong Mã đẹp )
Lưu ý! Địa chỉ mở , hay còn gọi là băm đóng , như đã lưu ý trong Wikipedia, không nên nhầm lẫn với băm mở đối diện của nó !
Địa chỉ mở có nghĩa là dict sử dụng các vị trí mảng và khi vị trí chính của đối tượng được lấy trong dict, vị trí của đối tượng được tìm kiếm ở một chỉ mục khác trong cùng một mảng, sử dụng sơ đồ "nhiễu loạn", trong đó giá trị băm của đối tượng đóng vai trò .