Skip to content

Commit ba3a87a

Browse files
Wolfgang Maierrhettinger
authored andcommitted
bpo-33144: random.Random and subclasses: split _randbelow implementation (GH-6291)
1 parent 28e8b66 commit ba3a87a

File tree

3 files changed

+107
-29
lines changed

3 files changed

+107
-29
lines changed

Lib/random.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
"""
3939

4040
from warnings import warn as _warn
41-
from types import MethodType as _MethodType, BuiltinMethodType as _BuiltinMethodType
4241
from math import log as _log, exp as _exp, pi as _pi, e as _e, ceil as _ceil
4342
from math import sqrt as _sqrt, acos as _acos, cos as _cos, sin as _sin
4443
from os import urandom as _urandom
@@ -94,6 +93,28 @@ def __init__(self, x=None):
9493
self.seed(x)
9594
self.gauss_next = None
9695

96+
def __init_subclass__(cls, **kwargs):
97+
"""Control how subclasses generate random integers.
98+
99+
The algorithm a subclass can use depends on the random() and/or
100+
getrandbits() implementation available to it and determines
101+
whether it can generate random integers from arbitrarily large
102+
ranges.
103+
"""
104+
105+
if (cls.random is _random.Random.random) or (
106+
cls.getrandbits is not _random.Random.getrandbits):
107+
# The original random() builtin method has not been overridden
108+
# or a new getrandbits() was supplied.
109+
# The subclass can use the getrandbits-dependent implementation
110+
# of _randbelow().
111+
cls._randbelow = cls._randbelow_with_getrandbits
112+
else:
113+
# There's an overridden random() method but no new getrandbits(),
114+
# so the subclass can only use the getrandbits-independent
115+
# implementation of _randbelow().
116+
cls._randbelow = cls._randbelow_without_getrandbits
117+
97118
def seed(self, a=None, version=2):
98119
"""Initialize internal state from hashable object.
99120
@@ -221,22 +242,23 @@ def randint(self, a, b):
221242

222243
return self.randrange(a, b+1)
223244

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

228-
random = self.random
229248
getrandbits = self.getrandbits
230-
# Only call self.getrandbits if the original random() builtin method
231-
# has not been overridden or if a new getrandbits() was supplied.
232-
if type(random) is BuiltinMethod or type(getrandbits) is Method:
233-
k = n.bit_length() # don't use (n-1) here because n can be 1
234-
r = getrandbits(k) # 0 <= r < 2**k
235-
while r >= n:
236-
r = getrandbits(k)
237-
return r
238-
# There's an overridden random() method but no new getrandbits() method,
239-
# so we can only use random() from here.
249+
k = n.bit_length() # don't use (n-1) here because n can be 1
250+
r = getrandbits(k) # 0 <= r < 2**k
251+
while r >= n:
252+
r = getrandbits(k)
253+
return r
254+
255+
def _randbelow_without_getrandbits(self, n, int=int, maxsize=1<<BPF):
256+
"""Return a random int in the range [0,n). Raises ValueError if n==0.
257+
258+
The implementation does not use getrandbits, but only random.
259+
"""
260+
261+
random = self.random
240262
if n >= maxsize:
241263
_warn("Underlying random() generator does not supply \n"
242264
"enough bits to choose from a population range this large.\n"
@@ -251,6 +273,8 @@ def _randbelow(self, n, int=int, maxsize=1<<BPF, type=type,
251273
r = random()
252274
return int(r*maxsize) % n
253275

276+
_randbelow = _randbelow_with_getrandbits
277+
254278
## -------------------- sequence methods -------------------
255279

256280
def choice(self, seq):

Lib/test/test_random.py

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time
66
import pickle
77
import warnings
8+
import logging
89
from functools import partial
910
from math import log, exp, pi, fsum, sin, factorial
1011
from test import support
@@ -619,6 +620,16 @@ def test_genrandbits(self):
619620
self.assertRaises(ValueError, self.gen.getrandbits, 0)
620621
self.assertRaises(ValueError, self.gen.getrandbits, -1)
621622

623+
def test_randrange_uses_getrandbits(self):
624+
# Verify use of getrandbits by randrange
625+
# Use same seed as in the cross-platform repeatability test
626+
# in test_genrandbits above.
627+
self.gen.seed(1234567)
628+
# If randrange uses getrandbits, it should pick getrandbits(100)
629+
# when called with a 100-bits stop argument.
630+
self.assertEqual(self.gen.randrange(2**99),
631+
97904845777343510404718956115)
632+
622633
def test_randbelow_logic(self, _log=log, int=int):
623634
# check bitcount transition points: 2**i and 2**(i+1)-1
624635
# show that: k = int(1.001 + _log(n, 2))
@@ -640,21 +651,22 @@ def test_randbelow_logic(self, _log=log, int=int):
640651
self.assertEqual(k, numbits) # note the stronger assertion
641652
self.assertTrue(2**k > n > 2**(k-1)) # note the stronger assertion
642653

643-
@unittest.mock.patch('random.Random.random')
644-
def test_randbelow_overridden_random(self, random_mock):
654+
def test_randbelow_without_getrandbits(self):
645655
# Random._randbelow() can only use random() when the built-in one
646656
# has been overridden but no new getrandbits() method was supplied.
647-
random_mock.side_effect = random.SystemRandom().random
648657
maxsize = 1<<random.BPF
649658
with warnings.catch_warnings():
650659
warnings.simplefilter("ignore", UserWarning)
651660
# Population range too large (n >= maxsize)
652-
self.gen._randbelow(maxsize+1, maxsize = maxsize)
653-
self.gen._randbelow(5640, maxsize = maxsize)
661+
self.gen._randbelow_without_getrandbits(
662+
maxsize+1, maxsize=maxsize
663+
)
664+
self.gen._randbelow_without_getrandbits(5640, maxsize=maxsize)
654665
# issue 33203: test that _randbelow raises ValueError on
655666
# n == 0 also in its getrandbits-independent branch.
656667
with self.assertRaises(ValueError):
657-
self.gen._randbelow(0, maxsize=maxsize)
668+
self.gen._randbelow_without_getrandbits(0, maxsize=maxsize)
669+
658670
# This might be going too far to test a single line, but because of our
659671
# noble aim of achieving 100% test coverage we need to write a case in
660672
# which the following line in Random._randbelow() gets executed:
@@ -672,8 +684,10 @@ def test_randbelow_overridden_random(self, random_mock):
672684
n = 42
673685
epsilon = 0.01
674686
limit = (maxsize - (maxsize % n)) / maxsize
675-
random_mock.side_effect = [limit + epsilon, limit - epsilon]
676-
self.gen._randbelow(n, maxsize = maxsize)
687+
with unittest.mock.patch.object(random.Random, 'random') as random_mock:
688+
random_mock.side_effect = [limit + epsilon, limit - epsilon]
689+
self.gen._randbelow_without_getrandbits(n, maxsize=maxsize)
690+
self.assertEqual(random_mock.call_count, 2)
677691

678692
def test_randrange_bug_1590891(self):
679693
start = 1000000000000
@@ -926,6 +940,49 @@ def test_betavariate_return_zero(self, gammavariate_mock):
926940
gammavariate_mock.return_value = 0.0
927941
self.assertEqual(0.0, random.betavariate(2.71828, 3.14159))
928942

943+
class TestRandomSubclassing(unittest.TestCase):
944+
def test_random_subclass_with_kwargs(self):
945+
# SF bug #1486663 -- this used to erroneously raise a TypeError
946+
class Subclass(random.Random):
947+
def __init__(self, newarg=None):
948+
random.Random.__init__(self)
949+
Subclass(newarg=1)
950+
951+
def test_subclasses_overriding_methods(self):
952+
# Subclasses with an overridden random, but only the original
953+
# getrandbits method should not rely on getrandbits in for randrange,
954+
# but should use a getrandbits-independent implementation instead.
955+
956+
# subclass providing its own random **and** getrandbits methods
957+
# like random.SystemRandom does => keep relying on getrandbits for
958+
# randrange
959+
class SubClass1(random.Random):
960+
def random(self):
961+
return super().random()
962+
963+
def getrandbits(self, n):
964+
logging.getLogger('getrandbits').info('used getrandbits')
965+
return super().getrandbits(n)
966+
with self.assertLogs('getrandbits'):
967+
SubClass1().randrange(42)
968+
969+
# subclass providing only random => can only use random for randrange
970+
class SubClass2(random.Random):
971+
def random(self):
972+
logging.getLogger('random').info('used random')
973+
return super().random()
974+
with self.assertLogs('random'):
975+
SubClass2().randrange(42)
976+
977+
# subclass defining getrandbits to complement its inherited random
978+
# => can now rely on getrandbits for randrange again
979+
class SubClass3(SubClass2):
980+
def getrandbits(self, n):
981+
logging.getLogger('getrandbits').info('used getrandbits')
982+
return super().getrandbits(n)
983+
with self.assertLogs('getrandbits'):
984+
SubClass3().randrange(42)
985+
929986
class TestModule(unittest.TestCase):
930987
def testMagicConstants(self):
931988
self.assertAlmostEqual(random.NV_MAGICCONST, 1.71552776992141)
@@ -937,13 +994,6 @@ def test__all__(self):
937994
# tests validity but not completeness of the __all__ list
938995
self.assertTrue(set(random.__all__) <= set(dir(random)))
939996

940-
def test_random_subclass_with_kwargs(self):
941-
# SF bug #1486663 -- this used to erroneously raise a TypeError
942-
class Subclass(random.Random):
943-
def __init__(self, newarg=None):
944-
random.Random.__init__(self)
945-
Subclass(newarg=1)
946-
947997
@unittest.skipUnless(hasattr(os, "fork"), "fork() required")
948998
def test_after_fork(self):
949999
# Test the global Random instance gets reseeded in child
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
``random.Random()`` and its subclassing mechanism got optimized to check only
2+
once at class/subclass instantiation time whether its ``getrandbits()`` method
3+
can be relied on by other methods, including ``randrange()``, for the
4+
generation of arbitrarily large random integers. Patch by Wolfgang Maier.

0 commit comments

Comments
 (0)