在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最初被定义为种子值,随后根据该种子产生出一个确定的数字序列:

不要太在意这个例子的细节,因为它主要是为了说明概念。如果使用种子值1234,那么后续调用random()所产生的序列应该始终相同:

很快您将会看到一个更严谨的例子。

什么是“密码学安全”?

你可能还没弄熟“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),这也被称为半开区间。

如果您自己运行这段代码,我敢打赌,您机器上返回的数字会不一样。默认情况下,当您没有设置随机数种子时,将使用当前系统时间或操作系统的“随机源”(如果有的话)作为随机数种子。

通过random.seed()您可以使结果具有可重复性,在此之后连续调用将会产生相同的数据。

注意重复的“随机”数字。随机数序列变为确定性的,或者说完全由随机数种子444确定。

让我们再来看看random的一些更基本的功能。上文中,您生成了一个随机的浮点数。您可以在Python中使用random.randint()函数在两个端点之间生成一个随机整数。随机数生成范围跨越整个[x, y]区间,可能包括两个端点:

使用random.randrange(),您可以将区间右端排除掉,也就是说产生的随机数总是处于[x,y)之间并且总是小于右端点。

如果您需要生成位于特定区间[x,y]之间的浮点数,您可以使用random.uniform()函数,该函数产生的随机数符合连续均匀分布:

您可以使用random.choice()函数从非空序列(如列表或元组)中选择随机元素。同时也可以使用random.choices()用于从序列中可放回(可以重复)地选择多个元素:

在不替换的情况下模拟采样,请使用random.sample()函数:

你可以使用random.shuffle()函数在原位置随机化一个序列。这将会修改序列对象并随机化元素的位置:

如果您不希望更改原始列表,那么您需要先创建一个副本,然后对副本执行shuffle操作。您可以使用copy模块创建Python列表的副本,或者使用x[:]或x.copy(),其中x是列表。

在继续使用NumPy生成随机数据之前,让我们先看一个稍微复杂一点的应用程序:生成一个具有统一长度的唯一随机字符串序列。

这可以帮助您思考函数设计的方法。您需要从一个字符“池”中选择如字母、数字和/或标点符号,将它们组合成一个字符串,然后再检查这个字符串是否已经生成过。Python中的set可以很好的用于这种成员关系测试:

''.join()将random.choices()所选取的字母连结成一个长度为k的python str。该token被添加到不能包含重复元素的集合中,while循环不断执行直到集合中的元素达到您指定好的数量。

资源:Python的string模块包含了一些有用的常量:ascii_lowercase, ascii_uppercase, string.punctuation, ascii_whitespace以及少数一些其他的东西。

让我们来试试这个函数:

Stack Overflow上有本函数的调整版。它使用了生成器函数,名称绑定以及一些其他的高级技巧创造了一个上面的unique_strings()的更快的,密码安全版本。

对于数组的PRNGs:numpy.random

您可能已经注意到,random中的大多数随机函数返回一个标量值(单个int、float或其他对象)。如果你想要生成一个随机数序列,有一种方法是使用Python列表生成式:

还有另一种选择是专门为此设计的。你可以把NumPy自己的numpy.random包看成类似于标准库的random包,但它是用于处理NumPy数组的。(它还具有从更多统计分布中提取数据的能力。)

注意,numpy.random使用自己的PRNG,它与普通的random不同。调用Python自己的random.seed()不会生成确定性的随机NumPy数组:

闲话少说,下面有几个例子来激发你的胃口:

如何生成相关数据?假设你想要模拟两个相关的时间序列。一种方法是使用NumPy的multivariate_normal()函数,该函数使用了协方差矩阵。换句话说,要从单个正态分布随机变量中生成数据,需要指定其均值和方差(或标准差)。

要从多元正态分布中进行采样,您需要指定均值和协方差矩阵,最后可以得到多个相关的数据序列,每个数据序列大致呈正态分布。

然而,相较于协方差,相关性是更为大多数人所熟悉并直观的度量标准。它是由标准差的乘积归一化的协方差,因此您还可以根据相关性和标准差来定义协方差:

那么,可以通过指定一个相关矩阵和标准差从一个多元正态分布中抽取随机样本吗? 答案是可以的,但是你需要先把上面的公式变成矩阵形式。这里,S是标准差的向量,P是它们的相关矩阵,C是结果(平方)协方差矩阵:

用Numpy表示如下:

现在,您可以生成两个相关的但是仍然随机的时间序列:

在开始介绍CSPRNGs之前,总结一下一些random以及它们在numpy.random中对应的函数应该会很有帮助:

注意: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()中,没有手动设置随机数种子的概念。虽然在技术上仍然是伪随机,但这个函数更符合我们对随机性的看法。唯一的参数是要返回的字节数:

os.urandom()返回单个字节组成的序列:

但是,这些东西最终是如何变成Python的str或数字序列呢?

首先,回忆一下计算的一个基本概念,即一个字节由8 bit组成。你可以把这些bit看作是一个个不是0就是1的位。一个字节有效地在0和1之间选择8次,所以01101100和11110000都可以表示字节。尝试下面的例子,它使用了Python 3.6中引入的Python f-string:

这相当于[bin(i) for i in range(256)],同时还带有特定的格式。bin()函数将一个整数转换成用字符串表示的二进制形式。

上述例子说明什么问题呢?上面的例子并不是随机选择使用range(256)的。每个字节有8位,每位有两种选择,则一共有2 ** 8 == 256种字节“组合”。

这意味着每个字节被映射到一个0到255之间的整数。换句话说,为了表示整数256,我们需要更多的位数。你可以通过 len(f'')为9而不是8来验证这一点。

好的,现在让我们回到您在上面看到的字节数据类型,通过构造一个字节序列,从而对应0到255的整数:

如果您调用list(bites),您将得到一个从0到255的Python列表。但如果你只打印bites,你就只会得到一个散落着反斜杠的难看的序列:

这些反斜杠是转义字符,\xhh表示十六进制值为hh的字符。bites中的一些元素按字面量显示(可打印的字符,如字母、数字和标点符号)。大多数则是用转义字符表示的。\x08表示键盘的退格,而\x13表示回车(在Windows系统上是新行的一部分)。

如果您需要复习一下十六进制,那么Charles petzold的Code:The Hidden Language是一个不错的选择。十六进制是一种基本的编号系统,它不使用0到9,而是使用0到9以及a到f作为基本数字。

最后,让我们回到最开始的地方,也就是那些随机的x字节序列。希望现在看起来这些序列会显得更有意义一些。在bytes对象上调用.hex()可以得到一个十六进制数字的str,对应于从0到255的一个十进制数:

最后一个问题:问什么上面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()的封装。它只导出少量用于生成随机数、字节和字符串的函数。下面这些例子中的大多数都应该是不言自明的:

现在,举个具体的例子?您可能使用过URL压缩服务,比如tinyurl.com或bit。把一个笨重的URL变成类似于https://bit.ly/2IcCp9u的东西。大多数压缩者并不做任何从输入到输出的复杂哈希;它们只是生成一个随机的字符串,并确保这个字符串之前没有生成过,然后将其与输入URL绑定。

假设在查看了Root Zone Database之后,您已经注册了站点short.ly。这里有一个函数让您开始您的服务:

这是一个成熟的真实例子吗?不。我敢打赌biy.ly所做的事情要比将数据存储在全局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():

有一个好处是uuid所有函数都会生成uuid类的一个实例,它封装了ID,并具有.int、.bytes和.hex等属性:

您可能还看到了其他一些变体: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中执行这个脚本:

在两者之间进行选择时,除了加密安全性外,5倍的时序差异当然是一个有效的考虑因素。

结尾再说一下:哈希

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

哈希被设计成从输入值到固定大小的字符串的单向映射,这几乎不可能进行反向工程。因此,虽然哈希函数的结果可能“看起来”像随机数据,但在这里的定义下,它实际上并不合格。

回顾

在本教程中,已经向您介绍了很多方面。简单回顾一下,这里有一个对Python中工程随机性可用选项的高级比较:

请在下面随机留下一些评论,感谢阅读。

英文原文:https://realpython.com/python-random/

译者:搞一个大新闻

  • 发表于:
  • 原文链接:https://kuaibao.qq.com/s/20180829A0A7O300?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券