Kapan hash (n) == n di Python?

100

Saya telah bermain dengan fungsi hash Python . Untuk bilangan bulat kecil, itu hash(n) == nselalu muncul . Namun ini tidak berlaku untuk jumlah besar:

>>> hash(2**100) == 2**100
False

Saya tidak terkejut, saya mengerti hash membutuhkan rentang nilai yang terbatas. Berapa kisaran itu?

Saya mencoba menggunakan pencarian biner untuk menemukan angka terkecilhash(n) != n

>>> import codejamhelpers # pip install codejamhelpers
>>> help(codejamhelpers.binary_search)
Help on function binary_search in module codejamhelpers.binary_search:

binary_search(f, t)
    Given an increasing function :math:`f`, find the greatest non-negative integer :math:`n` such that :math:`f(n) \le t`. If :math:`f(n) > t` for all :math:`n \ge 0`, return None.

>>> f = lambda n: int(hash(n) != n)
>>> n = codejamhelpers.binary_search(f, 0)
>>> hash(n)
2305843009213693950
>>> hash(n+1)
0

Apa yang spesial dari 2305843009213693951? Saya perhatikan itu kurang darisys.maxsize == 9223372036854775807

Sunting: Saya menggunakan Python 3. Saya menjalankan pencarian biner yang sama pada Python 2 dan mendapatkan hasil yang berbeda 2147483648, yang saya perhatikan adalah sys.maxint+1

Saya juga bermain-main dengan [hash(random.random()) for i in range(10**6)]memperkirakan kisaran fungsi hash. Maksimal secara konsisten di bawah n di atas. Membandingkan min, tampaknya hash Python 3 selalu bernilai positif, sedangkan hash Python 2 dapat mengambil nilai negatif.

Kolonel Panik
sumber
9
Sudahkah Anda memeriksa representasi biner nomor tersebut?
John Dvorak
3
'0b1111111111111111111111111111111111111111111111111111111111111' penasaran! Jadi n+1 == 2**61-1
Kolonel Panic
2
tampaknya bergantung pada sistem. Dengan python saya, hash adalah nuntuk seluruh kisaran int 64bit.
Daniel
1
Perhatikan tujuan nilai hash: Mereka digunakan untuk membandingkan kunci kamus dengan cepat selama pencarian kamus. Dengan kata lain, definisi implementasi, dan karena lebih pendek dari banyak nilai yang dapat memiliki nilai hash, mungkin memiliki benturan sangat baik bahkan dalam ruang input yang wajar.
CVn
2
Um, tidak 2147483647sama dengan sys.maxint(tidak sys.maxint+1), dan jika 'n = 0b1111111111111111111111111111111111111111111111111111111111111' maka bukan n+1 == 2**61atau n == 2**61-1(tidak n+1 == 2**61-1)?
phoog

Jawaban:

73

Berdasarkan dokumentasi python di pyhash.cfile:

Untuk tipe numerik, hash bilangan x didasarkan pada pengurangan x modulo bilangan prima P = 2**_PyHASH_BITS - 1. Ini dirancang sedemikian rupa sehingga hash(x) == hash(y)setiap kali x dan y sama secara numerik, bahkan jika x dan y memiliki tipe yang berbeda.

Jadi untuk mesin 64/32 bit, pengurangannya adalah 2 _PyHASH_BITS - 1, tapi apa itu _PyHASH_BITS?

Anda dapat menemukannya di pyhash.hfile header yang untuk mesin 64 bit telah didefinisikan sebagai 61 (Anda dapat membaca penjelasan lebih lanjut di pyconfig.hfile).

#if SIZEOF_VOID_P >= 8
#  define _PyHASH_BITS 61
#else
#  define _PyHASH_BITS 31
#endif

Jadi pertama-tama ini berdasarkan platform Anda misalnya di platform Linux 64bit saya pengurangannya adalah 2 61 -1, yaitu 2305843009213693951:

>>> 2**61 - 1
2305843009213693951

Anda juga dapat menggunakan math.frexpuntuk mendapatkan mantissa dan eksponen sys.maxintyang untuk mesin 64 bit menunjukkan bahwa max int adalah 2 63 :

>>> import math
>>> math.frexp(sys.maxint)
(0.5, 64)

Dan Anda dapat melihat perbedaannya dengan tes sederhana:

>>> hash(2**62) == 2**62
True
>>> hash(2**63) == 2**63
False

Baca dokumentasi lengkap tentang algoritma hashing python https://github.com/python/cpython/blob/master/Python/pyhash.c#L34

Seperti disebutkan dalam komentar, Anda dapat menggunakan sys.hash_info(dalam python 3.X) yang akan memberi Anda urutan struct parameter yang digunakan untuk menghitung hash.

>>> sys.hash_info
sys.hash_info(width=64, modulus=2305843009213693951, inf=314159, nan=0, imag=1000003, algorithm='siphash24', hash_bits=64, seed_bits=128, cutoff=0)
>>> 

Di samping modulus yang saya jelaskan di baris sebelumnya, Anda juga bisa mendapatkan infnilai sebagai berikut:

>>> hash(float('inf'))
314159
>>> sys.hash_info.inf
314159
Kasravnd
sumber
3
Akan bagus untuk disebutkan sys.hash_info, untuk kelengkapan.
Mark Dickinson
78

2305843009213693951adalah 2^61 - 1. Ini adalah bilangan prima Mersenne terbesar yang cocok dengan 64 bit.

Jika Anda harus membuat hash hanya dengan mengambil mod nilai beberapa, maka bilangan prima Mersenne besar adalah pilihan yang baik - mudah untuk menghitung dan memastikan distribusi kemungkinan yang merata. (Meskipun saya pribadi tidak akan pernah membuat hash seperti ini)

Sangat mudah untuk menghitung modulus untuk bilangan floating point. Mereka memiliki komponen eksponensial yang mengalikan bilangan bulat dengan 2^x. Karena 2^61 = 1 mod 2^61-1, Anda hanya perlu mempertimbangkan file (exponent) mod 61.

Lihat: https://en.wikipedia.org/wiki/Mersenne_prime

Matt Timmermans
sumber
8
Anda mengatakan Anda tidak akan pernah membuat hash seperti ini. Apakah Anda memiliki saran alternatif tentang bagaimana hal itu dapat dilakukan dengan cara yang membuatnya cukup efisien untuk menghitung int, float, Desimal, Pecahan dan memastikan bahwa x == yjaminan hash(x) == hash(y)di seluruh jenis? (Angka-angka seperti Decimal('1e99999999')sangat bermasalah, misalnya: Anda tidak ingin harus mengembangkannya ke bilangan bulat yang sesuai sebelum melakukan hashing.)
Mark Dickinson
@MarkDickinson Saya curiga dia mencoba menggambarkan perbedaan antara hash cepat keringanan sederhana ini, dan hash kriptografi yang juga peduli tentang membuat keluaran terlihat acak.
Mike Ounsworth
4
@ MarkDickinson Modulus adalah permulaan yang baik, tetapi saya kemudian akan mencampurnya lagi, terutama mencampurkan beberapa bit tinggi ke yang rendah. Tidak jarang melihat urutan bilangan bulat yang dapat dibagi dengan pangkat 2. Tidak jarang juga melihat tabel hash dengan kapasitas yang merupakan pangkat 2. Di Java, misalnya, jika Anda memiliki urutan bilangan bulat yang habis dibagi 16, dan Anda menggunakannya sebagai kunci dalam HashMap, Anda hanya akan menggunakan 1/16 dari keranjang (setidaknya dalam versi sumber yang saya lihat)! Saya pikir hash harus setidaknya terlihat sedikit acak untuk menghindari masalah ini
Matt Timmermans
Ya, hash gaya pencampuran bit jauh lebih unggul daripada hash yang terinspirasi matematika. Instruksi pencampuran bit sangat murah sehingga Anda dapat memiliki banyak dengan biaya yang sama. Selain itu, data dunia nyata tampaknya tidak memiliki pola yang tidak berfungsi baik dengan pencampuran bit. Tapi ada pola yang buruk untuk modulus.
usr
9
@usr: Tentu, tapi hash bit-pencampuran adalah tidak layak di sini: persyaratan bahwa pekerjaan hash untuk int, float, Decimaldan Fractionbenda-benda dan x == ymenyiratkan hash(x) == hash(y)bahkan ketika xdan ymemiliki berbagai jenis membebankan beberapa kendala yang agak parah. Jika ini hanya masalah menulis fungsi hash untuk integer, tanpa mengkhawatirkan jenis lainnya, itu akan menjadi masalah yang sama sekali berbeda.
Mark Dickinson
9

Fungsi hash mengembalikan int polos yang berarti nilai yang dikembalikan lebih besar dari -sys.maxintdan lebih rendah dari sys.maxint, yang berarti jika Anda meneruskannya sys.maxint + xhasilnya akan menjadi -sys.maxint + (x - 2).

hash(sys.maxint + 1) == sys.maxint + 1 # False
hash(sys.maxint + 1) == - sys.maxint -1 # True
hash(sys.maxint + sys.maxint) == -sys.maxint + sys.maxint - 2 # True

Sementara 2**200itu nkali lebih besar dari sys.maxint- tebakan saya adalah bahwa hash akan melewati rentang -sys.maxint..+sys.maxintn kali sampai berhenti pada integer biasa dalam kisaran itu, seperti dalam cuplikan kode di atas ..

Jadi secara umum, untuk n <= sys.maxint apa pun :

hash(sys.maxint*n) == -sys.maxint*(n%2) +  2*(n%2)*sys.maxint - n/2 - (n + 1)%2 ## True

Catatan: ini benar untuk python 2.

Andriy Ivaneyko
sumber
8
Ini mungkin benar untuk Python 2, tetapi jelas tidak untuk Python 3 (yang tidak memiliki sys.maxint, dan yang menggunakan fungsi hash yang berbeda).
interjay
0

The implementasi untuk tipe int di cpython dapat ditemukan di sini.

Itu hanya mengembalikan nilai, kecuali -1, daripada yang dikembalikan -2:

static long
int_hash(PyIntObject *v)
{
    /* XXX If this is changed, you also need to change the way
       Python's long, float and complex types are hashed. */
    long x = v -> ob_ival;
    if (x == -1)
        x = -2;
    return x;
}
Jieter
sumber
6
Ini tidak termasuk nilai besar, yang diimplementasikan oleh PyLongdaripada PyInt.
interjay