How to access a class's property using a partialmethod?

1k views Asked by At

I have a need to create many similar functions in class definitions using their properties. To me, it makes perfect sense to use partial functions for this. However, the properties are not passing what I want to the partial methods (e.g. the property object is being passed, not what it resolves to).

Sample code:

from functools import partialmethod

def mk_foobar(*args):
    for a in args:
        print(a)
    print('bar')

class Foo(object):
    @property
    def foo(self):
        return 'foo'
    echo = partialmethod(mk_foobar, foo)

This yields:

> <__main__.Foo object at 0x04326A50>
> <property object at 0x0432A4E0>
> bar

What I want is:

> <__main__.Foo object at 0x04326A50>
> foo
> bar

In case there's a difference, I am also using descriptor classes sometimes instead of properties. It has the same error:

from functools import partialmethod

class Child(object):
    def __init__(self, item):
        self.item = item
    def __get__(self, instance, objtype):
        return 'child argument was: %s' % self.item

def mk_foobar(*args):
    for a in args:
        print(a)
    print('foobar')

class Foo(object):
    a = Child('b')
    echo = partialmethod(mk_foobar, a)

This yields:

<__main__.Foo object at 0x01427690>
<__main__.Child object at 0x01427190>
foobar

When I want:

<__main__.Foo object at 0x01427690>
'child argument was b'
foobar
2

There are 2 answers

0
Martijn Pieters On BEST ANSWER

A partialmethod object will only handle descriptor delegation for the function object, no for any arguments that are descriptors themselves. At the time the partialmethod object is created, there is not enough context (no class has been created yet, let alone an instance of that class), and a partialmethod object should not normally act on the arguments you added.

You could write your own descriptor object that would do the delegation:

from functools import partial, partialmethod

class partialmethod_with_descriptorargs(partialmethod):
    def __get__(self, obj, cls):
        get = getattr(self.func, "__get__", None)
        result = None
        if get is not None:
            new_func = get(obj, cls)
            if new_func is not self.func:
                args = [a.__get__(obj, cls) if hasattr(a, '__get__') else a
                        for a in self.args]
                kw = {k: v.__get__(obj, cls) if hasattr(v, '__get__') else v 
                      for k, v in self.keywords.items()}
                result = partial(new_func, *args, **kw)
                try:
                    result.__self__ = new_func.__self__
                except AttributeError:
                    pass
        if result is None:
            # If the underlying descriptor didn't do anything, treat this
            # like an instance method
            result = self._make_unbound_method().__get__(obj, cls)
        return result

This binds any descriptors in the arguments as late as possible, to the same context the method is bound to. This means your properties are looked up when looking up the partial method, not when the partialmethod is created nor when the method is called.

Applying this to your examples then produces the expected output:

>>> def mk_foobar(*args):
...     for a in args:
...         print(a)
...     print('bar')
... 
>>> class Foo(object):
...     @property
...     def foo(self):
...         return 'foo'
...     echo = partialmethod_with_descriptorargs(mk_foobar, foo)
... 
>>> Foo().echo()
<__main__.Foo object at 0x10611c9b0>
foo
bar
>>> class Child(object):
...     def __init__(self, item):
...         self.item = item
...     def __get__(self, instance, objtype):
...         return 'child argument was: %s' % self.item
... 
>>> class Bar(object):
...     a = Child('b')
...     echo = partialmethod_with_descriptorargs(mk_foobar, a)
... 
>>> Bar().echo()
<__main__.Bar object at 0x10611cd30>
child argument was: b
bar

The binding of any descriptor arguments takes place at the same time the method is bound; take this into account when storing methods for delayed calling:

>>> class Baz(object):
...     _foo = 'spam'
...     @property
...     def foo(self):
...         return self._foo
...     echo = partialmethod_with_descriptorargs(mk_foobar, foo)
... 
>>> baz = Baz()
>>> baz.foo
'spam'
>>> baz.echo()
<__main__.Baz object at 0x10611e048>
spam
bar
>>> baz_echo = baz.echo  # just the method
>>> baz._foo = 'ham'
>>> baz.foo
'ham'
>>> baz_echo()
<__main__.Baz object at 0x10611e048>
spam
bar
>>> baz.echo()
<__main__.Baz object at 0x10611e048>
ham
bar

If this is an issue, don't use partialmethod objects; use a decorator(like) approach instead:

from functools import wraps

def with_descriptor_defaults(*defaultargs, **defaultkeywords):
    def decorator(fn):
        @wraps(fn)
        def wrapper(self, *args, **kwargs):
            args = [a.__get__(self, type(self)) if hasattr(a, '__get__') else a
                    for a in defaultargs] + list(args)
            kw = {k: v.__get__(self, type(self)) if hasattr(v, '__get__') else v for k, v in defaultkeywords.items()}
            kw.update(kwargs)
            return fn(self, *args, **kw)
        return wrapper
    return decorator

and use this by passing in the defaults, then the function:

class Foo(object):
    @property
    def foo(self):
        return 'foo'
    echo = with_descriptor_defaults(foo)(mk_foobar)

but it can be used as a decorator too:

class Bar(object):
    @property
    def foo(self):
        return 'foo'

    @with_descriptor_defaults(foo)
    def echo(*args):
        for a in args:
            print(a)
        print('bar')

This resolves the descriptor defaults at calling time:

>>> class Baz(object):
...     _foo = 'spam'
...     @property
...     def foo(self):
...         return self._foo
...     echo = with_descriptor_defaults(foo)(mk_foobar)
... 
>>> baz = Baz()
>>> baz_echo = baz.echo
>>> baz._foo = 'ham'
>>> baz_echo()
<__main__.Baz object at 0x10611e518>
ham
bar
0
Daniel On

You have to access foo from a instance context self.foo. So you cannot use partialmethod here. Define a methode:

def echo(self, *args):
    return mk_foobar(self.foo, *args)