Make name lookups follow MRO within a class definition

Back to the main topic of the base proposition, I think this would be a bad idea because I had to wrestle with it some time ago and the proposed “fix” would have prevented me from solving my problem.

I was tweaking some code which defined subclasses of random.Random, and which wrapped its methods. Of course, the file itself imported the random module. One of the method-wrapping lines looked something like this.

# this is inside the class block
randrange = thewrapper(random.randrange)
sample = thewrapper(random.sample)
random = thewrapper(random.random)

And I needed to add the new binomialvariate method (it wasn’t that one but it works for the example). So I added its line, giving the following.

# still inside the class block
randrange = thewrapper(random.randrange)
sample = thewrapper(random.sample)
random = thewrapper(random.random)
binomialvariate = thewrapper(random.binomialvariate)

And it failed. Did you guess why ? Because random was defined as a method just above, so it wasn’t the random module anymore. The solution to that bug was to reorder the random = to the bottom.

All this to say, existing code relies on the variables defined outside the class block being readable inside it, even if they have the same name as inherited members or methods. What if the variables defined outside the block were to take precedence over your new magic scope extension, you may ask ? (that may be what you’re proposing with ChainMaps)

In that case, a given variable name may hold three different meanings inside the class block, with the following order of precedence (unless you want to break compatibility) :

  • a method or member you just defined inside the class block
  • if not, a value defined outside the class block
  • if not, a value found in one of the parent classes.
import random

from ... import RandomSubclass # subclass of random.Random

class MyRandom(RandomSubclass):
    randrange = wrapper(randrange)
    # randrange here would be scoped from the parents and be RandomSubclass's

    # Suppose I want to skip RandomSubclass.sample and directly wrap the native one.
    sample = wrapper(random.sample)
    # random here is scoped outside the class body because it exists there.

    random = wrapper(RandomSubclass.random)
    # needs to be accessed that way to avoid collisions with the module defined outside

    # ...and now "random" evaluates to the method !

That’s way too magic in my opinion, and in the rare cases where it would be used, would probably cause more confusion and mistakes than the examples you gave.

There are things like that in Python which you can consider bad choices, a similar example is the fact that default values for function parameters are scoped statically rather than dynamically which I find to be a terrible design in the language as it is today, but when you take compatibility into account it just becomes impossible to change.


Also:

That’s not true, if by that you mean the scope when writing variables in a class block, in a def block and in a module file respectively. All three have major differences, and even though the function and class scopes look alike with separate local and global scopes, one interesting difference is that the function exists when its code gets executed, whereas a class doesn’t exist yet when its code gets executed. That makes implementation of your proposition very tricky if not impossible, when taking into account the MRO rules which the metaclass can meddle with.


Lastly, I think there could be value in allowing super() inside a class block to generate a sort of sentinel object, sort of like enum.auto(). super().randrange for example would hold a sentinel value which, once the class block is fully executed and the class is getting built, is resolved like Ben would like it to and replaced by (here) RandomSubclass.randrange. But the usability of such a feature would have to be weighed.

2 Likes