Skip to content

Commit cc1a10c

Browse files
author
Wolfgang Maier
committed
random.Random and subclasses: split _randbelow implementation
Splits the getrandbits-dependent and -independent branches of random.Random._randbelow into separate methods and selects the implementation to be used by Random and its subclasses at class creation time for increased performance.
1 parent 9c463ec commit cc1a10c

File tree

2 files changed

+118
-29
lines changed

2 files changed

+118
-29
lines changed

Lib/random.py

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

4040
from warnings import warn as _warn
41-
from types import MethodType as _MethodType, BuiltinMethodType as _BuiltinMethodType
41+
from types import FunctionType as _FunctionType
4242
from math import log as _log, exp as _exp, pi as _pi, e as _e, ceil as _ceil
4343
from math import sqrt as _sqrt, acos as _acos, cos as _cos, sin as _sin
4444
from os import urandom as _urandom
@@ -94,6 +94,18 @@ def __init__(self, x=None):
9494
self.seed(x)
9595
self.gauss_next = None
9696

97+
def __init_subclass__(cls, **kwargs):
98+
# Only call self.getrandbits if the original random() builtin method
99+
# has not been overridden or if a new getrandbits() was supplied.
100+
if type(cls.__dict__.get('getrandbits')) is _FunctionType:
101+
cls._randbelow = cls._randbelow_with_getrandbits
102+
elif 'random' in cls.__dict__:
103+
# There's an overridden random() method but no new getrandbits() method,
104+
# so we can only use random() from here.
105+
cls._randbelow = cls._randbelow_without_getrandbits
106+
else:
107+
cls._randbelow = getattr(cls, cls._randbelow.__name__)
108+
97109
def seed(self, a=None, version=2):
98110
"""Initialize internal state from hashable object.
99111
@@ -221,22 +233,26 @@ def randint(self, a, b):
221233

222234
return self.randrange(a, b+1)
223235

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

228-
random = self.random
229239
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.
240+
k = n.bit_length() # don't use (n-1) here because n can be 1
241+
r = getrandbits(k) # 0 <= r < 2**k
242+
while r >= n:
243+
r = getrandbits(k)
244+
return r
245+
246+
def _randbelow_without_getrandbits(self, n, int=int, maxsize=1<<BPF):
247+
"""Return a random int in the range [0,n). Raises ValueError if n==0.
248+
249+
The implementation does not use getrandbits, but only random.
250+
"""
251+
252+
if n == 0:
253+
raise ValueError("Upper boundary cannot be zero")
254+
255+
random = self.random
240256
if n >= maxsize:
241257
_warn("Underlying random() generator does not supply \n"
242258
"enough bits to choose from a population range this large.\n"
@@ -251,6 +267,8 @@ def _randbelow(self, n, int=int, maxsize=1<<BPF, type=type,
251267
r = random()
252268
return int(r*maxsize) % n
253269

270+
_randbelow = _randbelow_with_getrandbits
271+
254272
## -------------------- sequence methods -------------------
255273

256274
def choice(self, seq):

Lib/test/test_random.py

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -640,21 +640,22 @@ def test_randbelow_logic(self, _log=log, int=int):
640640
self.assertEqual(k, numbits) # note the stronger assertion
641641
self.assertTrue(2**k > n > 2**(k-1)) # note the stronger assertion
642642

643-
@unittest.mock.patch('random.Random.random')
644-
def test_randbelow_overridden_random(self, random_mock):
643+
def test_randbelow_without_getrandbits(self):
645644
# Random._randbelow() can only use random() when the built-in one
646645
# has been overridden but no new getrandbits() method was supplied.
647-
random_mock.side_effect = random.SystemRandom().random
648646
maxsize = 1<<random.BPF
649647
with warnings.catch_warnings():
650648
warnings.simplefilter("ignore", UserWarning)
651649
# Population range too large (n >= maxsize)
652-
self.gen._randbelow(maxsize+1, maxsize = maxsize)
653-
self.gen._randbelow(5640, maxsize = maxsize)
650+
self.gen._randbelow_without_getrandbits(
651+
maxsize+1, maxsize=maxsize
652+
)
653+
self.gen._randbelow_without_getrandbits(5640, maxsize=maxsize)
654654
# issue 33203: test that _randbelow raises ValueError on
655655
# n == 0 also in its getrandbits-independent branch.
656656
with self.assertRaises(ValueError):
657-
self.gen._randbelow(0, maxsize=maxsize)
657+
self.gen._randbelow_without_getrandbits(0, maxsize=maxsize)
658+
658659
# This might be going too far to test a single line, but because of our
659660
# noble aim of achieving 100% test coverage we need to write a case in
660661
# which the following line in Random._randbelow() gets executed:
@@ -672,8 +673,10 @@ def test_randbelow_overridden_random(self, random_mock):
672673
n = 42
673674
epsilon = 0.01
674675
limit = (maxsize - (maxsize % n)) / maxsize
675-
random_mock.side_effect = [limit + epsilon, limit - epsilon]
676-
self.gen._randbelow(n, maxsize = maxsize)
676+
with unittest.mock.patch.object(random.Random, 'random') as random_mock:
677+
random_mock.side_effect = [limit + epsilon, limit - epsilon]
678+
self.gen._randbelow_without_getrandbits(n, maxsize=maxsize)
679+
self.assertEqual(random_mock.call_count, 2)
677680

678681
def test_randrange_bug_1590891(self):
679682
start = 1000000000000
@@ -926,6 +929,81 @@ def test_betavariate_return_zero(self, gammavariate_mock):
926929
gammavariate_mock.return_value = 0.0
927930
self.assertEqual(0.0, random.betavariate(2.71828, 3.14159))
928931

932+
class TestRandomSubclassing(unittest.TestCase):
933+
def test_random_subclass_with_kwargs(self):
934+
# SF bug #1486663 -- this used to erroneously raise a TypeError
935+
class Subclass(random.Random):
936+
def __init__(self, newarg=None):
937+
random.Random.__init__(self)
938+
Subclass(newarg=1)
939+
940+
def test_overriding_random(self):
941+
# First, let's assert our base class gets the selection of its
942+
# _randbelow implementation right.
943+
self.assertIs(
944+
random.Random._randbelow, random.Random._randbelow_with_getrandbits
945+
)
946+
947+
# Subclasses with a random method that got more recently defined than
948+
# their getrandbits method should not rely on getrandbits in
949+
# _randbelow, but select the getrandbits-independent implementation
950+
# of _randbelow instead.
951+
952+
# Subclass doesn't override any of the methods => keep using
953+
# original getrandbits-dependent version of _randbelow
954+
class SubClass1(random.Random):
955+
pass
956+
self.assertIs(
957+
SubClass1._randbelow, random.Random._randbelow_with_getrandbits
958+
)
959+
# subclass providing its own random **and** getrandbits methods
960+
# like random.SystemRandom does => keep relying on getrandbits for
961+
# _randbelow
962+
class SubClass2(random.Random):
963+
def random(self):
964+
pass
965+
966+
def getrandbits(self):
967+
pass
968+
self.assertIs(
969+
SubClass2._randbelow, random.Random._randbelow_with_getrandbits
970+
)
971+
# subclass providing only random => switch to getrandbits-independent
972+
# version of _randbelow
973+
class SubClass3(random.Random):
974+
def random(self):
975+
pass
976+
self.assertIs(
977+
SubClass3._randbelow, random.Random._randbelow_without_getrandbits
978+
)
979+
# subclass defining getrandbits to complement its inherited random
980+
# => can now rely on getrandbits for _randbelow again
981+
class SubClass4(SubClass3):
982+
def getrandbits(self):
983+
pass
984+
self.assertIs(
985+
SubClass4._randbelow, random.Random._randbelow_with_getrandbits
986+
)
987+
# subclass overriding the getrandbits-dependent implementation of
988+
# _randbelow => make sure it is used
989+
class SubClass5(SubClass4):
990+
def _randbelow_with_getrandbits(self):
991+
pass
992+
self.assertIs(
993+
SubClass5._randbelow, SubClass5._randbelow_with_getrandbits
994+
)
995+
self.assertIsNot(
996+
SubClass5._randbelow, random.Random._randbelow_with_getrandbits
997+
)
998+
# subclass defining random making it more recent than its inherited
999+
# getrandbits => switch back to getrandbits-independent implementaion
1000+
class SubClass6(SubClass5):
1001+
def random(self):
1002+
pass
1003+
self.assertIs(
1004+
SubClass6._randbelow, random.Random._randbelow_without_getrandbits
1005+
)
1006+
9291007
class TestModule(unittest.TestCase):
9301008
def testMagicConstants(self):
9311009
self.assertAlmostEqual(random.NV_MAGICCONST, 1.71552776992141)
@@ -937,13 +1015,6 @@ def test__all__(self):
9371015
# tests validity but not completeness of the __all__ list
9381016
self.assertTrue(set(random.__all__) <= set(dir(random)))
9391017

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-
9471018
@unittest.skipUnless(hasattr(os, "fork"), "fork() required")
9481019
def test_after_fork(self):
9491020
# Test the global Random instance gets reseeded in child

0 commit comments

Comments
 (0)