python內建的hash函式對於字串來說,每次得到的值不一樣?
set/dict的hash還真就是這個玩意實現的,因為它保證了在同一個直譯器程序裡相同字串hash一致。
因為CPython 3。x裡的str,它的實體是unicode物件,實體是個utf-8 bytes或者是wstr(嗯這裡真特麼有個『或者』),並且透過一個叫做unicodedata_db的玩意來實現快取(不然就沒法兒保證str物件的不可變與地址一致性了)。
於是乎當你調內部hash的時候,反正不同程序中的直譯器不會共用一個unicodedata_db,這個直譯器程序裡的字串的hash到另一個程序裡指不定連個字串都不是,所以在計算這個內部hash的時候加入了一個code_magic的玩意,同時也均攤了一把複雜度,省得這個db以及set/dict對特定資料表現出極差效能。再說了,誰也不會傻到拿個直譯器內部hash去做跨程序交換。
所以真需要做可重現可跨程序保持一致性的hash,請用hashlib。
python的字串hash演算法並不是直接遍歷字串每個字元去計算hash,而是會有一個secret prefix和一個secret suffix,可以認為相當於是給字串加鹽後做hash,可以規避一些規律輸入的情況
顯然這個secret前後綴的值會直接影響計算結果,而且它有一個啟動時隨機生成的機制,只不過,在2。x版本中,這個機制預設是關閉的,前後綴每次啟動都設定為0,除非你改了相關環境變數來要求隨機,而在3。x中修改了預設行為,如果你不配置環境變數,則預設是隨機一個前後綴值,這樣每次啟動都會不同
這個環境變數是PYTHONHASHSEED,無論在2。x還是3。x中,配置為一個正整數,將作為隨機種子;配置為0,則secret前後綴預設清零(和2。x預設行為就一樣了),配置為空串或“random”,則表示讓程序隨機生成(和3。x預設行為一樣)
具體為啥要這麼做,猜測一個是為了安全性(防字串hash表的攻擊,比如php曾經碰到的攻擊),另一個可能也是強調不要依賴一些內建結果,因為這種演算法可能隨著版本而更新,避免有些使用者不看文件,誤以為是永遠不變的
給題主放倆老新聞的傳送門:
Huge portions of the Web vulnerable to hashing denial-of-service attack
Denial of service via hash collisions
這就是Python的字串hash隨機化的源頭。
Java(Oracle JDK / OpenJDK)也曾經採用過類似的修補方式,像這樣來可選地初始化一個隨機數seed:
http://
hg。openjdk。java。net/jdk
7u/jdk7u/jdk/file/1afb03696eb7/src/share/classes/java/util/HashMap。java#l332
/**
* Initialize the hashing mask value。 We defer initialization until we
* really need it。
*/
final
boolean
initHashSeedAsNeeded
(
int
capacity
)
{
boolean
currentAltHashing
=
hashSeed
!=
0
;
boolean
useAltHashing
=
sun
。
misc
。
VM
。
isBooted
()
&&
(
capacity
>=
Holder
。
ALTERNATIVE_HASHING_THRESHOLD
);
boolean
switching
=
currentAltHashing
^
useAltHashing
;
if
(
switching
)
{
hashSeed
=
useAltHashing
?
sun
。
misc
。
Hashing
。
randomHashSeed
(
this
)
:
0
;
}
return
switching
;
}
後來在Oracle JDK8 / OpenJDK8裡改為在衝突鏈長的時候使用紅黑樹來降低鏈的長度,就沒有繼續使用這個alternative hashing了。
新版本Python的一個功能,內建hash函式帶有隨機的magic。補充一下其他答案,這個功能有一定的安全性上的考慮,可以讓攻擊者難以預測內建的set或者dict的一些行為,但遠不足以承擔真正的密碼安全級別的hash的作用。傳遞set和dict到其他程序的時候,只會傳遞其中的值,而不會傳遞hash表結構,hash表是傳到之後重新建立起來的。
特意糾正一下 雷小雷 的回覆
菜鳥教程原文:
“在
hash()
對物件使用時,所得的結果不僅和物件的內容有關,還和物件的
id()
,也就是記憶體地址有關。”
這個是錯誤的說法。
我驗證了一下,和記憶體地址無關, 同一個python程序內只要值是一樣的,hash的值就是一樣的
result
=
[
‘12345’
,
12345
]
def
show_hash
(
i
):
name
=
str
(
result
[
i
])
(
f
‘id of name is {id(name)} hash
{name}
: {hash(name)}’
)
p_list
=
[]
for
i
in
range
(
2
):
p
=
Process
(
target
=
show_hash
,
args
=
(
i
,))
p
。
start
()
time
。
sleep
(
2
)