Question

I need to generate a class that would mimic another class's method set and behave like the latter via a proxy. e.g. If Base is the class to mimic and Deleguate is the class which needs to act as Base then:

b = Base(args)
b.any_function()

is strictly equivalent to

d = Deleguate(b)
d.any_function()

If Deleguate uses a function that already exists in Base, it won't be overwritten. It's the kind of behavior you expect with inheritance and method overriding. Inheritance is not an option in the context of the project I'm working on (among other constraint, I don't have access to the factory code). And that's what makes things complicated.

I therefore decided to code a "proxy" decorator :

import inspect

def proxy(bridge, target):
    def proxyfy(cls):
        for _, func in inspect.getmembers(target, predicate=inspect.ismethod):
            fname = func.__name__
            if fname in cls.__dict__:
                print 'ignoring %s.%s' % (cls, fname)
                continue
            print 'adding %s.%s' % (cls, fname)
            def proxy_func(self, *args, **kwargs):
                print 'calling %s.%s.%s' % (cls, bridge, fname)
                bridge_member = getattr(self, bridge)
                return getattr(bridge_member, fname)(*args, **kwargs)
            setattr(cls, fname, proxy_func)
        return cls
    return proxyfy

class Base(object):
    def __init__(self, i):
        self._i = i

    def __bar(self):
        print 0

    def foo(self):
        print self._i

    def foo2(self):
        print 2 * self._i


@proxy('_proxy', Base)
class Deleguate(object):
    def __init__(self, base):
        self._proxy = base

    def foo2(self):
        print 4 * self._proxy._i

d = Deleguate(Base(1))
d.__bar() # d._proxy.__bar()
d.foo()   # d._proxy.foo()
d.foo2()  # d.foo2()

I get the following output :

adding <class '__main__.Deleguate'>.__bar
ignoring <class '__main__.Deleguate'>.__init__
adding <class '__main__.Deleguate'>.foo
ignoring <class '__main__.Deleguate'>.foo2
calling <class '__main__.Deleguate'>._proxy.foo2
2
calling <class '__main__.Deleguate'>._proxy.foo2
2
4

I thought that setattr(cls, fname, proxy_func) would assign a new closure, but the arguments are overwritten at each loop step and only the arguments of the last function foo2 are kept. Therefore calling any "generated" function of Deleguate uses foo2 arguments...

Why are the closure arguments being overwritten ? Is there a way to generate that kind of proxy code ? The expected output is :

adding <class '__main__.Deleguate'>.__bar
ignoring <class '__main__.Deleguate'>.__init__
adding <class '__main__.Deleguate'>.foo
ignoring <class '__main__.Deleguate'>.foo2
calling <class '__main__.Deleguate'>._proxy.__bar
0
calling <class '__main__.Deleguate'>._proxy.foo
1
4
Was it helpful?

Solution

Functions create closures, loops do not. The variable name fname is a local variable in proxyfy. The nested function proxy_func refers to this local variable. But at the time when the nested function is called, the for-loop

    for _, func in inspect.getmembers(target, predicate=inspect.ismethod):

has completed, and the local variable fname references its last value at the end of the loop, which happens to be 'foo2'. So no matter what method you call, each proxy_func ends up calling foo2.

To bind different values of fname to each proxy_func, you could use a new keyword parameter, bname with a default value. The default value is bound to the function at definition-time not at the time the function in run. So if use

    for bname, func in inspect.getmembers(target, predicate=inspect.ismethod):

and use this bname as the default value:

        def proxy_func(self, bname=bname, *args, **kwargs):

then each proxy_func will call the appropriate bname.

So, with minimal changes to your code, you could add a keyword parameter with default value to proxy_func to remember the current method name:

def proxy(bridge, target):
    def proxyfy(cls):
        for bname, func in inspect.getmembers(target, predicate=inspect.ismethod):
            fname = func.__name__
            if fname in cls.__dict__:
                print 'ignoring %s.%s' % (cls, fname)
                continue
            print 'adding %s.%s' % (cls, fname)
            def proxy_func(self, bname=bname, *args, **kwargs):
                print 'calling %s.%s.%s' % (cls, bridge, bname)
                bridge_member = getattr(self, bridge)
                return getattr(bridge_member, bname)(*args, **kwargs)
            setattr(cls, fname, proxy_func)
        return cls
    return proxyfy

However, I think using __getattr__ might be easier:

def proxy(bridge):
    def proxyfy(cls):
        def __getattr__(self, attr):
            target = getattr(self, bridge)
            if attr.startswith('__') and not attr.endswith('__'):
                # unmangle
                attr = '_{}{}'.format(type(target).__name__, attr)
            return getattr(target, attr)
        setattr(cls, '__getattr__', __getattr__)
        return cls
    return proxyfy

Here's a runnable example:

import inspect

def proxy(bridge, target):
    def proxyfy(cls):
        for bname, func in inspect.getmembers(target, predicate=inspect.ismethod):
            fname = func.__name__
            if fname in cls.__dict__:
                print 'ignoring %s.%s' % (cls, fname)
                continue
            print 'adding %s.%s' % (cls, fname)
            def proxy_func(self, bname=bname, *args, **kwargs):
                print 'calling %s.%s.%s' % (cls, bridge, bname)
                bridge_member = getattr(self, bridge)
                return getattr(bridge_member, bname)(*args, **kwargs)
            setattr(cls, fname, proxy_func)
        return cls
    return proxyfy

def proxy(bridge):
    def proxyfy(cls):
        def __getattr__(self, attr):
            target = getattr(self, bridge)
            if attr.startswith('__') and not attr.endswith('__'):
                # unmangle
                attr = '_{}{}'.format(type(target).__name__, attr)
            return getattr(target, attr)
        setattr(cls, '__getattr__', __getattr__)
        return cls
    return proxyfy

class Base(object):
    def __init__(self, i):
        self._i = i

    def __bar(self):
        print 0

    def foo(self):
        print self._i

    def foo2(self):
        print 2 * self._i


# @proxy('_proxy', Base)
@proxy('_proxy')
class Delegate(object):
    def __init__(self, base):
        self._proxy = base

    def foo2(self):
        print 4 * self._proxy._i

d = Delegate(Base(1))
d.__bar() # d._proxy.__bar()
d.foo()   # d._proxy.foo()
d.foo2()  # d.foo2()
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top