在Python中生成隨機數據(指南)

在Python中生成随机数据(指南)

目錄

  • 隨機是如何隨機的?

  • 什麼是“密碼學安全"?

  • 將會在本教程中向您介紹什麼

  • Python中的PRNGs

    • 隨機模塊

    • 數組的PRNGs:numpy.random

  • Python中的CSPRNGs

    • os.urandom:隨機獲取

    • Python中保密的最好方法:secrets

  • 最後向您介紹:uuid

  • 為什麼不直接“默認使用”SystemRandom呢?

  • 結尾再說一下:哈希

  • 回顧

隨機是如何隨機的?這是一個奇怪的問題,但在涉及信息安全的情況下,這是一個至關重要的問題。每當你在Python中生成隨機數據,字符串或數字時,至少應該粗略地瞭解數據是如何生成的。

本篇教程,將會向您介紹用於在Python中生成隨機數據的幾個不同的方法,然後從安全級別,通用性,目的和速度等方面進行比較。

我保證本教程不會變成一堂數學課或是密碼學課,因為一開始我就沒有足夠的數學知識進行講授。您僅僅會了解到必要的數學知識,僅此而已。

隨機是如何隨機的?

首先,有必要進行聲明的是,用Python生成的大多數隨機數據從科學角度來說並不是真正隨機的。相反,它是偽隨機的:它是由偽隨機數生成器(pseudorandom number generator,PRNG)生成的,其本質是任意一種能夠產生看似隨機但仍可重複生成的數據的算法。

你猜得沒錯,“真”隨機數可以通過真隨機數生成器(true random number generator,TRNG)產生。舉例來說就是,從地板上反覆撿起一個骰子,扔到空中,然後讓它自己落到地上。

假設你的拋擲是無偏的,你真的不知道骰子會落在哪個數字上。扔骰子是一種使用硬件生成非確定性數字的簡單形式。(或者,你可以讓dice-o-matic幫你做這件事。)TRNGs超出了本文的範圍,但是為了進行比較,仍然值得一提。

PRNGs的工作方式有些許不同,通常使用軟件而非硬件進行相關操作。如下是一個簡單的描述:

它們以一個偽隨機數開始,即種子,然後使用一種算法在此基礎上生成一個偽隨機比特序列。

有些時候,您可能被告知要“閱讀文檔!”。好吧,這些人並沒有錯。這裡有一個來自random模塊文檔中特別值得注意的片段,您肯定不想錯過:

警告:不應將此模塊的偽隨機生成器用於安全目的。

您可能在Python中見過random.seed(999)、random.seed(1234)或類似的東西。此函數調用為Python中random模塊使用的底層隨機數生成器提供隨機數種子。它使後續調用產生的隨機數是確定的:輸入A總是產生輸出B。如果惡意使用,這種特性會產生嚴重的問題。

也許“隨機的”和“確定性的”這兩個術語看起來不能共存。為了更清楚地說明這一點,這裡有一個非常精簡的random版本,它使用x = (x * 3) % 19迭代地創建一個“隨機”數字。x最初被定義為種子值,隨後根據該種子產生出一個確定的數字序列:

在Python中生成随机数据(指南)

不要太在意這個例子的細節,因為它主要是為了說明概念。如果使用種子值1234,那麼後續調用random所產生的序列應該始終相同:

在Python中生成随机数据(指南)

很快您將會看到一個更嚴謹的例子。

什麼是“密碼學安全”?

你可能還沒弄熟“RNG”的含義,但這裡我們要再向你介紹一個:CSPRNG,或者可以稱為密碼學安全(cryptographically secure)PRNG。CSPRNGs適用於產生敏感數據,如密碼、身份驗證或是令牌。給定一個隨機字符串,惡意攻擊者Joe實際上沒有辦法確定在隨機字符串序列中該字符串之前或之後出現了什麼字符串。

另一個你也許會看到的術語是熵。簡而言之,這代表了引入或是期望的隨機性的數量。本文中介紹的一個Python模塊中定義DEFAULT_ENTROPY = 32,即默認返回的數字字節數。開發者將其視為“足夠”表示噪音的字節數量。

注意:在本教程中,假定一個字節代表8個比特而非其他的數據存儲單元。

對於GSPRNGs,關鍵的一點是它們仍然是偽隨機的。它們以某種內部確定性的方式被設計,但是它們添加了一些其他變量或者是其他的特性以禁止將其回退到任意確定性函數。

將會在本教程中向您介紹什麼

從實踐角度來說,您將會使用普通的PRNGs進行統計建模、模擬並使隨機數據可重現。您稍後將會看到,PRNGs明顯快於CSPRNGs。CSPRNGs被用於安全和密碼應用中,在這些應用中,數據敏感性是必要的。

在本教程中,除了擴展上述用例之外,您還將深入研究使用PRNGs和CSPRNGs的Python工具:

  • PRNG包括Python標準庫中的random模塊和在Numpy中對應的基於數組的numpy.random。

  • Python中的os,secrets以及uuid模塊包含產生密碼學安全對象的函數。

您將接觸到上述的所有內容,並從高層次進行比較。

Python中的PRNGs

random模塊

使用Python產生隨機數據最廣為人知的方式可能就是它的random模塊了,它使用Mersenne Twister PRNG算法作為它的核心生成器。

早些時候,您粗略的接觸過random.seed,現在是時候看看它是如何工作的。首先,讓我們在不設置隨機數種子的情況下構建一些隨機數據。random.random函數返回在區間[0.0, 1.0]之間的一個隨機浮點數。產生結果總是小於右邊端點(1.0),這也被稱為半開區間。

在Python中生成随机数据(指南)

如果您自己運行這段代碼,我敢打賭,您機器上返回的數字會不一樣。默認情況下,當您沒有設置隨機數種子時,將使用當前系統時間或操作系統的“隨機源”(如果有的話)作為隨機數種子。

通過random.seed您可以使結果具有可重複性,在此之後連續調用將會產生相同的數據。

在Python中生成随机数据(指南)

注意重複的“隨機”數字。隨機數序列變為確定性的,或者說完全由隨機數種子444確定。

讓我們再來看看random的一些更基本的功能。上文中,您生成了一個隨機的浮點數。您可以在Python中使用random.randint函數在兩個端點之間生成一個隨機整數。隨機數生成範圍跨越整個[x, y]區間,可能包括兩個端點:

在Python中生成随机数据(指南)

使用random.randrange,您可以將區間右端排除掉,也就是說產生的隨機數總是處於[x,y)之間並且總是小於右端點。

在Python中生成随机数据(指南)

如果您需要生成位於特定區間[x,y]之間的浮點數,您可以使用random.uniform函數,該函數產生的隨機數符合連續均勻分佈:

在Python中生成随机数据(指南)

您可以使用random.choice函數從非空序列(如列表或元組)中選擇隨機元素。同時也可以使用random.choices用於從序列中可放回(可以重複)地選擇多個元素:

在Python中生成随机数据(指南)

在不替換的情況下模擬採樣,請使用random.sample函數:

在Python中生成随机数据(指南)

你可以使用random.shuffle函數在原位置隨機化一個序列。這將會修改序列對象並隨機化元素的位置:

在Python中生成随机数据(指南)

如果您不希望更改原始列表,那麼您需要先創建一個副本,然後對副本執行shuffle操作。您可以使用copy模塊創建Python列表的副本,或者使用x[:]或x.copy,其中x是列表。

在繼續使用NumPy生成隨機數據之前,讓我們先看一個稍微複雜一點的應用程序:生成一個具有統一長度的唯一隨機字符串序列。

這可以幫助您思考函數設計的方法。您需要從一個字符“池”中選擇如字母、數字和/或標點符號,將它們組合成一個字符串,然後再檢查這個字符串是否已經生成過。Python中的set可以很好的用於這種成員關係測試:

在Python中生成随机数据(指南)

''.join將random.choices所選取的字母連結成一個長度為k的python str。該token被添加到不能包含重複元素的集合中,while循環不斷執行直到集合中的元素達到您指定好的數量。

資源:Python的string模塊包含了一些有用的常量:ascii_lowercase, ascii_uppercase, string.punctuation, ascii_whitespace以及少數一些其他的東西。

讓我們來試試這個函數:

在Python中生成随机数据(指南)

Stack Overflow上有本函數的調整版。它使用了生成器函數,名稱綁定以及一些其他的高級技巧創造了一個上面的unique_strings的更快的,密碼安全版本。

對於數組的PRNGs:numpy.random

您可能已經注意到,random中的大多數隨機函數返回一個標量值(單個int、float或其他對象)。如果你想要生成一個隨機數序列,有一種方法是使用Python列表生成式:

在Python中生成随机数据(指南)

還有另一種選擇是專門為此設計的。你可以把NumPy自己的numpy.random包看成類似於標準庫的random包,但它是用於處理NumPy數組的。(它還具有從更多統計分佈中提取數據的能力。)

注意,numpy.random使用自己的PRNG,它與普通的random不同。調用Python自己的random.seed不會生成確定性的隨機NumPy數組:

在Python中生成随机数据(指南)

閒話少說,下面有幾個例子來激發你的胃口:

在Python中生成随机数据(指南)

在語句randn(d0, d1, ..., dn)中,參數d0, d1, ..., dn是可選的,表示最終生成對象的形狀。這裡,np.random.randn(3,4)創建了一個3行4列的二維數組。其中數據是獨立同分布的,表示每個數據點都是獨立於其他數據點產生的。

另一個常見的操作是創建一個True或False的隨機布爾值序列。一種方法是使用np.random.choice([True, False])。然而,實際上直接從(0,1)中進行選擇要快4倍,然後再將這些整數轉換為相應的布爾值:

在Python中生成随机数据(指南)

如何生成相關數據?假設你想要模擬兩個相關的時間序列。一種方法是使用NumPy的multivariate_normal函數,該函數使用了協方差矩陣。換句話說,要從單個正態分佈隨機變量中生成數據,需要指定其均值和方差(或標準差)。

要從多元正態分佈中進行採樣,您需要指定均值和協方差矩陣,最後可以得到多個相關的數據序列,每個數據序列大致呈正態分佈。

然而,相較於協方差,相關性是更為大多數人所熟悉並直觀的度量標準。它是由標準差的乘積歸一化的協方差,因此您還可以根據相關性和標準差來定義協方差:

在Python中生成随机数据(指南)

那麼,可以通過指定一個相關矩陣和標準差從一個多元正態分佈中抽取隨機樣本嗎? 答案是可以的,但是你需要先把上面的公式變成矩陣形式。這裡,S是標準差的向量,P是它們的相關矩陣,C是結果(平方)協方差矩陣:

在Python中生成随机数据(指南)

用Numpy表示如下:

在Python中生成随机数据(指南)

現在,您可以生成兩個相關的但是仍然隨機的時間序列:

在Python中生成随机数据(指南)

您可以將data看作是500對負相關的數據點。下面是一個對應原始輸入近似的corr, stdev,和mean完整的檢查:

在Python中生成随机数据(指南)

在開始介紹CSPRNGs之前,總結一下一些random以及它們在numpy.random中對應的函數應該會很有幫助:

在Python中生成随机数据(指南)

注意:NumPy專門用於構建和操作大型多維數組。如果您只需要一個值,那麼random就足夠了,可能還會更快。對於小序列,random也可能更快,因為NumPy會帶來了一些額外開銷。

目前,已經向您介紹了PRNGs中的兩個基本成員,下面讓我們看看一些更安全的版本。

Python中的CSPRNGs

os.urandom:隨機獲取

Python的os.urandom函數也應用在secrets和uuid中(這兩者一會兒都會向您介紹)。簡單來說,os.urandom生成依賴於操作系統的隨機字節,符合密碼學安全:

  • 在Unix操作系統中,它從/dev/urandom這個特殊的文件中讀取隨機字節,該文件“允許訪問從設備驅動以及其他來源產生的環境噪聲”(感謝維基百科)。這是一些混亂的信息,這些信息與您的硬件和系統狀態有關,但同時又是足夠隨機的

  • 在Windows中,則是調用C++函數CryptGenRandom。這個函數從技術上來說仍然是偽隨機的,但是在運行過程中從進程ID,內存狀態等類似變量中產生隨機數種子

在os.urandom中,沒有手動設置隨機數種子的概念。雖然在技術上仍然是偽隨機,但這個函數更符合我們對隨機性的看法。唯一的參數是要返回的字節數:

在Python中生成随机数据(指南)

在我們進行進一步討論之前,也許是深入學習字符編碼迷你課程的好時機。許多人,包括我自己,在看到字節對象和一長串\\x字符時都會有某種過敏反應。然而,知道像上面的x這樣的序列最終如何變成字符串或數字是很有用的。(Python部落:python.freelycode.com上的《編解碼那些事兒》很好地講解了這點)

os.urandom返回單個字節組成的序列:

在Python中生成随机数据(指南)

但是,這些東西最終是如何變成Python的str或數字序列呢?

首先,回憶一下計算的一個基本概念,即一個字節由8 bit組成。你可以把這些bit看作是一個個不是0就是1的位。一個字節有效地在0和1之間選擇8次,所以01101100和11110000都可以表示字節。嘗試下面的例子,它使用了Python 3.6中引入的Python f-string:

在Python中生成随机数据(指南)

這相當於[bin(i) for i in range(256)],同時還帶有特定的格式。bin函數將一個整數轉換成用字符串表示的二進制形式。

上述例子說明什麼問題呢?上面的例子並不是隨機選擇使用range(256)的。每個字節有8位,每位有兩種選擇,則一共有2 ** 8 == 256種字節“組合”。

這意味著每個字節被映射到一個0到255之間的整數。換句話說,為了表示整數256,我們需要更多的位數。你可以通過 len(f'{256:0>8b}')為9而不是8來驗證這一點。

好的,現在讓我們回到您在上面看到的字節數據類型,通過構造一個字節序列,從而對應0到255的整數:

在Python中生成随机数据(指南)

如果您調用list(bites),您將得到一個從0到255的Python列表。但如果你只打印bites,你就只會得到一個散落著反斜槓的難看的序列:

在Python中生成随机数据(指南)

這些反斜槓是轉義字符,\\xhh表示十六進制值為hh的字符。bites中的一些元素按字面量顯示(可打印的字符,如字母、數字和標點符號)。大多數則是用轉義字符表示的。\\x08表示鍵盤的退格,而\\x13表示回車(在Windows系統上是新行的一部分)。

如果您需要複習一下十六進制,那麼Charles petzold的Code:The Hidden Language是一個不錯的選擇。十六進制是一種基本的編號系統,它不使用0到9,而是使用0到9以及a到f作為基本數字。

最後,讓我們回到最開始的地方,也就是那些隨機的x字節序列。希望現在看起來這些序列會顯得更有意義一些。在bytes對象上調用.hex可以得到一個十六進制數字的str,對應於從0到255的一個十進制數:

在Python中生成随机数据(指南)

最後一個問題:問什麼上面b.hex是十二個字符長,即便x只有6個字節?這是因為兩個十六進制數字剛好可以表示一個字節。字節的str版本總是我們眼睛所看到的兩倍長:

即使字節(比如\\x01)不需要完整的8位來表示,b.hex也總是使用兩個十六進制數字表示每個字節,因此數字1將被表示為01而不僅僅是1。然而,從數學上講,這兩個數的大小是相同的。

技術細節:這裡主要分析的是字節對象如何變成Python str。一個技術性問題是os.urandom生成的字節如何轉換為區間[0.0,1.0]中的浮點數,就像random.random的密碼學安全版本一樣。


有了這些,讓我們瞭解一下最近引入的secrets模塊,它使生成安全令牌的用戶體驗變得更加友好。

Python中保密的最好方法:secrets

secrets模塊是由Pyhton3.6中的PEPs所引入,試圖成為Python3.6中用於產生密碼學安全的隨機字節與字符串的標準模塊。

你可以查看該模塊的源代碼,非常的精簡,只有25行。secrets基本上就是os.urandom的封裝。它只導出少量用於生成隨機數、字節和字符串的函數。下面這些例子中的大多數都應該是不言自明的:

在Python中生成随机数据(指南)

現在,舉個具體的例子?您可能使用過URL壓縮服務,比如tinyurl.com或bit。把一個笨重的URL變成類似於https://bit.ly/2IcCp9u的東西。大多數壓縮者並不做任何從輸入到輸出的複雜哈希;它們只是生成一個隨機的字符串,並確保這個字符串之前沒有生成過,然後將其與輸入URL綁定。

假設在查看了Root Zone Database之後,您已經註冊了站點short.ly。這裡有一個函數讓您開始您的服務:

在Python中生成随机数据(指南)

這是一個成熟的真實例子嗎?不。我敢打賭biy.ly所做的事情要比將數據存儲在全局Python字典(在會話之間非持續)中稍微高級一些。然而,它在概念上大致準確:

在Python中生成随机数据(指南)

稍等:您可能會注意到,當您請求5個字節時,這兩個結果的長度都是7。等等,我記得你說過結果會是原來的兩倍?好吧,在這個例子中,不完全是這樣。在這個例子中:token_urlsafe使用base64編碼,其中每個字符都是6位數據。(從0到63以及相應的字符。字符是A-Z、A-Z、0-9和+/。)


如果您最開始指定了一定數量的字節nbytes,那麼secrets.token_urlsafe(nbytes)結果的長度將與math.ceil(nbytes * 8 / 6)相同。如果您感到好奇,那麼您可以進一步證明和研究它。

最後向您介紹:uuid

生成隨機令牌的最後一個選擇是Python中uuid模塊中的uuid4函數。UUID是一個全局唯一的標識符,是一個128位序列(str長度32),旨在“保證跨時空的唯一性”。uuid4是模塊中最有用的函數之一,該函數也使用了os.urandom:

在Python中生成随机数据(指南)

有一個好處是uuid所有函數都會生成uuid類的一個實例,它封裝了ID,並具有.int、.bytes和.hex等屬性:

在Python中生成随机数据(指南)

您可能還看到了其他一些變體:uuid1、uuid3和uuid5。它們與uuid4的關鍵區別在於,這三個函數都採用某種形式的輸入,因此不符合“隨機”的定義,其隨機程度與版本4的UUID不同:

  • uuid1默認使用機器的主機ID和當前時間。由於對當前時間擁有直到納秒分辨率的依賴,這個版本UUID“保證跨時間的唯一性”。

  • uuid3和uuid5都採用命名空間標識符和名稱。前者使用MD5散列,後者使用SHA-1。

相反,uuid4完全是偽隨機(或者說隨機)的。它通過os.urandom獲取16個字節,將其轉換為一個big-endian整數,並執行一些按位操作以符合格式規範。

現在,希望您對不同“類型”的隨機數據之間的區別以及如何創建它們有了一個很好的認識。然而,另一個可能會想到的問題就是碰撞。

在本例中,衝突只是指生成兩個相同的UUIDs。這個概率是多少?嗯,從技術上講,它不是零,但也許它已經足夠接近了:有2 ** 128或340個undecillion可能的uuid4值。所以,你還是自己來判斷吧,以保證你能睡得好。

uuid常常用在在Django中,Django有一個UUIDField,它通常用作模型底層關係數據庫中的主鍵。

為什麼不直接“默認使用” SystemRandom呢?

除了這裡討論的安全模塊(如secrets)之外,Python的random模塊實際上還有一個很少使用的SystemRandom類,它使用的是os.urandom。(SystemRandom,反過來也被用於secrets。這是一個都可以追溯到urandom的網絡。)

此時,您可能會問自己為什麼不“默認使用”這個版本?為什麼不使用“始終是安全的”,而默認使用非密碼學安全的確定性隨機函數呢?

我已經提到了一個原因:有時候,您希望您的數據是確定性的,並且可以被其他人跟蹤。

但是第二個原因是,至少在Python中,CSPRNGs比PRNGs慢得多。讓我們用timed.py腳本來測試它,使用Python的time.repeat比較randint的PRNG和CSPRNG版本:

現在在shell中執行這個腳本:

在Python中生成随机数据(指南)

在兩者之間進行選擇時,除了加密安全性外,5倍的時序差異當然是一個有效的考慮因素。

結尾再說一下:哈希

本教程中沒有提到的一個概念是哈希,它可以用Python的hashlib模塊完成。

哈希被設計成從輸入值到固定大小的字符串的單向映射,這幾乎不可能進行反向工程。因此,雖然哈希函數的結果可能“看起來”像隨機數據,但在這裡的定義下,它實際上並不合格。

回顧

在本教程中,已經向您介紹了很多方面。簡單回顧一下,這裡有一個對Python中工程隨機性可用選項的高級比較:

在Python中生成随机数据(指南)

請在下面隨機留下一些評論,感謝閱讀。

英文原文:https://realpython.com/python-random/
譯者:搞一個大新聞


分享到:


相關文章: