setattr and getattr with methods
Asked Answered
H

2

9

I have a boiler platey class that delegates some actions to a reference class. It looks like this:

class MyClass():

    def __init__(self, someClass):            
        self.refClass = someClass

    def action1(self):
        self.refClass.action1()

    def action2(self):
        self.refClass.action2()

    def action3(self):
        self.refClass.action3()

This is the refClass:

class RefClass():

    def __init__(self):
        self.myClass = MyClass(self)

    def action1(self):
        #Stuff to execute action1

    def action2(self):
        #Stuff to execute action2

    def action3(self):
        #Stuff to execute action3

I'd like to use Python Metaprogramming to make this more elegant and readable, but I'm not sure how.

I've heard of setattr and getattr, and I think I could do something like

class MyClass():

    def __init__(self, someClass):            
        self.refClass = someClass

    for action in ['action1', 'action2', 'action3']:
        def _delegate(self):
            getattr(self.refClass, action)()

And then I know I need to do this from somewhere, I guess:

MyClass.setattr(action, delegate)

I just can't totally grasp this concept. I understand the basics about not repeating code, and generating the methods with a for loop with functional programming, but then I don't know how to call this methods from elsewhere. Heeeelp!

Harlanharland answered 28/9, 2015 at 17:7 Comment(2)
can't RefClass subclass MyClass and override the methods?Bustard
It might very well be that my architecture design is not the best, @Pynchia! This is probably out of the scope of the question, but this code is part of a GUI for a text editor. There are views (for the menu, the text area, etc), each of them controlled by a controller class (MenuController, TextAreaController, etc). When an action triggered from the menu needs to modify the text area, I delegate the action to a MainController, that is in charge of setting up the whole GUI and handle actions that imply more than one controller. So I don't think inheritance would be a possible solution here.Harlanharland
K
9

Python already includes support for generalized delegation to a contained class. Just change the definition of MyClass to:

class MyClass:

    def __init__(self, someClass):            
        self.refClass = someClass  # Note: You call this someClass, but it's actually some object, not some class in your example

    def __getattr__(self, name):
        return getattr(self.refClass, name)

When defined, __getattr__ is called on the instance with the name of the accessed attribute any time an attribute is not found on the instance itself. You then delegate to the contained object by calling getattr to look up the attribute on the contained object and return it. This costs a little each time to do the dynamic lookup, so if you want to avoid it, you can lazily cache attributes when they're first requested by __getattr__, so subsequent access is direct:

def __getattr__(self, name):
     attr = getattr(self.refClass, name)
     setattr(self, name, attr)
     return attr
Koehler answered 28/9, 2015 at 17:44 Comment(3)
Thanks, @ShadowRanger. This is exactly what I was looking for! And you are very right, I was wrongly naming an object someClass. I have marked this as the right answer, but I haven't tried it yet. Will do first thing tomorrow, but it looks like exactly what I needed.Harlanharland
You're welcome. A warning: If you're running on the Python reference interpreter prior to version 3.4 (where they finally fixed object finalization), the design from your question is definitely creating a reference cycle. If any object involved in the cycle has a __del__ clean up method, then the pre-3.4 cyclic garbage collector cannot handle it, and you'll leak memory every time you create an object like this and don't explicitly break the cycle (by clearing attributes) before losing the last reference to it.Koehler
:O I am using Python 2.7. Maybe it's time to rethink the design before going any further. These is very valuable advice, thanks again!Harlanharland
C
3

Personally, for delegating things I usually do something like that:

def delegate(prop_name, meth_name):
    def proxy(self, *args, **kwargs):
        prop = getattr(self, prop_name)
        meth = getattr(prop, meth_name)
        return meth(*args, **kwargs)
    return proxy

class MyClass(object):
    def __init__(self, someClass):
        self.refClass = someClass

    action1 = delegate('refClass', 'action1')
    action2 = delegate('refClass', 'action2')

This will create all delegate methods you need :)

For some explanations, the delegate function here just create a "proxy" function which will act as a class method (see the self argument?) and will pass all arguments given to it to the referenced object's method with the args and kwargs arguments (see *args and **kwargs? for more informations about these arguments)

You can create this with a list too, but I prefer the first because it's more explicit for me :)

class MyClass(object):
    delegated_methods = ['action1', 'action2']

    def __init__(self, someClass):
        self.refClass = someClass

        for meth_name in self.delegated_methods:
            setattr(self, meth_name, delegate('refClass', meth_name))
Cori answered 28/9, 2015 at 17:27 Comment(4)
Thanks, @Koka-El-Kiwi. Super username!! I don't think my Python is good enough to understand your delegate function. Is that function part of MyClass? I need some time to investigate on what *args and **kwargs exactly are 0:) Let me check...Harlanharland
No no, the delegate function doesn't have to be in your class, you can even put it in a "utils" module (or something like that, I leave the architecture design to you :p)Cori
I just edited my answer with some explanations about the delegate function and the *args/**kwargs thing :)Cori
Thanks for including the explanations, it's much clearer now and I've also learned about * and **. I'd like to accept your answer as well, but @ShadowRanger's was slightly easier to adopt.Harlanharland

© 2022 - 2024 — McMap. All rights reserved.