I have a program which is implemented in Python 2.7 that I want to migrate to Python 3.9.
I want to use regression testing to check if the migration is successful and confirm that the new version is performing the same calculation as the old version.
However...
The program uses random.Random(seed)
which the previous developer unwisely decided to seed with floating point numbers.
The code ports to python 3.9 easily but unfortunately it produces different random numbers (apart from instances where the float happens to be an integer). Fiddling around with the generators it looks like this is a consequence of the two versions using a different hash() method. It is not a consequence of changes in float point representation.
Is there a way to generate the same set of random numbers in Python 3.9 that would have been generated by that particular line of code in Python 2.7? i.e. How can I generate the result that legacy code random.Random(1.01)
would produce without using a legacy interpreter?
The code ports to python 3.9 easily but unfortunately it produces different random numbers (apart from instances where the float happens to be an integer). Fiddling around with the generators it looks like this is a consequence of the two versions using a different hash() method.
If you read the source code of Python 2.7, you can see how floating point hashing is implemented.
Once you have the C implementation, you can translate that into Python. I've simplified some details, such as not implementing integer hashing.
import math
def hash_double_version_27(v):
fractpart, intpart = math.modf(v)
if fractpart == 0:
raise Exception("hash for exact integer values not implemented")
v, expo = math.frexp(v)
v *= 2147483648.0
hipart = int(v)
v = (v - hipart) * 2147483648.0
x = hipart + int(v) + (expo << 15)
if x == -1:
x = -2
return x
I tested this by generating a few thousand random floating point numbers, and testing it against a copy of Python 2.7.18.
To use this with the random number generator, you can pass the resulting integer to random.Random()
, and it will not hash it any further. There is a subtlety around how negative numbers are handled, however: if you pass in a negative integer, it will get rid of the negative by taking the absolute value of the number. However, if it hashes an object and that results in a negative number, it casts the signed integer into an unsigned integer.
Here's how to convert the negative integer in the same manner as the C implementation. (Note that the comment there is incorrect - absolute value is not used for values from object hashing.)
import random
def cast_hash_to_unsigned(integer, bits):
if integer < 0:
integer += 1 << bits
return integer
def get_rand_object(f):
float_hash = hash_double_version_27(f)
float_hash = cast_hash_to_unsigned(float_hash, 64)
rng = random.Random(float_hash)
return rng
Similarly, I checked a few thousand generated values from this against Python 2.7.18, and it matched the cases I checked.