10

I am writing a program compatible with both Python 2.7 and 3.5. Some parts of it rely on stochastic process. My unit tests use an arbitrary seed, which leads to the same results across executions and languages... except for the code using random.shuffle.

Example in Python 2.7:

In[]:   import random
        random.seed(42)
        print(random.random())
        l = list(range(20))
        random.shuffle(l)
        print(l)
Out[]:  0.639426798458
        [6, 8, 9, 15, 7, 3, 17, 14, 11, 16, 2, 19, 18, 1, 13, 10, 12, 4, 5, 0]

Same input in Python 3.5:

In []:  import random
        random.seed(42)
        print(random.random())
        l = list(range(20))
        random.shuffle(l)
        print(l)
Out[]:  0.6394267984578837
        [3, 5, 2, 15, 9, 12, 16, 19, 6, 13, 18, 14, 10, 1, 11, 4, 17, 7, 8, 0]

Note that the pseudo-random number is the same, but the shuffled lists are different. As expected, reexecuting the cells does not change their respective output.

How could I write the same test code for the two versions of Python?

Aristide
  • 3,606
  • 2
  • 30
  • 50
  • 1
    Also, quick glance over [Py2 implementation](https://svn.python.org/projects/python/tags/r27/Lib/random.py) and [Py3 implementation](https://svn.python.org/projects/python/tags/r32/Lib/random.py) shows that there are more than one implementations of shuffle itself (although they may be equivalent - I'd expect there should be an unittests for that when modification was introduced). – Łukasz Rogalski Aug 14 '16 at 14:21
  • 1
    Try ```random.seed(42, version=1)``` (or maybe version=2; but set this argument) as documented [here](https://docs.python.org/3/library/random.html#random.seed). – sascha Aug 14 '16 at 14:24
  • `[random.random() for _ in range(20)]` does return the same results on both versions - so looks like it's an implementation change in `random.shuffle`... – Jon Clements Aug 14 '16 at 14:24
  • @sascha that's not relevant when directly using an `int` as the seed sadly. – Jon Clements Aug 14 '16 at 14:26
  • @sascha same output under Py3, `unexpected keyword argument` under Py2. – Aristide Aug 14 '16 at 14:26

3 Answers3

16

In Python 3.2 the random module was refactored a little to make the output uniform across architectures (given the same seed), see issue #7889. The shuffle() method was switched to using Random._randbelow().

However, the _randbelow() method was also adjusted, so simply copying the 3.5 version of shuffle() is not enough to fix this.

That said, if you pass in your own random() function, the implementation in Python 3.5 is unchanged from the 2.7 version, and thus lets you bypass this limitation:

random.shuffle(l, random.random)

Note however, than now you are subject to the old 32-bit vs 64-bit architecture differences that #7889 tried to solve.

Ignoring several optimisations and special cases, if you include _randbelow() the 3.5 version can be backported as:

import random
import sys

if sys.version_info >= (3, 2):
    newshuffle = random.shuffle
else:
    try:
        xrange
    except NameError:
        xrange = range

    def newshuffle(x):
        def _randbelow(n):
            "Return a random int in the range [0,n).  Raises ValueError if n==0."
            getrandbits = random.getrandbits
            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

        for i in xrange(len(x) - 1, 0, -1):
            # pick an element in x[:i+1] with which to exchange x[i]
            j = _randbelow(i+1)
            x[i], x[j] = x[j], x[i]

which gives you the same output on 2.7 as 3.5:

>>> random.seed(42)
>>> print(random.random())
0.639426798458
>>> l = list(range(20))
>>> newshuffle(l)
>>> print(l)
[3, 5, 2, 15, 9, 12, 16, 19, 6, 13, 18, 14, 10, 1, 11, 4, 17, 7, 8, 0]
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • @Marijn Pieters It's just fine, and as a bonus I can use the same tests I already wrote for Python 2. I would read the issue though. – Aristide Aug 14 '16 at 14:36
  • Actually, it seems that my tests still don't work under both versions. Specifically, the Py3 version sometimes passes, and sometimes fails, which proves that my seed is not enough to make it deterministic. I guess the other `random` functions I use (namely `randrange` and `choice`) have the same kind of problem. Very strange indeed. – Aristide Aug 14 '16 at 15:00
  • Yes, `choice` and `randrange` were also refactored, see [this patch](https://hg.python.org/cpython/diff/c2f8418a0e14/Lib/random.py). – Martijn Pieters Aug 14 '16 at 15:01
  • Both functions are easily backported based on the extracted section of `_randbelow()` I included in my answer as a local function. Just extract that one, and extract the functions you need from the [3.5 source code of `random.py`](https://hg.python.org/cpython/file/3.5/Lib/random.py). – Martijn Pieters Aug 14 '16 at 15:04
  • Thank you for your help. I really appreciate your insider view on this, but I'm starting to wonder whether modifying my code to make it pass the same tests would not be overkill, or even dangerous. Sorry, I'll just leave it unchanged for the moment. Do you think we should open an issue? I'm quite puzzled that a single seed does not command *all* random functions. – Aristide Aug 14 '16 at 15:12
  • @Aristide: that single seed most definitely commands all functions. Only the *path by which it commands them* has changed in Python 3. No, a ticket would be closed, there was never a guarantee that setting the seed would give you the same output across versions. – Martijn Pieters Aug 14 '16 at 15:13
  • I understand that the seed is not guaranteed to produce the same output across versions. But, in my case (cf. my second comment), it seems that the outcome varies across different executions with the same Py3 interpreter. – Aristide Aug 14 '16 at 15:18
  • @Aristide: Ah, misunderstood what you meant there. No, the seed is definitely deterministic. Make sure you are not relying on the order of a dictionary or a set to be deterministic however, as those are subject to hash randomisation. – Martijn Pieters Aug 14 '16 at 15:19
  • 1
    Yes, I definitely iterate on a dictionary. That would explain a lot of things, cf. http://stackoverflow.com/questions/30585108/disable-hash-randomization-from-within-python-program for more on this. You, sir, not only answered my exact question, but learnt me a lot of interesting stuff on the way. Thanks again for your time and your expertise. – Aristide Aug 14 '16 at 15:31
1

Elaborating on Martijn Pieters excellent answer and comments, and on this discussion, I finally found a workaround, which arguably doesn't answer my very question, but at the same time doesn't require deep changes. To sum up:

  • random.seed actually makes every random function deterministic, but doesn't necessarily produces the same output across versions;
  • setting PYTHONHASHSEED to 0 disables hash randomization for dictionaries and sets, which by default introduces a factor of non-determinism in Python 3.

So, in the bash script which launches the Python 3 tests, I added:

export PYTHONHASHSEED=0

Then, I temporarily changed my test functions in order to brute-force my way to an integer seed which would reproduces in Python 3 the results expected in Python 2. Lastly, I reverted my changes and replaced the lines:

seed(42)

by something like that:

seed(42 if sys.version_info.major == 2 else 299)

Nothing to brag about, but as the saying goes, sometimes practicality beats purity ;)

This quick workaround may be useful to somebody who wants to test the same stochastic code across different versions of Python!

Community
  • 1
  • 1
Aristide
  • 3,606
  • 2
  • 30
  • 50
0

Someone may correct me if I'm wrong but it seems that numpy.random module do not change between python 2 and 3.

>>> import numpy as np
>>> l = list(range(20))
>>> np.random.RandomState(42).shuffle(l)
>>> l
[0, 17, 15, 1, 8, 5, 11, 3, 18, 16, 13, 2, 9, 19, 4, 12, 7, 10, 14, 6]

I got the same result in both Python 2.7 (with np 1.12.1) and 3.7 (with np 1.14.5).

The doc also states that generated numbers should be the same between versions.

Compatibility Guarantee A fixed seed and a fixed series of calls to ‘RandomState’ methods using the same parameters will always produce the same results up to roundoff error except when the values were incorrect. Incorrect values will be fixed and the NumPy version in which the fix was made will be noted in the relevant docstring. Extension of existing parameter ranges and the addition of new parameters is allowed as long the previous behavior remains unchanged.

Conchylicultor
  • 4,631
  • 2
  • 37
  • 40