Python中的random.random()函数是如何工作的?

10

我对Python中random.random()函数的工作方式感到有些困惑。

文档中说它“返回范围在[0.0, 1.0)之间的下一个随机浮点数”。我理解伪随机数生成器是通过对值执行某些操作来工作的。通常,这个值是由生成器生成的前一个数。所以我认为这里的“下一个随机浮点数”就是表示下一个被生成的随机数。(如果我理解有误,请指出)

但是当我看到random库的源代码时,我发现并没有在class Random中定义random函数。相反,在class SystemRandom中定义了它,代码的第671行如下:

 def random(self):
        """Get the next random number in the range [0.0, 1.0)."""
        return (int.from_bytes(_urandom(7), 'big') >> 3) * RECIP_BPF

如果我理解正确的话,这个函数使用os.urandom生成随机数。根据文档,该函数从特定于操作系统的随机源返回随机字节。因此,它不会给出“下一个”浮点随机数。
两者有什么联系?还是说它们是两个不同的东西?
我感到相当困惑。任何帮助都将不胜感激。
谢谢!
4个回答

22
Python中的random模块包含两个接口(类)的伪随机数生成器(PRNGs)。您可以将其视为生成随机数的两种方式。

A note on the module secrets.

The module secrets does not implement any type of PRNG but provides helper functions(which is awesome because we don't have to write them ourselves) based on SystemRandom and os.urandom(which SystemRandom is based upon). The comments are mine:

from random import SystemRandom
_sysrand = SystemRandom() #secrets._sysrand
randbits = _sysrand.getrandbits #secrets.randbits
choice = _sysrand.choice #secrets.choice

def randbelow(exclusive_upper_bound): #secrets.randbelow
    ...
    return _sysrand._randbelow(exclusive_upper_bound) #uses SystemRandom

def token_bytes(nbytes=None): #secrets.token_bytes
    ...
    return os.urandom(nbytes)

def token_hex(nbytes=None): #secrets.token_hex(uses token_bytes())
    ...
    return binascii.hexlify(token_bytes(nbytes)).decode('ascii')

def token_urlsafe(nbytes=None): # secrets.token_urlsafe(uses token_bytes())
    ...
    tok = token_bytes(nbytes)
    return base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii')

Random.random()是如何工作的?

random.random()在'random.py'模块中的第749行(对我而言)进行定义。

_inst = Random()
...
random = _inst.random

random.Random()本身并没有定义random()方法,而是继承了_random.Random()(它定义了一个名为random()的方法),这是一个名为Random()的类,位于模块_random中。

_random(它是一个内置模块)的C源代码可以在这里找到(实际上称为_randommodule.c。请参见下面的说明)。

用于使用C/C++编写的Python模块的命名约定

(历史上,如果一个模块叫做spam,包含其实现的C文件就被称为spammodule.c;如果模块名非常长,比如spammify,则模块名可以只是spammify.c。)

_random.Random.random()(或random.random())方法在_randommodule.c文件中定义为_random_Random_random_impl()

static PyObject *
_random_Random_random_impl(RandomObject *self)
{
    uint32_t a=genrand_int32(self)>>5, b=genrand_int32(self)>>6;
    return PyFloat_FromDouble((a*67108864.0+b)*(1.0/9007199254740992.0));
}

genrand_int32()是由Mersenne Twister PRNG实现定义的函数,它返回一个4字节的数字。

SystemRandom().random()如何工作?

(我知道你没有要求SystemRandom(),但当我写这篇文章时还没有意识到)

我制作了这张图片作为我的回答概述(然而,我鼓励你阅读全部内容)

Overview of SystemRandom's random() method

SystemRandom().random()定义在模块random.py中。

 ...
 def random(self):
    """Get the next random number in the range [0.0, 1.0)."""
    return (int.from_bytes(_urandom(7), 'big') >> 3) * RECIP_BPF**strong text**

该函数使用模块os.py中定义的另一个函数urandom()
from os import urandom as _urandom

os.py模块本身并没有定义urandom()函数,但是从一个内置模块中导入它。如果你使用的是POSIX OS,那么os.py将会导入posix内置模块;如果你使用的是Windows NT OS,那么os.py将会导入nt内置模块。这些模块包含了urandom()的定义。

if 'posix' in _names:
    name = 'posix'
    linesep = '\n'
    from posix import *

或者

elif 'nt' in _names:
    name = 'nt'
    linesep = '\r\n'
    from nt import *

"posix"和"nt"是内置模块,因此它们没有"__file__"属性。
深入源代码
POSIX
"urandom()"在"posixmodule.c"中定义为os_urandom_impl(),它调用_PyOS_URandom()
static PyObject *
os_urandom_impl(PyObject *module, Py_ssize_t size)
{
  ...
  bytes = PyBytes_FromStringAndSize(NULL, size);
  ...
  result = _PyOS_URandom(PyBytes_AS_STRING(bytes), PyBytes_GET_SIZE(bytes));
  ...
  return bytes
}
  • _PyOS_URandom() 定义在 bootstrap_hash.c 文件中,然后调用 pyurandom()
int
_PyOS_URandom(void *buffer, Py_ssize_t size)
{
    return pyurandom(buffer, size, 1, 1);
}
  • pyurandom()定义在bootstrap_hash.c文件中,然后调用dev_urandom()
static int
pyurandom(void *buffer, Py_ssize_t size, int blocking, int raise)
{
  ...
  return dev_urandom(buffer, size, raise);
  ...
}
  • dev_urandom 定义在 bootstrap_hash.c 文件中,然后使用 /dev/urandom 目录获取随机字节。
static int
dev_urandom(char *buffer, Py_ssize_t size, int raise)
{
  ...
  fd = _Py_open("/dev/urandom", O_RDONLY);
  ...
  do {
    n = _Py_read(fd, buffer, (size_t)size);
    ...
  } while (0 < size);
  ...
}

Windows NT

这看起来有点奇怪(我也这么认为),但是posixmodule.c文件也用于NT系统,以下是文件开头的一段引用(注释)

该文件还用于Windows NT / MS-Win。 在这种情况下,该模块实际上将自己称为“nt”,而不是“posix”,并且一些函数未实现或以不同方式实现。源代码假定对于Windows NT,宏“MS_WINDOWS”已定义,而与使用的编译器无关。 不同的编译器定义自己的特征测试宏,例如'_MSC_VER'。

对于Windows NT,函数调用链与POSIX相同,直到pyurandom()函数。

  • pyurandom()bootstrap_hash.c文件中定义,然后调用win32_urandom()
static int
pyurandom(void *buffer, Py_ssize_t size, int blocking, int raise)
{
  ...
  #ifdef MS_WINDOWS
      return win32_urandom((unsigned char *)buffer, size, raise);
  #else
  ...
}
  • win32_urandom() 定义在 bootstrap_hash.c 文件中,然后调用 CryptGenRandom()
static int
win32_urandom(unsigned char *buffer, Py_ssize_t size, int raise)
{
  ...
  if (!CryptGenRandom(hCryptProv, chunk, buffer))
  {
  ...
  }
  ...
  return 0;
}
  • CryptGenRandom()wincrypt.h文件中声明,在Advapi32.libAdvapi32.dll库中定义(这些文件由Microsoft提供)

5

random.random()实际上是在这里定义的:

random = _inst.random

然而,它只是对C实现的一个参考。
以下是源代码的引用:
关于基础Mersenne Twister核心生成器的一般说明:
- 周期为2 ** 19937-1。 - 它是存在的最经过广泛测试的发生器之一。 - random()方法在C中实现,在单个Python步骤中执行,因此是线程安全的。
您可能想查看Mersenne Twister上的文章。简而言之,发生器的状态与“先前的数字”不同,它是更加复杂的东西。因此,您在“…伪随机数生成器通过对值执行某些操作来工作。通常,此值是由生成器生成的先前数字”的说法是错误的。
至于SystemRandom.random(),它在某种程度上与random.random()无关。在Python中,具有相同名称的函数从不同模块导入可能是不同的,因此在这里不能依赖函数名称。

2
你知道在哪里可以找到C模块吗? - SarcasticSully

1
这是你错过的片段(CPython32):
import _random

class Random(_random.Random):

在哪里可以找到_random.Random

>>> import _random
>>> dir(_random.Random)
[(...) 'getrandbits', 'getstate', 'jumpahead', 'random', 'seed', 'setstate']

以下是同一random.py实现文件开头的docstring中的内容:

关于基础Mersenne Twister核心生成器的一般说明:

  • 周期为2 ** 19937-1。
  • 它是现有生成器中经过最广泛测试的之一。
  • random()方法是用C实现的,可以在单个Python步骤中执行,因此是线程安全的。

_random是一个编译的C库,包含一些基本操作,在random.py文件中包含Python的实现进一步扩展了这些操作。


0

你是正确的,这是使用 os.urandom 函数

from os import urandom as _urandom

BPF = 53        # Number of bits in a float
RECIP_BPF = 2**-BPF

def random():
    """Get the next random number in the range [0.0, 1.0)."""
    return (int.from_bytes(_urandom(7), 'big') >> 3) * RECIP_BPF

print(random())
print(RECIP_BPF)
print(int.from_bytes(_urandom(7), 'big')>> 3)
print(int.from_bytes(_urandom(7), 'little') >> 3)

多试着操作一下,你就能够正确理解它。


1
你得到了我的点赞,尽管53不是浮点数中的位数。它是float64中用于包含尾数的双精度位数。指数在此处被忽略。因此,称变量BPF也有点可疑。但我们可以跳过这些细节,因为Python总是使用双精度浮点数,除非通过使用ctypes.c_float()或numpy.float()进行“显式排序”来禁用。但说53是float(64)中的位数就是错误的。它只是尾数。 - Dalen
1
使用2 ** -53作为最大倒数(1.0 / 2 ** 53)将int转换为Python浮点数,范围从0到1。虽然移位也有它的作用。 :D - Dalen
是的,我也对浮点数中53位有所疑虑,但由于它在随机py中被定义,我认为应该给予它怀疑的好处,因为它可以正常工作,在Python中,浮点数和双精度浮点数并没有太大的区别,至少在我使用过的范围内。 - Harry
这就是为什么我确保在BPF定义的随机py中提到了确切的注释。 - Harry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接