【问题标题】:How exactly does random.random() work in python?random.random() 在 python 中究竟是如何工作的?
【发布时间】:2017-02-02 09:23:59
【问题描述】:

我对 random.random() 函数在 python 中的工作方式有点困惑。

docs 表示它“返回 [0.0, 1.0) 范围内的下一个随机浮点数”。 我知道伪随机数生成器通过对一个值执行一些操作来工作。通常,此值是生成器生成的先前数字。所以我认为这就是 'next random floating point' 在这里的意思。 (如有错误请指正)

但是当我看到随机库的source code时,class 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 生成一个随机数。根据documentation,它从特定于操作系统的随机源返回随机字节。所以这不会给出“下一个”浮点随机数。

这两者是如何联系起来的?还是它们是两个不同的东西?

我在这里很困惑。任何形式的帮助将不胜感激。

谢谢!

【问题讨论】:

    标签: python random


    【解决方案1】:

    python中的random模块包含pseudorandom number generators(PRNGs)的两个接口(类)。您可以将其视为生成随机数的两种方式。


    关于模块的注释 secrets.

    模块secrets实现任何类型的PRNG,但提供了基于 SystemRandom 和 os.urandom 的辅助函数(这很棒,因为我们不必自己编写它们) ( SystemRandom 基于)。 cmets是我的:

    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() 在第 749 行的“random.py”模块中定义(对我来说)

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

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

    _random(它是一个内置模块)模块的C源代码可以在here找到(它实际上叫_randommodule.c。见下文解释)

    Naming convention for python modules written in C/C++

    (从历史上看,如果一个模块被称为垃圾邮件,那么包含它的 C 文件 实现称为 spammodule.c;如果模块名称很 long,和 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(),但在我写这个的时候我还没有意识到)

    我制作了这张图片作为我的答案的概述(但是,我鼓励您阅读全部内容)

    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 上,则将导入 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 *
    

    posixnt 是内置模块,因此它们没有 __file__ 属性。

    source code潜水:

    POSIX

    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_urandombootstrap_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 提供)

    【讨论】:

      【解决方案2】:

      random.random()实际上是定义here

      random = _inst.random
      

      但是,它只是对 C 实现的引用。

      以下是引用来源:

      关于底层 Mersenne Twister 核心生成器的一般说明:

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

      您可能想查看Mersenne Twister 上的文章。简而言之,生成器的状态与“以前的数字”是不一样的,它要复杂得多。所以你在«...伪随机数生成器通过对一个值执行一些操作来工作是错误的。通常这个值是生成器之前生成的数字»。

      至于SystemRandom.random(),它在某种程度上与random.random()无关。在 Python 中,从不同模块导入的同名函数可能是不同的,所以这里不能依赖函数名。

      【讨论】:

      • 你知道在哪里可以找到 C 模块吗?
      【解决方案3】:

      这是你错过的片段(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 实现中进一步扩展。

      【讨论】:

        【解决方案4】:

        你是对的,这是使用 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)
        

        玩弄它,你会得到它。

        【讨论】:

        • 你有我的赞成票,虽然 53 不是浮点数中的位数。它是 float64 中的位数,即用于包含尾数的双精度数。指数在这里被忽略。因此调用变量 BPF 也有点可疑。但是我们可以跳过这些细节,因为 Python 总是对他的浮点数使用双精度数,除非“明确命令”不使用 ctypes.c_float() 或 numpy.float()。但是说 53 是 float(64) 中的位数是完全错误的。这只是螳螂。
        • 使用 2**-53 作为最大倒数 (1.0/2**53) 将 int 转换为 Python 浮点数,范围从 0 到 1。好吧,转变也发挥了作用。 :D
        • 是的,我知道我也对 53 作为 float 中的位存有疑问,但正如它在随机 py 本身中定义的那样,被认为给它带来怀疑的好处,因为它可以正常工作,并且在 python 中 float 和 double 不是至少和我用过的一样多。
        • 这就是为什么一定要提到 BPF 定义的随机 py 中存在的确切注释。
        猜你喜欢
        • 1970-01-01
        • 2023-04-01
        • 2013-05-06
        • 1970-01-01
        • 2011-06-26
        • 2021-08-15
        • 2012-06-08
        • 2011-10-11
        相关资源
        最近更新 更多