Why does this code print a randomly selected attribute?

Today, having written some particularly scary code, I came across this mysterious behavior. The Python 3 program below displays a randomly selected object attribute. How does this happen?

The obvious suspect in non-determinism is the random ordering of the vars(object) dictionary, but I don't see how this causes the observed behavior. One hypothesis was that this was caused by an override of the __setattr__ order, but this is disproved by the fact that lambda is always called only once (print debugging is checked).

 class TypeUnion: pass class t: pass def super_serious(obj): proxy = t() for name, val in vars(object).items(): if not callable(val) or type(val) is type: continue try: setattr(t, name, lambda _, *x, **y: val) except AttributeError: pass return proxy print(super_serious(TypeUnion()).x) 

NB The above program does not try to do anything useful; It is greatly reduced from the original.

+5
source share
3 answers

Andrea Chioara's answer is basically correct:

  • Randomness arises from Python 3.3 and later randomized default hash orders (see Why is a dictionary not deterministic?.

    / li>
  • Accessing x calls the lambda function, which was bound to __getattribute__ .

See The difference between __getattr__ vs __getattribute__ and the Python3 directory reference notes for object.__getattribute__ .

We can make all this a lot less confusing:

 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 what happens to lambda. Please note that in the loop we do not bind / save the current value of val . 1 This means that we get the last value that val has in the function. With the source code, we do all this work at the time of creating the object t , and not later, when we call t.__getattribute__ , but it still comes down to: Of <name, value> pairs in vars (object), find the last one that meets our criteria : the value must be callable, while the type of the value is not type itself.

Using class t(object) makes the class object t a new style even in Python2, so this code now "works" in Python2 as well as Python3. Of course, in Py2k, word 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 PYTHONHASHSEED environment PYTHONHASHSEED to 0 makes order deterministic in Python3:

 $ 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> 

1 To find out what this means, 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 the same thing in the sample program, it would return the first val , which would meet the criteria, not the last.

+3
source

Non-determinism comes from randomness in __dict__ returned by vars(object)

Printing is a little suspicious since your TypeUnion does not have an "x"

 super_serious(TypeUnion()).x 

The reason that something is returning is because your for loop overwrites __getattribute__ and therefore captures the point. Adding this line will show it.

  if name == '__getattribute__': continue 

Once get is compromised, set will also be dead. Think of it this way:

 setattr(t, name, lambda *x, **y: val) 

Conceptually coincides with

 t.__dict__[name] = lambda *x, **y: val 

But get now always returns the same link, regardless of the value of name , which is then overwritten. Therefore, the final answer will be the last element of this iteration, which is random, as the for loop goes through the random order of the initial __dict__

Also, keep in mind that if your goal is to make a copy of the object, then setattr is wrong. A lambda call would just return the original function, but would not call the original function that you need along the lines

 setattr(t, name, lambda *x, **y: val(*x, **y) # Which doesn't work 
+3
source

Yes, torek is true that your code does not bind the current value of val , so you get the last value assigned by val . Here is a version that "correctly" associates a value with a closure:

 class TypeUnion: pass class t: pass def super_serious(obj): proxy = t() for name, val in vars(object).items(): if not callable(val) or type(val) is type: continue try: setattr(t, name, (lambda v: lambda _, *x, **y: v)(val)) except AttributeError: pass return proxy print(super_serious(TypeUnion()).x) 

This will output <slot wrapper '__getattribute__' of 'object' objects> sequentially, proving that the problem is that __getattribute__ captured.

+3
source

Source: https://habr.com/ru/post/1268062/


All Articles