There's a situation involving sub-classing I can't figure out.
I'm sub-classing Random
(the reason is besides the point). Here's a basic example of what I have:
import random
class MyRandom(random.Random):
def __init__(self, x): # x isn't used here, but it's necessary to show the problem.
print("Before")
super().__init__() # Nothing passed to parent
print("After")
MyRandom([])
The above code, when run, gives the following error (and doesn't print "Before"):
>>> import test
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Users\_\PycharmProjects\first\test.py", line 11, in <module>
MyRandom([])
TypeError: unhashable type: 'list'
To me, this doesn't make any sense. Somehow, the argument to MyRandom
is apparently being passed directly to Random.__init__
even though I'm not passing it along, and the list is being treated as a seed. "Before"
never prints, so apparently my initializer is never even being called.
I thought maybe this was somehow due to the parent of Random
being implemented in C and this was causing weirdness, but a similar case with list
sub-classing doesn't yield an error saying that ints aren't iterable:
class MyList(list):
def __init__(self, y):
print("Before")
super().__init__()
print("After")
r = MyList(2) # Prints "Before", "After"
I have no clue how to even approach this. I rarely ever sub-class, and even rarer is it that I sub-class a built-in, so I must have developed a hole in my knowledge. This is not how I expect sub-classing to work. If anyone can explain what's going on here, I'd appreciate it.
Python 3.9
Instantiating a class causes its __new__
method to be called. It is passed the name of the class and the arguments in the constructor call. So MyRandom([1, 2])
results in the call MyRandom.__new__(MyRandom, [1, 2])
. (3.9.10 documentation).
Because there isn't a MyRandom.__new__()
method, the base classes are searched. random.Random
does have a __new__()
method (see random_new()
in _randommodule.c). So we get a call something like this random_new(MyRandom, [1, 2])
.
Looking at the C code for random_new()
, it calls random_seed(self, [1, 2])
. Because the second argument isn't Null, or None
, or an int
, or a subclass of int, the code calls PyObject_Hash([1, 2])
. But a list isn't hashable, hence the error.
If __new__()
returns a instance of the class, then the __init__()
method is called with the arguments in the constuctor call.
One possible fix is to define a MyRandom.__new__()
method, which calls super().__new__()
but only passes the appropriate args.
class MyRandom(random.Random):
def __new__(cls, *args, **kwargs):
#print(f"In __new__: {args=}, {kwargs=}")
# Random.__new__ expects an optional seed. We are going to
# implement out own RNG, so ignore args and kwargs. Pass in a
# junk integer value so that Random.__new__ doesn't waste time
# trying to access urandom or calling time to initialize the MT RNG
# since we aren't going to use it anyway.
return super().__new__(cls, 123)
def __init__(cls, *args, **kwargs):
#print(f"In __init__: {args=}, {kwargs=}")
# initialize your custom RNG here
pass
Also override the methods: random()
, seed()
, getstate()
, setstate()
, and optionally getrandbits()
.
An alternative fix is to only use keyword arguments in the __init__()
methods of the subclasses. The C code for random_new()
checks to see if a an instance of random.Random is being created. If true, the code throws and error if there are any keyword arguments. However, if a subclass is being created, any keyword arguments are ignored by random_new()
, but can be used in the subclass __init__()
.
class MyRandom(random.Random):
def __init__(self, *, x): # make x a keyword only argument
print("Before")
super().__init__() # Nothing passed to parent
print("After")
MyRandom(x=[])
Interestingly, in Python 3.10, the code for random_new
has been changed to raise an error if more that 1 positional argument is supplied.