Tại sao thứ tự trong từ điển và đặt tùy ý?


151

Tôi không hiểu cách lặp qua từ điển hoặc đặt trong python được thực hiện theo thứ tự 'tùy ý'.

Ý tôi là, đó là ngôn ngữ lập trình nên mọi thứ trong ngôn ngữ phải được xác định 100%, đúng không? Python phải có một số loại thuật toán quyết định phần nào của từ điển hoặc bộ được chọn, thứ 1, thứ hai, v.v.

Tôi đang thiếu gì?


1
Bản dựng PyPy mới nhất (2.5, cho Python 2.7) làm cho các từ điển được đặt hàng theo mặc định .
Veedrac 6/2/2015

Câu trả lời:


236

Lưu ý: Câu trả lời này được viết trước khi triển khai dictloại thay đổi, trong Python 3.6. Hầu hết các chi tiết triển khai trong câu trả lời này vẫn được áp dụng, nhưng thứ tự liệt kê các khóa trong từ điển không còn được xác định bởi các giá trị băm. Việc thực hiện thiết lập vẫn không thay đổi.

Thứ tự không phải là tùy ý, mà phụ thuộc vào lịch sử chèn và xóa của từ điển hoặc bộ, cũng như vào việc triển khai Python cụ thể. Trong phần còn lại của câu trả lời này, đối với 'từ điển', bạn cũng có thể đọc 'bộ'; các bộ được thực hiện dưới dạng từ điển chỉ với các khóa và không có giá trị.

Các khóa được băm và các giá trị băm được gán cho các vị trí trong bảng động (nó có thể tăng hoặc thu hẹp dựa trên nhu cầu). Và quá trình ánh xạ đó có thể dẫn đến va chạm, có nghĩa là một khóa sẽ phải được đặt vào một khe tiếp theo dựa trên những gì đã có.

Liệt kê các vòng lặp nội dung trên các vị trí và do đó các khóa được liệt kê theo thứ tự chúng hiện đang nằm trong bảng.

Lấy các phím 'foo''bar', ví dụ, và giả sử kích thước bảng là 8 vị trí. Trong Python 2.7, hash('foo')-4177197833195190597, hash('bar')327024216814240868. Modulo 8, có nghĩa là hai phím này được đặt trong các khe 3 và 4 sau đó:

>>> hash('foo')
-4177197833195190597
>>> hash('foo') % 8
3
>>> hash('bar')
327024216814240868
>>> hash('bar') % 8
4

Điều này thông báo thứ tự niêm yết của họ:

>>> {'bar': None, 'foo': None}
{'foo': None, 'bar': None}

Tất cả các vị trí ngoại trừ 3 và 4 đều trống, lặp qua bảng trước tiên liệt kê vị trí 3, sau đó là vị trí 4, do đó 'foo'được liệt kê trước 'bar'.

barbaz, tuy nhiên, có các giá trị băm cách nhau chính xác 8 và do đó ánh xạ tới cùng một vị trí , 4:

>>> hash('bar')
327024216814240868
>>> hash('baz')
327024216814240876
>>> hash('bar') % 8
4
>>> hash('baz') % 8
4

Thứ tự của họ bây giờ phụ thuộc vào khóa nào được đặt trước; khóa thứ hai sẽ phải được chuyển sang vị trí tiếp theo:

>>> {'baz': None, 'bar': None}
{'bar': None, 'baz': None}
>>> {'bar': None, 'baz': None}
{'baz': None, 'bar': None}

Thứ tự bảng khác nhau ở đây, bởi vì một hoặc khóa khác được đặt trước.

Tên kỹ thuật cho cấu trúc cơ bản được sử dụng bởi CPython (công cụ Python được sử dụng phổ biến nhất) là một bảng băm , một bảng sử dụng địa chỉ mở. Nếu bạn tò mò và hiểu rõ về C, hãy xem triển khai C để biết tất cả các chi tiết (được ghi chép đầy đủ). Bạn cũng có thể xem bài thuyết trình Pycon 2010 này của Brandon Rhodes về cách CPython dicthoạt động hoặc chọn một bản sao của Mã đẹp , bao gồm một chương về việc triển khai được viết bởi Andrew Kuchling.

Lưu ý rằng kể từ Python 3.3, một hạt băm ngẫu nhiên cũng được sử dụng, làm cho các xung đột băm không thể đoán trước để ngăn chặn một số loại từ chối dịch vụ (trong đó kẻ tấn công làm cho máy chủ Python không phản hồi bằng cách gây ra va chạm băm lớn). Điều này có nghĩa là thứ tự của một từ điển hoặc bộ đã cho cũng phụ thuộc vào hạt băm ngẫu nhiên cho lệnh gọi Python hiện tại.

Các triển khai khác có thể tự do sử dụng một cấu trúc khác cho từ điển, miễn là chúng thỏa mãn giao diện Python được ghi lại cho chúng, nhưng tôi tin rằng tất cả các triển khai đều sử dụng một biến thể của bảng băm.

CPython 3.6 giới thiệu một triển khai mới dict duy trì thứ tự chèn và khởi động nhanh hơn và hiệu quả hơn về bộ nhớ. Thay vì giữ một bảng thưa thớt lớn trong đó mỗi hàng tham chiếu giá trị băm được lưu trữ và các đối tượng khóa và giá trị, triển khai mới thêm một mảng băm nhỏ hơn chỉ tham chiếu các chỉ mục trong một bảng 'dày đặc' (một chỉ chứa nhiều hàng vì có các cặp khóa-giá trị thực tế) và đó là bảng dày đặc xảy ra để liệt kê các mục được chứa theo thứ tự. Xem đề xuất với Python-Dev để biết thêm chi tiết . Lưu ý rằng trong Python 3.6, đây được coi là một chi tiết triển khai, Python-the-ngôn ngữ không chỉ định rằng các triển khai khác phải giữ trật tự. Điều này đã thay đổi trong Python 3.7, trong đó chi tiết này được nâng lên thành một đặc tả ngôn ngữ ; để bất kỳ triển khai nào tương thích đúng với Python 3.7 hoặc mới hơn, nó phải sao chép hành vi giữ trật tự này. Và để rõ ràng: thay đổi này không áp dụng cho các bộ, vì các bộ đã có cấu trúc băm 'nhỏ'.

Python 2.7 và mới hơn cũng cung cấp một OrderedDictlớp , một lớp con dictcó thêm cấu trúc dữ liệu bổ sung để ghi lại thứ tự khóa. Với mức giá của một số tốc độ và bộ nhớ thêm, lớp này ghi nhớ theo thứ tự bạn đã chèn các phím; liệt kê các khóa, giá trị hoặc các mục sau đó sẽ làm như vậy theo thứ tự đó. Nó sử dụng một danh sách liên kết đôi được lưu trữ trong một từ điển bổ sung để giữ cho thứ tự được cập nhật hiệu quả. Xem bài viết của Raymond Hettinger phác thảo ý tưởng . OrderedDictcác đối tượng có lợi thế khác, chẳng hạn như được sắp xếp lại .

Nếu bạn muốn một bộ được đặt hàng, bạn có thể cài đặt osetgói ; nó hoạt động trên Python 2.5 trở lên.


1
Tôi không nghĩ các triển khai Python khác có thể sử dụng bất cứ thứ gì không phải là bảng băm theo cách này hay cách khác (mặc dù hiện tại có hàng tỷ cách khác nhau để thực hiện bảng băm, vì vậy vẫn còn một số quyền tự do). Thực tế là từ điển sử dụng __hash____eq__(và không có gì khác) thực tế là một bảo đảm ngôn ngữ, không phải là một chi tiết thực hiện.

1
@delnan: Tôi tự hỏi nếu bạn vẫn có thể sử dụng BTree với băm và kiểm tra công bằng .. Tôi chắc chắn không loại trừ điều đó, trong mọi trường hợp. :-)
Martijn Pieters

1
Điều đó chắc chắn là đúng, và tôi rất vui khi được chứng minh tính khả thi của văn bản sai, nhưng tôi không thấy cách nào người ta có thể đánh bại một bảng băm mà không yêu cầu một hợp đồng rộng hơn. Một BTree sẽ không có hiệu suất trường hợp trung bình tốt hơn và cũng không cung cấp cho bạn trường hợp xấu nhất tốt hơn (va chạm băm vẫn có nghĩa là tìm kiếm tuyến tính). Vì vậy, bạn chỉ có được sức đề kháng tốt hơn đối với nhiều băm neomg đồng quy (mod bảng kích thước), và có nhiều cách tuyệt vời khác để xử lý điều đó (một số trong đó được sử dụng dictobject.c) và kết thúc với sự so sánh ít hơn nhiều so với một BTree thậm chí cần tìm đúng cây con

@delnan: Tôi hoàn toàn đồng ý; Tôi hầu hết không muốn bị bash vì không cho phép các tùy chọn thực hiện khác.
Martijn Pieters

37

Đây là một phản hồi nhiều hơn cho Python 3.41 Một tập hợp trước khi nó được đóng lại dưới dạng trùng lặp.


Những người khác là đúng: không dựa vào thứ tự. Thậm chí đừng giả vờ có một.

Điều đó nói rằng, có một điều bạn có thể dựa vào:

list(myset) == list(myset)

Đó là, trật tự ổn định .


Hiểu lý do tại sao có một trật tự nhận thức đòi hỏi phải hiểu một số điều:

  • Python đó sử dụng các bộ băm ,

  • Cách bộ băm của CPython được lưu trữ trong bộ nhớ và

  • Làm thế nào các số được băm

Từ đầu trang:

Một bộ băm là một phương pháp lưu trữ dữ liệu ngẫu nhiên với thời gian tra cứu thực sự nhanh chóng.

Nó có một mảng sao lưu:

# A C array; items may be NULL,
# a pointer to an object, or a
# special dummy object
_ _ 4 _ _ 2 _ _ 6

Chúng ta sẽ bỏ qua đối tượng giả đặc biệt, chỉ tồn tại để giúp loại bỏ dễ dàng hơn để xử lý, vì chúng ta sẽ không xóa khỏi các bộ này.

Để có một tra cứu thực sự nhanh chóng, bạn thực hiện một số phép thuật để tính toán hàm băm từ một đối tượng. Quy tắc duy nhất là hai đối tượng bằng nhau có cùng hàm băm. (Nhưng nếu hai đối tượng có cùng hàm băm thì chúng có thể không bằng nhau.)

Sau đó, bạn thực hiện chỉ mục bằng cách lấy mô-đun theo độ dài mảng:

hash(4) % len(storage) = index 2

Điều này làm cho nó thực sự nhanh chóng để truy cập các yếu tố.

Băm chỉ là hầu hết các câu chuyện, hash(n) % len(storage)hash(m) % len(storage)có thể dẫn đến cùng một số. Trong trường hợp đó, một số chiến lược khác nhau có thể thử và giải quyết xung đột. CPython sử dụng "thăm dò tuyến tính" 9 lần trước khi thực hiện những việc phức tạp, do đó, nó sẽ nhìn sang bên trái của vị trí cho tối đa 9 địa điểm trước khi tìm nơi khác.

Các bộ băm của CPython được lưu trữ như thế này:

  • Một bộ băm có thể không quá 2/3 . Nếu có 20 phần tử và mảng sao lưu dài 30 phần tử, cửa hàng sao lưu sẽ thay đổi kích thước để lớn hơn. Điều này là do bạn bị va chạm thường xuyên hơn với các cửa hàng nhỏ, và va chạm làm chậm mọi thứ.

  • Cửa hàng sao lưu thay đổi kích thước theo quyền hạn 4, bắt đầu từ 8, ngoại trừ các bộ lớn (50k phần tử) thay đổi kích thước theo quyền hạn của hai: (8, 32, 128, ...).

Vì vậy, khi bạn tạo một mảng, cửa hàng sao lưu có độ dài 8. Khi nó đầy đủ 5 và bạn thêm một phần tử, nó sẽ chứa ngắn gọn 6 phần tử. 6 > ²⁄₃·8vì vậy, điều này kích hoạt thay đổi kích thước và cửa hàng sao lưu tăng gấp bốn lần kích thước 32.

Cuối cùng, hash(n)chỉ trả về nsố (trừ trường hợp -1đặc biệt).


Vì vậy, hãy nhìn vào cái đầu tiên:

v_set = {88,11,1,33,21,3,7,55,37,8}

len(v_set)là 10, vì vậy cửa hàng sao lưu ít nhất là 15 (+1) sau khi tất cả các mục đã được thêm vào . Sức mạnh liên quan của 2 là 32. Vì vậy, cửa hàng sao lưu là:

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

Chúng ta có

hash(88) % 32 = 24
hash(11) % 32 = 11
hash(1)  % 32 = 1
hash(33) % 32 = 1
hash(21) % 32 = 21
hash(3)  % 32 = 3
hash(7)  % 32 = 7
hash(55) % 32 = 23
hash(37) % 32 = 5
hash(8)  % 32 = 8

vì vậy những chèn như:

__  1 __  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __
   33 ← Can't also be where 1 is;
        either 1 or 33 has to move

Vì vậy, chúng tôi mong đợi một đơn đặt hàng như

{[1 or 33], 3, 37, 7, 8, 11, 21, 55, 88}

với 1 hoặc 33 không bắt đầu ở một nơi khác. Điều này sẽ sử dụng thăm dò tuyến tính, vì vậy chúng ta sẽ có:


__  1 33  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __

hoặc là


__ 33  1  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __

Bạn có thể mong đợi 33 sẽ là người bị thay thế bởi vì 1 đã ở đó, nhưng do việc thay đổi kích thước xảy ra khi bộ đang được xây dựng, thực tế không phải vậy. Mỗi khi bộ được xây dựng lại, các mục đã thêm sẽ được sắp xếp lại một cách hiệu quả.

Bây giờ bạn có thể thấy tại sao

{7,5,11,1,4,13,55,12,2,3,6,20,9,10}

có thể theo thứ tự. Có 14 yếu tố, vì vậy cửa hàng sao lưu ít nhất là 21 + 1, có nghĩa là 32:

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

1 đến 13 băm trong 13 vị trí đầu tiên. 20 đi vào khe 20.

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ __ __ __ __ __ __ __ __ __

55 đi trong khe hash(55) % 32là 23:

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ 55 __ __ __ __ __ __ __ __

Nếu chúng tôi chọn 50 thay vào đó, chúng tôi sẽ mong đợi

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ 50 __ 20 __ __ __ __ __ __ __ __ __ __ __

Và lo và kìa:

{1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 20, 50}
#>>> {1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 50, 20}

pop được thực hiện khá đơn giản bởi vẻ bề ngoài của mọi thứ: nó đi qua danh sách và bật cái đầu tiên.


Đây là tất cả các chi tiết thực hiện.


17

"Tùy tiện" không giống như "không xác định".

Điều họ đang nói là không có thuộc tính hữu ích nào của thứ tự lặp từ điển là "trong giao diện công cộng". Hầu như chắc chắn có nhiều thuộc tính của thứ tự lặp được xác định đầy đủ bởi mã hiện đang thực hiện phép lặp từ điển, nhưng các tác giả không hứa hẹn chúng với bạn như một thứ bạn có thể sử dụng. Điều này cho phép họ tự do hơn để thay đổi các thuộc tính này giữa các phiên bản Python (hoặc thậm chí chỉ trong các điều kiện hoạt động khác nhau hoặc hoàn toàn ngẫu nhiên khi chạy) mà không lo chương trình của bạn sẽ bị hỏng.

Do đó, nếu bạn viết một chương trình phụ thuộc vào bất kỳ thuộc tính nào theo thứ tự từ điển, thì bạn đang "phá vỡ hợp đồng" sử dụng loại từ điển và các nhà phát triển Python không hứa rằng nó sẽ luôn hoạt động, ngay cả khi nó có vẻ hoạt động bây giờ khi bạn kiểm tra nó Về cơ bản, nó tương đương với việc dựa vào "hành vi không xác định" trong C.


3
Lưu ý rằng một phần của phép lặp từ điển được xác định rõ: Lặp lại các khóa, giá trị hoặc các mục của một từ điển nhất định sẽ xảy ra theo cùng một thứ tự, miễn là không có thay đổi nào được thực hiện cho từ điển ở giữa. Điều đó có nghĩa d.items()là về cơ bản là giống hệt nhau zip(d.keys(), d.values()). Tuy nhiên, nếu bất kỳ mục nào được thêm vào từ điển, tất cả các cược đã tắt. Thứ tự có thể thay đổi hoàn toàn (nếu cần thay đổi kích thước bảng băm), mặc dù hầu hết thời gian bạn chỉ cần tìm mục mới xuất hiện ở một số vị trí tùy ý trong chuỗi.
Blckknght

6

Các câu trả lời khác cho câu hỏi này là tuyệt vời và được viết tốt. OP hỏi "làm thế nào" mà tôi diễn giải là "làm thế nào để họ thoát khỏi" hoặc "tại sao".

Tài liệu Python nói rằng từ điển không được đặt hàng vì từ điển Python thực hiện mảng kết hợp kiểu dữ liệu trừu tượng . Như họ nói

thứ tự mà các ràng buộc được trả về có thể là tùy ý

Nói cách khác, một sinh viên khoa học máy tính không thể cho rằng một mảng kết hợp được đặt hàng. Điều tương tự cũng đúng với các bộ trong toán học

thứ tự mà các yếu tố của một bộ được liệt kê là không liên quan

khoa học máy tính

một tập hợp là một kiểu dữ liệu trừu tượng có thể lưu trữ các giá trị nhất định mà không cần bất kỳ thứ tự cụ thể nào

Việc thực hiện một từ điển bằng cách sử dụng bảng băm là một chi tiết triển khai thú vị ở chỗ nó có các thuộc tính giống như các mảng kết hợp khi có liên quan đến thứ tự.


1
Bạn về cơ bản đúng, nhưng nó sẽ là một chút gần gũi hơn (và đưa ra một gợi ý tốt tại lý do đó là "không có thứ tự") để nói nó là một thi hành một bảng băm chứ không phải là một mảng assoc.
Nhà giả kim hai bit

5

Python sử dụng bảng băm để lưu trữ từ điển, do đó không có thứ tự nào trong từ điển hoặc các đối tượng lặp khác sử dụng bảng băm.

Nhưng liên quan đến các chỉ mục của các mục trong một đối tượng băm, python tính toán các chỉ mục dựa trên mã sau tronghashtable.c :

key_hash = ht->hash_func(key);
index = key_hash & (ht->num_buckets - 1);

Do đó, vì giá trị băm của các số nguyên là chính số nguyên * chỉ số dựa trên số ( ht->num_buckets - 1là hằng số) nên chỉ số được tính theo Bitwise-và giữa (ht->num_buckets - 1)và chính số đó * (mong đợi -1 mà băm của nó là -2 ) và cho các đối tượng khác có giá trị băm của chúng.

xem xét ví dụ sau với setbảng băm đó:

>>> set([0,1919,2000,3,45,33,333,5])
set([0, 33, 3, 5, 45, 333, 2000, 1919])

Đối với số lượng 33chúng tôi có:

33 & (ht->num_buckets - 1) = 1

Đó thực sự là:

'0b100001' & '0b111'= '0b1' # 1 the index of 33

Lưu ý trong trường hợp (ht->num_buckets - 1)này là 8-1=7hoặc 0b111.

Và cho 1919:

'0b11101111111' & '0b111' = '0b111' # 7 the index of 1919

Và cho 333:

'0b101001101' & '0b111' = '0b101' # 5 the index of 333

Để biết thêm chi tiết về hàm băm python, tốt nhất là đọc các trích dẫn sau từ mã nguồn python :

Sự tinh tế chính ở phía trước: Hầu hết các lược đồ băm phụ thuộc vào việc có hàm băm "tốt", theo nghĩa mô phỏng ngẫu nhiên. Python không: các hàm băm quan trọng nhất của nó (đối với chuỗi và int) rất thường xuyên trong các trường hợp phổ biến:

>>> map(hash, (0, 1, 2, 3))
  [0, 1, 2, 3]
>>> map(hash, ("namea", "nameb", "namec", "named"))
  [-1658398457, -1658398460, -1658398459, -1658398462]

Điều này không hẳn là xấu! Ngược lại, trong một bảng có kích thước 2 ** i, lấy các bit i thứ tự thấp làm chỉ số bảng ban đầu là cực kỳ nhanh và không có xung đột nào đối với các dicts được lập chỉ mục bởi một phạm vi int liền kề. Điều tương tự cũng đúng khi các khóa là các chuỗi "liên tiếp". Vì vậy, điều này mang lại hành vi tốt hơn ngẫu nhiên trong các trường hợp phổ biến và đó là điều rất đáng mong đợi.

OTOH, khi va chạm xảy ra, xu hướng lấp đầy các lát liền kề của bảng băm làm cho một chiến lược giải quyết va chạm tốt là rất quan trọng. Chỉ lấy các bit i cuối cùng của mã băm cũng dễ bị tổn thương: ví dụ: coi danh sách [i << 16 for i in range(20000)]là một bộ khóa. Vì int là mã băm riêng của chúng và điều này phù hợp với một kích thước 2 ** 15, 15 bit cuối cùng của mỗi mã băm đều là 0: tất cả chúng ánh xạ tới cùng một chỉ mục bảng.

Nhưng phục vụ cho các trường hợp bất thường không nên làm chậm những trường hợp thông thường, vì vậy chúng tôi chỉ lấy các bit i cuối cùng. Đó là tùy thuộc vào giải quyết va chạm để làm phần còn lại. Nếu chúng ta thường tìm thấy khóa mà chúng ta đang tìm kiếm trong lần thử đầu tiên (và, hóa ra, chúng ta thường làm - hệ số tải bảng được giữ dưới 2/3, vì vậy tỷ lệ cược chắc chắn có lợi cho chúng ta), sau đó nó có ý nghĩa tốt nhất để giữ cho chỉ số ban đầu tính toán bụi bẩn giá rẻ.


* Hàm băm cho lớp int:

class int:
    def __hash__(self):
        value = self
        if value == -1:
            value = -2
        return value


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.