Reputation: 113
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
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:
Pros:
Upvotes: 0
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
Reputation: 133919
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