My desire was to design a class with defined attributes that when assigned on instances, would expand the instance's functionality. In other words if I create an instance of class A, then assign a 'component' attribute upon that instance, I should be able to call methods of the component object through the original instance. I believe this is somewhat similar to interfaces and abstract base classes (and I read up on both a bit), but I want to rely more on introspection of the object to see what it can do versus confining it to a set interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | def _get_combined_method(method_list):
def new_func(*args, **kwargs):
[m(*args, **kwargs) for m in method_list]
return new_func
def component_method(func):
""" method decorator """
func._is_component_method = True
return func
class Component(object):
""" data descriptor """
_is_component = True
def __init__(self):
self._cache = {} # id(instance) -> component obj
def __get__(self, instance, owner):
if instance is None:
return self
else:
return self._cache.get(id(instance), None)
def __set__(self, instance, value):
self._cache[id(instance)] = value
self._refresh_component_methods(instance)
def __delete__(self, instance):
# delete this instance from the cache
del(self._cache[id(instance)])
self._refresh_component_methods(instance)
def _refresh_component_methods(self, instance):
icls = instance.__class__
# get all components defined in instance cls
components = []
for attr in dir(icls):
obj = getattr(icls, attr)
if getattr(obj, '_is_component', False):
comp = getattr(instance, attr, None)
if comp is not None:
components.append(comp)
# clear all of the current instance _component_methods
icms = getattr(instance, '_instance_component_methods', [])
for meth in icms:
delattr(instance, meth)
# generate new set of instance component methods
icms = {}
for c in components:
ccls = c.__class__
for attr in dir(ccls):
obj = getattr(ccls, attr)
if getattr(obj, '_is_component_method', False):
if attr not in icms:
icms[attr] = []
icms[attr].append(getattr(c, attr))
# also maintain the instance's class original functionality
for attr, meths in icms.items():
obj = getattr(icls, attr, None)
if obj is not None:
if callable(obj):
icms[attr].insert(0, getattr(instance, attr))
else:
raise ValueError("Component method overrides attribute!")
# assign the methods to the instance
for attr, meths in icms.items():
if len(meths) == 1:
setattr(instance, attr, icms[attr][0])
else:
setattr(instance, attr, _get_combined_method(meths))
# write all of the assigned methods in a list so we know which ones to
# remove later
instance._instance_component_methods = icms.keys()
if __name__ == "__main__":
class Robot(object):
firmware = Component()
arm = Component()
def power_on(self):
print 'Robot.power_on'
def kill_all_humans(self):
""" demonstrates a method that components didn't take over """
print 'Robot.kill_all_humans'
class RobotFW(object):
@component_method
def power_on(self):
print 'RobotFW.power_on'
self.power_on_checks()
def power_on_checks(self):
""" demonstrates object encapsulation of methods """
print 'RobotFW.power_on_checks'
class UpgradedRobotFW(RobotFW):
""" demonstrates inheritance of components possible """
@component_method
def laser_eyes(self, wattage):
print "UpgradedRobotFW.laser_eyes(%d)" % wattage
class RobotArm(object):
@component_method
def power_on(self):
print 'RobotArm.power_on'
@component_method
def bend_girder(self):
print 'RobotArm.bend_girder'
r = Robot()
print dir(r)
r.power_on()
print '-'*20 + '\n'
r.firmware = RobotFW()
print dir(r)
r.power_on()
print '-'*20 + '\n'
print "try to bend girder (demonstrating adding a component)"
try:
r.bend_girder()
except AttributeError:
print "Could not bend girder (I have no arms)"
print "adding an arm..."
r.arm = RobotArm()
print "try to bend girder"
r.bend_girder()
print dir(r)
print '-'*20 + '\n'
print "upgrading firmware (demonstrating inheritance)"
r.firmware = UpgradedRobotFW()
r.power_on()
r.laser_eyes(300)
del(r.firmware)
try:
r.laser_eyes(300)
except AttributeError:
print "I don't have laser eyes!"
|
This code will look for methods decorated with 'component_method' that DON'T have the same name as an uncallable attribute on the base instance. If there is a method with the same name in the base instance and in other components, a new method will be created that will call all of them, but return None (it is up to the implementor to make sure the args match up) If there is only 1 method of that name between the base class and the component objects it will call that method and return whatever the method returns. You can still directly access the component's methods by calling through the descriptor attribute. (e.g. r.firmware.power_on())
Please play with it and let me know if there is a better/more concise way of performing the above (or if there are any bugs/questions). Thanks!
Future thoughts include creating a 'component_protected_method', which components cannot overload, and also thinking about perhaps a better return value for the combined methods. Another relatively simple optimization would be to make the Component instance's '_cache' attribute into a weakref dict.
NOTE I developed a different implementation in [Recipe 576854] which I believe is a bit cleaner from the user's standpoint.
I guess I can talk a little bit more about it. Another reason I was going for this was to have easily pluggable and unpluggable components to an instance without having to mess with the instance's class. I didn't want to have to reinstantiate the object to a different subclass everytime I wanted to switch one of the components.
What about "downgrading"? Add these 3 lines at the end:
and the robot still uses its enhanced capabilities even after you take them off :)
In general, I don't like the implementation very much - there is too much magic here. What I would like (you may have other ideas in mind) is something like this: I have an instance (of Robot in your example). I "plug" certain component into it. Now my Robot instance has new methods (or enhanced versions of old methods) that other Robot instances don't have. And if I "unplug" the component, I get back the original behavior.
In _get_combined_method, using a list comprehension just for its iteration effect isn't good style (you create a list just to discard it at the very next moment).
Ah good call on the delete attribute... I have fixed this by creating a 'refresh_component_methods' in the data descriptor. As for the list comprehension, I hadn't heard that this was "bad style"... but to each their own. Thanks for the review.
Ah I wish I could edit my posts ;) I always think of more to say. I do agree that this is quite a bit of magic, which is why I'm calling out for different suggestions. This might be a bit cleaner if implemented in a metaclass. I've looked at those as well, and have a general aversion to them and that is why I implemented mine through a data descriptor.
This is my implementation of your same idea. The main difference is that I maintain only a list of plugged components; the component methods are searched at runtime.
http://python.pastebin.com/f5ad43e46
Have you seen my library (1) circuits ? A similar although somewhat different approach, an event-driven model.
cheers James
UPDATE I know it's only been 1 day, but I believe I made a cleaner (from the user's point of view) implementation of what this is trying to accomplish in Recipe 576854.
@Gabriel Genellina: I looked at your code briefly (I will look at it more in depth shortly) and appreciate the feedback. I will post it in the code section of this recipe if you'd like. After I look at it some more I may have questions for you. Now I ask if you don't mind to tell me what you think of the new recipe (linked above). Please put any comments about it in its comment section instead of here. Thanks!
@James Mills: I went to your Trac page and glanced VERY briefly at some of the examples. It appears to be different in that you use a set interface (list of methods) of which the components should implement, and makes it easy for the outside object to know which methods it can call onto each component. My solution makes the components a bit more dynamic in that which methods that are available to call aren't known until runtime. This makes it harder for the introspecting object to know what it can call, but I believe as long as the component methods describe themselves well enough it should be fine.
@Danny: Actually this is a little incorrect. It's not methods that are the key in circuits, it's channels and event handlers. Have a 2nd look at /wiki/docs/Tutorial :)
But nice work with your Recipe(s) :)
--JamesMills