AJP
AJP

Reputation: 28573

Python random.seed behaved strangely

I called random.seed(234), then called random.randint(0, 99) and received 92. When I repeated this process again several times I received 86. When I called random.randint a second time then it return 92. I was expecting the first value to be 86 not 92. Why was it 92?

The full log output is below. I've included all of it incase there was some previous action that can explain the seemingly buggy behaviour:

In [1]: import random

In [2]: import string

In [3]: string.letters
Out[3]: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'

In [4]: string.ascii_letters
Out[4]: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

In [5]: string.printable
Out[5]: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

In [6]: len(string.printable)
Out[6]: 100

In [7]: [string.printable[random.randint(0,99)] for i in range(20)]
Out[7]: 
['{',
'+',
'[',
'\r',
'R',
'Z',
'v',
'|',
'v',
'e',
'T',
'x',
'\\',
'}',
'0',
'>',
'V',
'\n',
'`',
'`']

In [8]: ''.join([string.printable[random.randint(0,99)] for i in range(20)])
Out[8]: '%Z\\%mx4Z53uUZIa5KHe*'

In [9]: ''.join([string.printable[random.randint(0,99)] for i in range(20)])
Out[9]: 'Fg\nDHW+oV?-9``}\x0by%xD'

In [10]: import os

In [11]: os.urandom(1)
Out[11]: '('

In [12]: os.urandom(1)
Out[12]: '8'

In [13]: os.urandom(1)
Out[13]: '\xb1'

In [14]: os.urandom(1)
Out[14]: ')'

In [15]: os.urandom(1)
Out[15]: '\x8c'

In [16]: os.urandom(1)
Out[16]: '^'

In [17]: os.urandom(1)
Out[17]: '{'

In [18]: os.urandom(1)
Out[18]: '\x8f'

In [19]: ''.join(os.urandom(10))
Out[19]: '{t\x8dR\x1d\x83\xef\xd6N\xbd'

In [20]: ''.join(os.urandom(10))
Out[20]: '\x96\\\xf6\xe3\xf4/\x1f\xc7\x90\x02'

In [21]: from random import SystemRandom

In [22]: crypt = SystemRandom()

In [23]: ''.join([string.printable[crypt.randrange(100)] for i in range(20)])
Out[23]: "WoDVH\r1!?1+djB'f<;nW"

In [24]: ''.join([string.printable[crypt.randrange(100)] for i in range(20)])
Out[24]: '\rf?zo`7^{Y_Zx^[SYw7c'

In [25]: ''.join([string.printable[crypt.randrange(100)] for i in range(20)])
Out[25]: "3k*uGVIP'~^{P*~bserk"

In [26]: ''.join([string.printable[crypt.randrange(100)] for i in range(20)])
Out[26]: '~lkM/a&#_F&D\n<sC&i\r\n'

In [27]: random.seed(234)

In [28]: random.randint(0,99)
Out[28]: 92

In [29]: random.seed(234)

In [30]: random.randint(0,99)
Out[30]: 86

In [31]: random.seed(234)

In [32]: random.randint(0,99)
Out[32]: 86

In [33]: random.seed(234)

In [34]: random.randint(0,99)
Out[34]: 86

In [35]: random.randint(0,99)
Out[35]: 92

In [36]: random.randint(0,99)
Out[36]: 48

In [37]: random.seed(234)

In [38]: random.randint(0,99)
Out[38]: 86

In [39]: import sys

In [40]: sys.version_info
Out[40]: sys.version_info(major=2, minor=7, micro=13, releaselevel='final', serial=0)

In [41]: sys.version
Out[41]: '2.7.13 (default, Dec 17 2016, 23:03:43) \n[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)]'

** edit, weird: "same" seemingly buggy behaviour repeated ** In the same terminal window I closed the previous ipython session. I did some command line activity, then I opened ipython again. I did some different work. Then I tried this again:

In [37]: import random

In [38]: random.seed(234)

In [39]: random.randint(0, 99)
Out[39]: 85

In [40]: random.randint(0, 99)
Out[40]: 50

In [41]: random.seed(234)

In [42]: random.randint(0, 99)
Out[42]: 86

In [43]: random.randint(0, 99)
Out[43]: 92

Upvotes: 5

Views: 887

Answers (2)

Mark Dickinson
Mark Dickinson

Reputation: 30865

What's happening here is that something in the IPython system is making use of the random module, and so consuming numbers from the random stream provided by the core Mersenne Twister generator. That means that if you're also using the random module, you only see an unpredictable subset of the numbers from the stream, since IPython gets the rest.

I can reproduce the effect you're seeing reliably (on both Python 2 and Python 3) by hitting the <Enter> key a few times randomly between invocations of random.randint (though actually I'm using random.random for simplicity). Here's one example session, with Python 3.6.2 and IPython 6.2.0 on macOS 10.12.6.

In [1]: import random

In [2]: random.seed(234)

In [3]: 

In [3]: 

In [3]: random.random()
Out[3]: 0.8579160018299248

In [4]: random.random()
Out[4]: 0.5055065431394443

In [5]: random.seed(234)

In [6]: random.random()
Out[6]: 0.26476014305349627

In [7]: random.random()
Out[7]: 0.8579160018299248

In [8]: random.random()
Out[8]: 0.5055065431394443

To check my hypothesis, I hacked in an override to the Random.random method in the random.py file in the standard library, by adding the following method to the Random class:

def random(self):
    print("random being called")
    import traceback; traceback.print_stack()
    return super(Random, self).random()

Now launch IPython, and hey presto! Lots of tracebacks. I won't reproduce the tracebacks in full (they're long), but here's the tail end of one of them:

  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/IPython/terminal/interactiveshell.py", line 376, in prompt_for_code
    pre_run=self.pre_prompt, reset_current_buffer=True)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/prompt_toolkit/interface.py", line 415, in run
    self.eventloop.run(self.input, self.create_eventloop_callbacks())
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/prompt_toolkit/eventloop/posix.py", line 157, in run
    random.shuffle(tasks)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/random.py", line 278, in shuffle
    j = randbelow(i+1)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/random.py", line 250, in _randbelow
    r = random()
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/random.py", line 98, in random
    import traceback; traceback.print_stack()

As you can see, the prompt_toolkit library, which is used by IPython, uses the random module to shuffle its tasks (though this change was recently removed, according to the CHANGELOG).

If you need a reliably reproducible random stream, create an explicit random.Random instance and use that:

In [1]: from random import Random

In [2]: my_random = Random()

In [3]: my_random.seed(234)

In [4]: my_random.randint(0, 99)
Out[4]: 43

In [5]: my_random.randint(0, 99)
Out[5]: 33

In [6]: my_random.seed(234)

In [7]: my_random.randint(0, 99)
Out[7]: 43

In [8]: my_random.randint(0, 99)
Out[8]: 33

Upvotes: 10

Jiri Semmler
Jiri Semmler

Reputation: 421

My Python 2.7.5 can not reproduce the same behavior, but the documentation ( https://docs.python.org/2/library/random.html ) says

If a is not None or an int or a long, then hash(a) is used instead. Note that the hash values for some types are nondeterministic when PYTHONHASHSEED is enabled.

I would (just opinion) say that it can be caused by the nondeterministic behavior of the hash function. Is your PYTHONHASHSEED enabled?

Upvotes: -1

Related Questions