Andrei Cioara's answer is largely correct:
The randomness comes from Python 3.3 and later randomizing hash order by default (see Why is dictionary ordering non-deterministic?).
Accessing x
calls the lambda function that has been bound to __getattribute__
.
See Difference between __getattr__ vs __getattribute__ and the Python3 datamodel reference notes for object.__getattribute__
.
We can make this whole thing far less obfuscated with:
class t(object):
def __getattribute__(self, name):
use = None
for val in vars(object).values():
if callable(val) and type(val) is not type:
use = val
return use
def super_serious(obj):
proxy = t()
return proxy
which is sort of what happens with the lambda. Note that in the loop, we don't bind / save the current value of val
.1 This means that we get the last value that val
has in the function. With the original code, we do all this work at the time we create object t
, rather than later when t.__getattribute__
gets called—but it still boils down to: Of <name, value> pairs in vars(object), find the last one that meets our criteria: the value must be callable, while the value's type is not itself type
.
Using class t(object)
makes t
a new-style class object even in Python2, so that this code now "works" in Python2 as well as Python3. Of course, in Py2k, dictionary ordering is not randomized, so we always get the same thing every time:
$ python2 foo3.py
<slot wrapper '__init__' of 'object' objects>
$ python2 foo3.py
<slot wrapper '__init__' of 'object' objects>
vs:
$ python3 foo3.py
<slot wrapper '__eq__' of 'object' objects>
$ python3 foo3.py
<slot wrapper '__lt__' of 'object' objects>
Setting the environment variable PYTHONHASHSEED
to 0
makes the order deterministic in Python3 as well:
$ PYTHONHASHSEED=0 python3 foo3.py
<method '__subclasshook__' of 'object' objects>
$ PYTHONHASHSEED=0 python3 foo3.py
<method '__subclasshook__' of 'object' objects>
$ PYTHONHASHSEED=0 python3 foo3.py
<method '__subclasshook__' of 'object' objects>
1To see what this is about, try the following:
def f():
i = 0
ret = lambda: i
for i in range(3):
pass
return ret
func = f()
print('func() returns', func())
Note that it says func() returns 2
, not func() return 0
. Then replace the lambda line with:
ret = lambda stashed=i: stashed
and run it again. Now the function returns 0. This is because we saved the current value of i
here.
If we did this same sort of thing to the sample program, it would return the first val
that meets the criteria, rather than the last one.
class c: def __getattr__(self, a): print('getattr') def __getattribute__(self, a): print('getattribute') c().x = 9
and neither getter was called. – Starr