random --- 產生偽隨機數

原始碼:Lib/random.py


本章中所提及的 module(模組)用來實現各種分佈的虛擬隨機數產生器。

對於整數,可以從範圍中進行均勻選擇。對於序列,有一個隨機元素的均勻選擇,一個用來原地 (in-place) 產生隨機排列清單的函式,以及一個用來隨機取樣不重置的函式。

在實數線上,有一些函式用於處理均勻分佈、常態分佈(高斯分佈)、對數常態分佈、負指數分佈、gamma 分佈和 Beta 分佈。對於產生角度分佈,可以使用馮·米塞斯分佈 (von Mises distribution)。

幾乎所有 module 函式都相依於基本函式 random(),此函式在半開放範圍 0.0 <= X < 1.0 內均勻地產生一個隨機 float(浮點數)。Python 使用 Mersenne Twister(梅森旋轉演算法)作為核心的產生器,它產生 53 位元精度 float,其週期為 2**19937-1,透過 C 語言進行底層的實作既快速又支援執行緒安全 (threadsafe)。Mersenne Twister 是現存最廣泛被驗證的隨機數產生器之一,但是基於完全確定性,它並不適合所有目的,並且完全不適合加密目的。

該 module 提供的函式實際上是 random.Random class(類別)中一個隱藏實例的綁定方法 (bound method)。你可以實例化自己的 Random 實例,以得到不共享狀態的產生器。

如果你想使用你自己設計的基本產生器,Random 也可以進行子類別化 (subclass)。有關詳細資訊,請參考該類別的文件。

random module 也提供了 SystemRandom class,使用系統函式 os.urandom() 從作業系統提供的來源產生隨機數。

警告

本章所提及的虛擬隨機數產生器不應該使用於安全目的。有關安全性或加密用途,請參考 secrets module。

也參考

M. Matsumoto and T. Nishimura, "Mersenne Twister: A 623-dimensionally equidistributed uniform pseudorandom number generator", ACM Transactions on Modeling and Computer Simulation Vol. 8, No. 1, January pp.3--30 1998.

進位互補乘法 (Complementary-Multiply-with-Carry) 用法,可作為隨機數產生器的一個可相容替代方案,具有較長的週期和相對簡單的更新操作。

備註

全域隨機數產生器和 Random 的實例是執行緒安全的。然而,在自由執行緒構建中,對全域產生器或同一 Random 實例的並行呼叫可能會遇到爭用和性能不佳。請考慮改為每個執行緒使用單獨的 Random 實例。

簿記函式 (bookkeeping functions)

random.seed(a=None, version=2)

初始化隨機數產生器。

如果 a 被省略或為 None,則使用目前系統時間。如果隨機來源由作業系統提供,則使用它們而不是系統時間(有關可用性的詳細資訊,請參考 os.urandom() 函式)。

如果 a 為 int(整數),則直接使用它。

如使用版本 2(預設值),strbytesbytearray 物件將轉換為 int,並使用其所有位元。

若使用版本 1(為復現於舊版本 Python 中產生隨機序列而提供),strbytes 的演算法會產生範圍更窄的種子 (seed)。

在 3.2 版的變更: 移至版本 2 方案,該方案使用字串種子中的所有位元。

在 3.11 版的變更: seed 必須是以下型別之一:Noneintfloatstrbytesbytearray

random.getstate()

回傳一個物件,捕獲產生器的目前內部狀態。此物件可以傳遞給 setstate() 以恢復狀態。

random.setstate(state)

state 應該要從之前對 getstate() 的呼叫中獲得,並且以 setstate() 將產生器的內部狀態恢復到呼叫 getstate() 時的狀態。

回傳位元組的函式

random.randbytes(n)

產生 n 個隨機位元組。

此方法不應使用於產生安全性權杖 (Token)。請改用 secrets.token_bytes()

在 3.9 版被加入.

回傳整數的函式

random.randrange(stop)
random.randrange(start, stop[, step])

傳回從 range(start, stop, step) 中隨機選擇的元素。

這大致相當於 choice(range(start, stop, step)),但支援任意大的範圍,並針對常見情況進行了最佳化。

位置引數模式與 range() 函式相符。

不應使用關鍵字引數,因為它們可能會以意想不到的方式被直譯。例如 randrange(start=100) 會被直譯為 randrange(0, 100, 1)

在 3.2 版的變更: randrange() 在產生均勻分佈的值方面更為複雜。以前,它使用像 int(random()*n) 這樣的樣式,這可能會產生稍微不均勻的分佈。

在 3.12 版的變更: 已經不再支援非整數類型到等效整數的自動轉換。像是 randrange(10.0)randrange(Fraction(10, 1)) 的呼叫將會引發 TypeError

random.randint(a, b)

回傳一個隨機整數 N,使得 a <= N <= b。為 randrange(a, b+1) 的別名。

random.getrandbits(k)

回傳一個具有 k 個隨機位元的非負 Python 整數。此方法會隨 Mersenne Twister 產生器一起提供,一些其他的產生器也可能將其作為 API 的可選部分。如果可用,getrandbits() 使 randrange() 能夠處理任意大的範圍。

在 3.9 版的變更: 此方法現在接受 k 為零。

回傳序列的函式

random.choice(seq)

從非空序列 seq 回傳一個隨機元素。如果 seq 為空,則引發 IndexError

random.choices(population, weights=None, *, cum_weights=None, k=1)

回傳從 population 中重置取樣出的一個大小為 k 的元素 list。如果 population 為空,則引發 IndexError

如果指定了 weights 序列,則根據相對權重進行選擇。另外,如果給定 cum_weights 序列,則根據累積權重進行選擇(可能使用 itertools.accumulate() 計算)。例如,相對權重 [10, 5, 30, 5] 等同於累積權重 [10, 15, 45, 50]。在內部,相對權重在進行選擇之前會轉換為累積權重,因此提供累積權重可以節省工作。

如果既未指定 weights 也未指定 cum_weights,則以相等的機率進行選擇。如果提供了加權序列,則該序列的長度必須與 population 序列的長度相同。它是一個 TypeError 來指定 weightscum_weights

weightscum_weights 可以使用任何與 random() 所回傳的 float 值(包括整數、float 和分數,但不包括小數)交互操作 (interoperates) 的數值類型。權重假定為非負數和有限的。如果所有權重均為零,則引發 ValueError

對於給定的種子,具有相等權重的 choices() 函式通常產生與重複呼叫 choice() 不同的序列。choices() 使用的演算法使用浮點運算來實現內部一致性和速度。choice() 使用的演算法預設為整數運算和重複選擇,以避免捨入誤差產生的小偏差。

在 3.6 版被加入.

在 3.9 版的變更: 如果所有權重均為零,則引發 ValueError

random.shuffle(x)

將序列 x 原地 (in place) 隨機打亂位置。

要打亂一個不可變的序列並回傳一個新的被打亂的 list(串列),請使用 sample(x, k=len(x))

請注意,即使對於較小的 len(x)x 的置換總數也會快速成長到大於大多數隨機數產生器的週期。這意味著長序列的大多數置換永遠無法產生。例如,長度為 2080 的序列是 Mersenne Twister 隨機數產生器週期內可以容納的最大序列。

在 3.11 版的變更: 移除可選參數 random

random.sample(population, k, *, counts=None)

回傳從母體序列中選擇出的一個包含獨特元素、長度為 k 的 list。用於不重置的隨機取樣。

回傳包含母體元素的新清單,同時保持原始母體不變。產生的清單按選擇順序排列,因此所有子切片也會是有效的隨機樣本。這允許抽獎獲獎者(樣本)分為大獎和第二名獲獎者(子切片)。

母體成員不必是 hashable 或唯一的。如果母體包含重複項,則每次出現都是樣本中可能出現的一個選擇。

可以一次指定一個重複元素,也可以使用可選的僅關鍵字 counts 參數指定重複元素。例如 sample(['red', 'blue'], counts=[4, 2], k=5) 等同於 sample(['red', 'red', 'red', 'red', 'blue', 'blue'], k=5)

若要從整數範圍中選擇範例,請使用 range() 物件作為引數。這對於從大型母體中取樣特別快速且節省空間:sample(range(10000000), k=60)

如果樣本大小大於母體大小,ValueError 會被引發。

在 3.9 版的變更: 新增 counts 參數。

在 3.11 版的變更: population 必須是一個序列。不再支援將 set 自動轉換為 list。

離散分布

以下函式產生離散分佈。

random.binomialvariate(n=1, p=0.5)

二項分佈 (Binomial distribution)。回傳 n 個獨立試驗的成功次數,每個試驗的成功機率為 p

數學上等價於:

sum(random() < p for i in range(n))

試驗次數 n 應為非負整數。成功的機率 p 應在 0.0 <= p <= 1.0 之間。結果是 0 <= X <= n 範圍內的整數。

在 3.12 版被加入.

實數分布

以下函式產生特定的實數分佈。函式參數以分佈方程中的對應變數命名,如常見的數學實踐所示;這些方程式中的大多數都可以在任意統計文本中找到。

random.random()

回傳範圍 0.0 <= X < 1.0 中的下一個隨機浮點數

random.uniform(a, b)

回傳一個隨機浮點數 N,當 a <= b 時確保 N 為 a <= N <= bb < a 時確保 N 為 b <= N <= a

終點值 b 可能包含在範圍內,也可能不包含在範圍內,取決於運算式 a + (b-a) * random() 中的浮點捨入。

random.triangular(low, high, mode)

回傳一個隨機浮點數 N,使得 low <= N <= high,並在這些邊界之間具有指定的 modelowhigh 邊界預設為零和一。mode 引數預設為邊界之間的中點,從而給出對稱分佈。

random.betavariate(alpha, beta)

Beta(貝它)分布。參數的條件為 alpha > 0beta > 0。回傳值的範圍介於 0 和 1 之間。

random.expovariate(lambd=1.0)

指數分佈。lambd 為 1.0 除以所需的平均數。它應該不為零。(該參數將被稱為 "lambda",但這是 Python 中的保留字)如果 lambd 為正,則回傳值的範圍從 0 到正無窮大;如果 lambd 為負,則回傳值的範圍從負無窮大到 0。

在 3.12 版的變更: 新增 lambd 的預設值。

random.gammavariate(alpha, beta)

Gamma(伽瑪)分佈。(不是 Gamma 函式!)。形狀 (shape) 和比例 (scale) 參數 alphabeta 必須具有正值。(根據呼叫習慣不同,部分來源會將 'beta' 定義為比例的倒數)。

Probability distribution function(機率密度函式)是:

          x ** (alpha - 1) * math.exp(-x / beta)
pdf(x) =  --------------------------------------
            math.gamma(alpha) * beta ** alpha
random.gauss(mu=0.0, sigma=1.0)

常態分佈,也稱為高斯分佈。mu 是平均數,sigma 是標準差。這比下面定義的 normalvariate() 函式快一點。

多執行緒須注意:當兩個執行緒同時呼叫此函式時,它們可能會收到相同的傳回值。這可以透過三種方式避免。1)讓每個執行緒使用隨機數產生器的不同實例。2)在所有呼叫周圍加鎖。3)使用較慢但執行緒安全的 normalvariate() 函式代替。

在 3.11 版的變更: musigma 現在有預設引數。

random.lognormvariate(mu, sigma)

對數常態分佈。如果你取此分佈的自然對數,你將獲得一個具有平均數 mu 和標準差 sigma 的常態分佈。mu 可以為任何值,並且 sigma 必須大於零。

random.normalvariate(mu=0.0, sigma=1.0)

常態分佈。mu 是平均數,sigma 是標準差。

在 3.11 版的變更: musigma 現在有預設引數。

random.vonmisesvariate(mu, kappa)

mu 是平均角度,以 0 到 2*pi 之間的弧度表示,kappa 是濃度參數,必須大於或等於零。如果 kappa 等於零,則此分佈在 0 到 2*pi 的範圍內將減小為均勻的隨機角度。

random.paretovariate(alpha)

Pareto distribution(柏拉圖分佈)。alpha 是形狀參數。

random.weibullvariate(alpha, beta)

Weibull distribution(韋伯分佈)。alpha 是比例參數,beta 是形狀參數。

替代產生器

class random.Random([seed])

實現 random 模組使用的預設偽隨機數產生器的 class。

在 3.11 版的變更: 過去 seed 可以是任何可雜湊物件,但現在必須是以下類型之一:Noneintfloatstrbytesbytearray

如果 Random 的子類別希望使用不同的基礎產生器,則應該覆寫以下方法:

seed(a=None, version=2)

在子類別中覆寫此方法以自訂 Random 實例的 seed() 行為。

getstate()

在子類別中覆寫此方法以自訂 Random 實例的 getstate() 行為。

setstate(state)

在子類別中覆寫此方法以自訂 Random 實例的 setstate() 行為。

random()

在子類別中覆寫此方法以自訂 Random 實例的 random() 行為。

或者,自訂產生器子類別還可以提供以下方法:

getrandbits(k)

在子類別中覆寫此方法以自訂 Random 實例的 getrandbits() 行為。

randbytes(n)

在子類別中覆寫此方法以自訂 Random 實例的 randbytes() 行為。

class random.SystemRandom([seed])

使用 os.urandom() 函式從作業系統提供的來源產生隨機數的 Class。並非在所有系統上都可用。不依賴於軟體狀態,並且序列不可復現。因此 seed() 方法沒有效果且被忽略。如果呼叫 getstate()setstate() 方法會引發 NotImplementedError

關於 Reproducibility(復現性)的注意事項

有時,能夠重現偽隨機數產生器給出的序列很有用。只要多執行緒未運行,透過重複使用種子值,同一序列就應該可以被復現。

大多數隨機 module 的演算法和 seed 設定函式在 Python 版本中可能會發生變化,但可以保證兩個方面不會改變:

  • 如果增加了新的 seed 設定函式,則將提供向後相容的播種器 (seeder)。

  • 當相容的播種器被賦予相同的種子時,產生器的 random() 方法將持續產生相同的序列。

範例

基礎範例:

>>> random()                          # 隨機浮點數:0.0 <= x < 1.0
0.37444887175646646

>>> uniform(2.5, 10.0)                # 隨機浮點數:2.5 <= x <= 10.0
3.1800146073117523

>>> expovariate(1 / 5)                # 到達間隔平均為 5 秒
5.148957571865031

>>> randrange(10)                     # 0 到 9(含)的整數
7\n

>>> randrange(0, 101, 2)              # 0 到 100(含)的偶數整數
26\n

>>> choice(['win', 'lose', 'draw'])   # 從序列中隨機選取單一元素
'draw'

>>> deck = 'ace two three four'.split()
>>> shuffle(deck)                     # 打亂串列
>>> deck
['four', 'two', 'ace', 'three']

>>> sample([10, 20, 30, 40, 50], k=4) # 四個無重置的樣本
[40, 10, 50, 30]

模擬:

>>> # 六次輪盤旋轉(有重置的加權抽樣)
>>> choices(['red', 'black', 'green'], [18, 18, 2], k=6)
['red', 'green', 'black', 'black', 'red', 'black']

>>> # 從一副 52 張撲克牌中無重置發 20 張牌
>>> # 並確定具有十點值的牌的比例:十、傑克、王后或國王
>>> deal = sample(['tens', 'low cards'], counts=[16, 36], k=20)
>>> deal.count('tens') / 20
0.15

>>> # 估計從 7 次旋轉中得到 5 次或更多正面的概率
>>> # 使用一枚 60% 概率為正面的偏幣
>>> sum(binomialvariate(n=7, p=0.6) >= 5 for i in range(10_000)) / 10_000
0.4169

>>> # 計算 5 個樣本的中位數位於中間兩個四分位數的概率
>>> def trial():
...     return 2_500 <= sorted(choices(range(10_000), k=5))[2] < 7_500
...
>>> sum(trial() for i in range(10_000)) / 10_000
0.7958

統計 bootstrapping(自助法)的範例,使用有重置的重新取樣來估計樣本平均數的信賴區間:

# https://www.thoughtco.com/example-of-bootstrapping-3126155
from statistics import fmean as mean
from random import choices

data = [41, 50, 29, 37, 81, 30, 73, 63, 20, 35, 68, 22, 60, 31, 95]
means = sorted(mean(choices(data, k=len(data))) for i in range(100))
print(f'The sample mean of {mean(data):.1f} has a 90% confidence '
      f'interval from {means[5]:.1f} to {means[94]:.1f}')

重新取樣排列測試的範例,來確定觀察到的藥物與安慰劑之間差異的統計學意義或 p 值

# 範例來自 Dennis Shasha 和 Manda Wilson 的 "Statistics is Easy"
from statistics import fmean as mean
from random import shuffle

drug = [54, 73, 53, 70, 73, 68, 52, 65, 65]
placebo = [54, 51, 58, 44, 55, 52, 42, 47, 58, 46]
observed_diff = mean(drug) - mean(placebo)

n = 10_000
count = 0
combined = drug + placebo
for i in range(n):
    shuffle(combined)
    new_diff = mean(combined[:len(drug)]) - mean(combined[len(drug):])
    count += (new_diff >= observed_diff)

print(f'{n} 次標籤重新洗牌僅產生 {count} 個差異實例')
print(f'至少與觀察到的差異 {observed_diff:.1f} 一樣極端。')
print(f'單邊 p 值 {count / n:.4f} 使我們拒絕無效假設')
print(f'即藥物與安慰劑之間沒有差異。')

模擬多伺服器佇列 (queue) 的到達時間與服務交付:

from heapq import heapify, heapreplace
from random import expovariate, gauss
from statistics import mean, quantiles

average_arrival_interval = 5.6
average_service_time = 15.0
stdev_service_time = 3.5
num_servers = 3

waits = []
arrival_time = 0.0
servers = [0.0] * num_servers  # 每個伺服器可用的時間
heapify(servers)
for i in range(1_000_000):
    arrival_time += expovariate(1.0 / average_arrival_interval)
    next_server_available = servers[0]
    wait = max(0.0, next_server_available - arrival_time)
    waits.append(wait)
    service_duration = max(0.0, gauss(average_service_time, stdev_service_time))
    service_completed = arrival_time + wait + service_duration
    heapreplace(servers, service_completed)

print(f'平均等待時間:{mean(waits):.1f}   最大等待時間:{max(waits):.1f}')
print('四分位數:', [round(q, 1) for q in quantiles(waits)])

也參考

Statistics for Hackers 是由 Jake Vanderplas 製作的教學影片,僅使用幾個基本概念(包括模擬、取樣、洗牌、交叉驗證)進行統計分析。

Economics Simulation是由 Peter Norvig 對市場進行的模擬,顯示了該模組提供的許多工具和分佈(高斯、均勻、樣本、 beta 變數、選擇,三角形、隨機數)的有效使用。

機率的具體介紹(使用Python)Peter Norvig 的教學課程,涵蓋了機率理論的基礎知識與如何模擬以及使用 Python 執行數據分析。

使用方案

These recipes show how to efficiently make random selections from the combinatoric iterators in the itertools module or the more-itertools project:

def random_product(*args, repeat=1):
    "Random selection from itertools.product(*args, **kwds)"
    pools = [tuple(pool) for pool in args] * repeat
    return tuple(map(random.choice, pools))

def random_permutation(iterable, r=None):
    "Random selection from itertools.permutations(iterable, r)"
    pool = tuple(iterable)
    r = len(pool) if r is None else r
    return tuple(random.sample(pool, r))

def random_combination(iterable, r):
    "Random selection from itertools.combinations(iterable, r)"
    pool = tuple(iterable)
    n = len(pool)
    indices = sorted(random.sample(range(n), r))
    return tuple(pool[i] for i in indices)

def random_combination_with_replacement(iterable, r):
    "Choose r elements with replacement.  Order the result to match the iterable."
    # Result will be in set(itertools.combinations_with_replacement(iterable, r)).
    pool = tuple(iterable)
    n = len(pool)
    indices = sorted(random.choices(range(n), k=r))
    return tuple(pool[i] for i in indices)

def random_derangement(iterable):
    "Choose a permutation where no element is in its original position."
    seq = tuple(iterable)
    if len(seq) < 2:
        raise ValueError('derangments require at least two values')
    perm = list(seq)
    while True:
        random.shuffle(perm)
        if all(p != q for p, q in zip(seq, perm)):
            return tuple(perm)

預設的 random() 回傳 0.0 ≤ x < 1.0 範圍內 2⁻⁵³ 的倍數。所有數字都是均勻分佈的,並且可以完全表示為 Python float。但是,該間隔中的許多其他可表示的 float 不是可能的選擇。 例如 0.05954861408025609 不是 2⁻⁵³ 的整數倍。

以下範例採用不同的方法。間隔中的所有 float 都是可能的選擇。尾數來自 2⁵² ≤ 尾數 < 2⁵³ 範圍內的整數均勻分佈。指數來自幾何分佈,其中小於 -53 的指數的出現頻率是下一個較大指數的一半。

from random import Random
from math import ldexp

class FullRandom(Random):

    def random(self):
        mantissa = 0x10_0000_0000_0000 | self.getrandbits(52)
        exponent = -53
        x = 0
        while not x:
            x = self.getrandbits(32)
            exponent += x.bit_length() - 32
        return ldexp(mantissa, exponent)

Class 中的所有實數分佈都將使用新方法:

>>> fr = FullRandom()
>>> fr.random()
0.05954861408025609
>>> fr.expovariate(0.25)
8.87925541791544

該範例在概念上等效於一種演算法,該演算法從 0.0 ≤ x < 1.0 範圍內 2⁻¹⁰⁷⁴ 的所有倍數中進行選擇。這些數字都是均勻分佈的,但大多數必須向下捨入到最接近的可表示的 Python float。(2⁻¹⁰⁷⁴ 是最小為正的非正規化 float,等於 math.ulp(0.0)

也參考

產生偽隨機浮點值 Allen B. Downey 的一篇論文描述了產生比通常由 random() 產生的 float 更 fine-grained(細粒的)的方法。

命令列用法

在 3.13 版被加入.

random 模組可以從命令列執行。

python -m random [-h] [-c CHOICE [CHOICE ...] | -i N | -f N] [input ...]

接受以下選項:

-h, --help

顯示幫助訊息並退出。

-c CHOICE [CHOICE ...]
--choice CHOICE [CHOICE ...]

列印一個隨機選擇,使用 choice()

-i <N>
--integer <N>

列印 1 到 N(含)之間的隨機整數,使用 randint()

-f <N>
--float <N>

列印 0 到 N(含)之間的隨機浮點數,使用 uniform()

如果未給定選項,則輸出取決於輸入:

命令列範例

以下是 random 命令列介面的一些範例:

$ # 隨機選擇一個
$ python -m random egg bacon sausage spam "Lobster Thermidor aux crevettes with a Mornay sauce"
Lobster Thermidor aux crevettes with a Mornay sauce

$ # 隨機整數
$ python -m random 6
6

$ # 隨機浮點數
$ python -m random 1.8
1.7080016272295635

$ # 使用明確的引數
$ python  -m random --choice egg bacon sausage spam "Lobster Thermidor aux crevettes with a Mornay sauce"
egg

$ python -m random --integer 6
3

$ python -m random --float 1.8
1.5666339105010318

$ python -m random --integer 6
5

$ python -m random --float 6
3.1942323316565915