Home > Software design >  `random.seed` doesn't work with generators
`random.seed` doesn't work with generators

Time:10-30

I have a function that generates a sequence of random integers using random.randint. I also include a seed parameter for reproducibility.

I noticed an odd behavior when I returned a generator object. Different calls with the same seed did not return the same result. However, when I returned a tuple instead of a generator, the results from different calls were the same.

Returning a generator.

In [2]: import random

In [3]: def rand_sequence(n, seed=None):
   ...:     if seed is not None:
   ...:         random.seed(seed)
   ...:     return (random.randint(0, n) for _ in range(n))
   ...:

In [4]: first = rand_sequence(10, seed=0)

In [5]: second = rand_sequence(10, seed=0)

In [6]: assert tuple(first) == tuple(second)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Input In [6], in <cell line: 1>()
----> 1 assert tuple(first) == tuple(second)

AssertionError:

Returning a tuple

In [7]: def rand_sequence(n, seed=None):
   ...:     if seed is not None:
   ...:         random.seed(seed)
   ...:     return tuple(random.randint(0, n) for _ in range(n))
   ...:

In [8]: first = rand_sequence(10, seed=0)

In [9]: second = rand_sequence(10, seed=0)

In [10]: assert first == second

In [11]:

I guess it might have something to do with the control-flow mechanism of generators, but can't figure out how it plays out.

Edit:

What's even more confusing is when I used yield directly then the comparison checked out.

In [11]: def rand_sequence(n, seed=None):
    ...:     if seed is not None:
    ...:         random.seed(seed)
    ...:
    ...:     for _ in range(n):
    ...:         yield random.randint(0, n)
    ...:
    ...:

In [12]: first = rand_sequence(10, seed=0)

In [13]: second = rand_sequence(10, seed=0)

In [14]: assert tuple(first) == tuple(second)

CodePudding user response:

The generator expression is evaluated later, lazily, and it has side effects on the PRNG.

You will find it much easier to reason about your algorithm if you first store PRNG outputs in a container, such as a tuple or list.

Specifically, if your code produced

first = [f0, f1, f2, ..., f9]
second = [s0, s1, s2, ..., s9]

you are grappling with whether comparing equality of

f0 == s0
f1 == s1
...
f9 == s9

is "the same as" equality of

[f0, f1, ..., f9] == [s0, s1, ..., s9]

Perturbing the PRNG state in the middle of such equality tests is going to cause you grief, in the manner you have noted. Simplify, store values in containers, and you'll be happier. Certainly the debugging situation will improve.

EDIT

I wouldn't go so far as @Carcigenicate's remark about

... and generators only evaluate as equal if they're literally the same generator.

We can certainly evaluate whether each successive value generated by one matches what's generated by the other. The core difficulty OP encountered is the global PRNG state is shared across generators, so order-of-evaluation matters.

>>> r1 = range(7)
>>> r2 = range(7)
>>> id(r1)
4396054832
>>> id(r2)
4396054880
>>> r1 == r2
True
  • Related