Почему упаковка случайного метода random.Random, похоже, влияет на RNG? - PullRequest
5 голосов
/ 05 августа 2020

Я пытался регистрировать вызовы random(), помещая вокруг него оболочку для печати. К своему удивлению я заметил, что начал получать разные случайные значения. Я создал небольшой пример, демонстрирующий поведение:

Script main.py:

import random

def not_wrapped(seed):
    """ not wrapped """
    gen = random.Random()
    gen.seed(seed)

    # def wrappy(func):
    #     return lambda: func()
    # gen.random = wrappy(gen.random)

    gen.randint(1, 1)
    return gen.getstate()

def wrapped(seed):
    """ wrapped """
    gen = random.Random()
    gen.seed(seed)

    def wrappy(func):
        return lambda: func()
    gen.random = wrappy(gen.random)

    gen.randint(1, 1)
    return gen.getstate()

for s in range(20):
    print(s, not_wrapped(s) == wrapped(s))

Вывод python3.7.5 main.py (то же, что python 3.6.9)

0 True
1 False
2 False
3 False
4 False
5 True
6 False
7 False
8 False
9 False
10 True
11 False
12 False
13 False
14 False
15 True
16 False
17 True
18 False
19 True

Итак, как вы видите, состояние экземпляра Random gen отличается и зависит от того, оборачиваю ли я gen.random в wrappy или нет.

Если я вызываю randint() дважды тест не пройден для всех семян 0-19. Для Python 2.7.17 функции wrapped и not_wrapped каждый раз возвращают одно и то же случайное значение (выводит True для каждого начального числа).

Кто-нибудь знает, что происходит?

Python 3.6 онлайн-пример: http://tpcg.io/inbKc8hK

На Python 3.8.2 в этом онлайн-ответе проблема не не показывает: https://repl.it/@DavidMoberg / ExemplaryTeemingDos

(Этот вопрос был размещен по адресу: https://www.reddit.com/r/learnpython/comments/i597at/is_there_a_bug_in_randomrandom/)

1 Ответ

4 голосов
/ 07 августа 2020

Это примерно Python 3.6.

Состояние изменяется только при вызове gen.randint(1, 1), поэтому давайте посмотрим, что он вызывает под капотом.

  1. Здесь - реализация random.Random.randint:

    def randint(self, a, b):
        """Return random integer in range [a, b], including both end points.
        """
    
        return self.randrange(a, b+1)
    
  2. random.Random.randrange это прямо над , и он генерирует случайные числа используя random.Random._randbelow ...

  3. ... который реализован прямо под randint, а проверяет, был ли метод random переопределено!

Похоже, проблема в _randbelow:

...
from types import MethodType as _MethodType, BuiltinMethodType as _BuiltinMethodType
...


def _randbelow(self, n, int=int, maxsize=1<<BPF, type=type,
               Method=_MethodType, BuiltinMethod=_BuiltinMethodType):
    "Return a random int in the range [0,n).  Raises ValueError if n==0."

    random = self.random
    getrandbits = self.getrandbits

    # CHECKS IF random HAS BEEN OVERRIDDEN!

    # If not, this is the default behaviour:

    # Only call self.getrandbits if the original random() builtin method
    # has not been overridden or if a new getrandbits() was supplied.
    if type(random) is BuiltinMethod or type(getrandbits) is Method:
        k = n.bit_length()  # don't use (n-1) here because n can be 1
        r = getrandbits(k)          # 0 <= r < 2**k
        while r >= n:
            r = getrandbits(k)
        return r

    # OTHERWISE, it does something different!

    # There's an overridden random() method but no new getrandbits() method,
    # so we can only use random() from here.

    # And then it goes on to use `random` only, no `getrandbits`!

(Для справки, getrandbits реализовано в C здесь , а random также реализован в C здесь .)

Итак, есть разница: метод randint ведет себя по-разному в зависимости от того, random было отменено или нет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...