Matthew Schmidt
Matthew Schmidt

Reputation: 113

Getting the original class's member

I'm writing a game where things may be modified by special effects. I've decided to use python's ability to assign to an instance, rather than the class. Note that this is NOT modifying the entire class' member, just one instance of it. Think of it as a local enchantment in MtG.

How do I get back to the original class's member?

Take the following example code:

class Dog(object):
    noise = "Woof!"

    def bark(self):
        print self.noise

    def make_alpha(self):
        self.noise = self.noise.upper()

class Bulldog(Dog):
    noise = "Grr!"

class EnormousBulldog(Bulldog):
    pass

puppy = Dog()
print puppy.bark() # Woof!
bulldog = Bulldog()
print bulldog.bark() #Grr!
big_dog = EnormousBulldog()
print big_dog.bark() # Grr!

big_dog.make_alpha()
print puppy.bark() # Woof!
print bulldog.bark() # Grr!
print big_dog.bark() # GRR!

This all works.

But suppose I want to make a remove_alpha() method. I could try to reverse the effects of make_alpha() on the noise (and therefore do this for every possible special effect in the actual game), but I sense down that road lies madness. Simpler would be to just get back to Bulldog.noise. But how does EnormousBulldog get to Bulldog.noise?

self.__dict__["noise"] will give me the modified noise.

EnormousBulldog.__dict__ will give me nothing, because noise is in Bulldog, its superclass.

type(self).__getattribute__(self, "noise") will see there's a modified noise, and give me that instead.

I've considered overriding __getattribute__ instead of altering the instances as an alternate architecture entirely, but I think the performance hit isn't worth it.

Any ideas?

Upvotes: 1

Views: 66

Answers (3)

marianosimone
marianosimone

Reputation: 3606

I'd suggest a different approach, which has some performance impact, but keeps the code a bit simpler and way more flexible:

class EnhancedAction(object):
    """
    A way to keep track of the enhanced implementation,
    as well as the original one, to allow to go back to it
    """

    def __init__(self, target, original, impl):
        self._target = target
        self._original = original
        self._impl = impl

    def __call__(self, *args):
        return self._impl(self._target, self._original, args)

    @property
    def original(self):
        return self._original


class Dog(object):
    noise = "Woof!"

    def bark(self):
        return self.noise

    def make_alpha(self):
        self.bark = EnhancedAction(
            target=self, original=self.bark,
            impl=lambda self, original, args: original().upper()
    )

    def revert_alpha(self):
        if isinstance(self.bark, EnhancedAction):
            self.bark = self.bark.original


class Bulldog(Dog):
    noise = "Grr!"


class EnormousBulldog(Bulldog):
    pass


big_dog = EnormousBulldog()
print "Regular bark:", big_dog.bark()  # Grr!
big_dog.make_alpha()
print "Enhanced bark:", big_dog.bark()  # GRR!
big_dog.revert_alpha()
print "Back to regular:", big_dog.bark()  # Grr!

Cons:

  • Yet another level of indirection (performance hit)

Pros:

  • You can call the original implementation from the enhanced one
  • You can composed enhanced actions

Upvotes: 0

Savir
Savir

Reputation: 18418

There's a quite dirty and confusing solution, but that will probably do what you want: It relies on the fact that instance attributes are evaluated before class attributes:

class Dog(object):
    noise = "Woof!"

    def bark(self):
        print self.noise

    def make_alpha(self):
        self.noise = self.__class__.noise.upper()

    def remove_alpha(self):
        try:
            del self.noise
        except AttributeError:
            print ("You tried to call remove_alpha into an"
                   " instance that doesn't have its own noise!!")

class Bulldog(Dog):
    noise = "Grr!"

if __name__ == '__main__':
    bulldog = Bulldog()
    bulldog.bark()
    print "Checkpoint: noise is NOT among the instance's vars: %s" % vars(bulldog)
    bulldog.make_alpha()
    print "Checkpoint: now noise is among the instance's vars: %s" % vars(bulldog)
    bulldog.bark()
    bulldog.remove_alpha()
    print "Checkpoint: noise is NOT among the instance's vars: %s" % vars(bulldog)
    bulldog.bark()

    print "Second test:"
    bulldog02 = Bulldog()
    bulldog02.bark()
    print "Checkpoint: noise is NOT among the instance's vars: %s" % vars(bulldog)
    bulldog02.remove_alpha()
    print "Checkpoint: noise is NOT among the instance's vars: %s" % vars(bulldog)
    bulldog02.bark() 

Which outputs:

Grr!
Checkpoint: noise is NOT among the instance's vars: {}
Checkpoint: now noise is among the instance's vars: {'noise': 'GRR!'}
GRR!
Checkpoint: noise is NOT among the instance's vars: {}
Grr!
Second test:
Grr!
Checkpoint: noise is NOT among the instance's vars: {}
You tried to call remove_alpha into an instance that doesn't have its own noise!!
Checkpoint: noise is NOT among the instance's vars: {}
Grr!

What's happening is that when you call make_alpha, a new noise attribute is added to the instance, which supersedes the noise attribute on the class level. You may want to check what the built-in vars does.

Upvotes: 1

The superclass variable is always available as self.__class__.noise or EnormousBulldog.noise, or getattr(self.__class__, 'noise').

Or if your question is "how do you cancel the per-instance changes", then del the attribute from the instance, possibly with a guard:

def remove_alpha(self):
    if 'noise' in self.__dict__:
        del self.noise

After that the attribute lookup would find it in the superclass.


And no, do not confuse __getattr__ with __getattribute__; you want to override the former __getattr__; the latter is almost certainly never what you want to do.

Upvotes: 3

Related Questions