Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 147 lines
  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.

8 comments

Danny G (author) 14 years, 9 months ago  # | flag

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.

Gabriel Genellina 14 years, 9 months ago  # | flag

What about "downgrading"? Add these 3 lines at the end:

del r.firmware
r.power_on()
r.laser_eyes(300)

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).

Danny G (author) 14 years, 9 months ago  # | flag

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.

Danny G (author) 14 years, 9 months ago  # | flag

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.

Gabriel Genellina 14 years, 9 months ago  # | flag

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

James Mills 14 years, 9 months ago  # | flag

Have you seen my library (1) circuits ? A similar although somewhat different approach, an event-driven model.

cheers James

  1. http://trac.softcircuit.com.au/circuits/
Danny G (author) 14 years, 9 months ago  # | flag

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.

James Mills 14 years, 9 months ago  # | flag

@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

Created by Danny G on Tue, 21 Jul 2009 (MIT)
Python recipes (4591)
Danny G's recipes (2)

Required Modules

  • (none specified)

Other Information and Tasks