2

Thinking about this and I'm wondering if it is possible (and if so, how to make such a decorator etc.) to have a classmethod, that IF called from an instance, can retrieve data on the instance? Perhaps some more clarity on how the staticmethod and classmethod decorators work would be helpful too (looking at the implementation __builtin__.py did not help)

Example use would be:

class A(object):

    def __init__(self, y):
        self.y = y

    @classmethod
    def f(cls, x, y=None):

        # if y is unspecified, retrieve it from cls which is presumably an instance 
        # (should throw an error if its a class because y is not set
        if y is None:
            y = cls.y

        return x + y

So that we could do:

>>>A.f(3, 5)
8

>>>a = A(5)
>>>a.f(3)
8

I came up with this below to mimic the behavior but its pretty inconvenient to implement:

class A(object):

    def __init__(self, y):
        self.y = y
        self.f = self.f_

    def f_(self, x):
        return x + self.y

    @classmethod
    def f(cls, x, y):
        return x + y
The Dude
  • 330
  • 6
  • 16
  • So you want a function that acts both as a classmethod and as an instance method? – Adirio Mar 05 '18 at 15:25
  • I think you may have some misunderstanding of what classmethods are for. See [this question](https://stackoverflow.com/questions/12179271/meaning-of-classmethod-and-staticmethod-for-beginner) first for clarification on staticmethods and classmethods. – sytech Mar 05 '18 at 15:36
  • Yes, I want function that acts both as a classmethod and as an instance method (since classmethods can be called from instances too) I understand what classmethods are intended for, I'm asking how to create a decorator that has different behavior for this use case. Perhaps if there were a way to rebind a function through a decorator when __init__ is called we could achieve this. I'm sure it will be tricky but I'm curious nonetheless because this would be very "cool" – The Dude Mar 05 '18 at 15:40

4 Answers4

2

To expand on the comments made by @Adirio You could make a decorator that accomplishes this dynamically.

In this particular implementation, when the decorated method is called it will do a partial bind of the provided arguments to the method and uses the method's signature to determine what parameters have not been provided.

For any unspecified argument, if the calling object has an attribute matching the unspecified parameter name, the object's attribute value will be injected into the function.

import inspect
class BindableConstructor(object):
    def __init__(self, meth):
        self.meth = meth
        self.sig = inspect.signature(self.meth)

    def __get__(self, obj, klass=None):
        if obj is not None:
            print('Method ', repr(self.meth), ' called from instance ', repr(obj))
        if klass is None:
            klass = type(obj)
        def newmeth(*args, **kwargs):
            ba = self.sig.bind_partial(*args, **kwargs)
            ba.apply_defaults()
            for paramname in self.sig.parameters:
                if paramname not in ba.arguments and hasattr(obj, paramname):
                    ba.arguments[paramname] = getattr(obj, paramname)
            return self.meth(klass, *ba.args, **ba.kwargs)
        return newmeth

Then suppose you have the following class using this decorator

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

    @BindableConstructor
    def my_constructor(cls, x, y):
        return cls(x + y)

Then the following behavior would be observed

>>> a = MyClass(5)
>>> b = MyClass.my_constructor(3, 2)
>>> b
<__main__.MyClass object at 0x0605C770>
>>> b.y
5
>>> c = b.my_constructor(3) # c.y == b.y + 3
Method  <function MyClass.my_constructor at 0x05396420>  called from instance  <__main__.MyClass object at 0x0605C770>
>>> c.y
8

In this particular case ba.apply_defaults is called before checking the object's attributes to inject. If you want the object's attributes to take precedence over defaults, call ba.apply_defaults after the parameter injection logic.

sytech
  • 29,298
  • 3
  • 45
  • 86
  • I like this but is it only for Python 3? Inspect module doesn't have signature in Python2.7 – The Dude Mar 05 '18 at 21:39
  • @TheDude, `inspect.signature` was introduced in Python3. In Python2 there is [`inspect.getargspec`](https://docs.python.org/2/library/inspect.html#inspect.getargspec), and `inspect.getcallargs` which should provide information necessary to accomplish something analogous to this. There are also 3rd party [packages that claim to add this functionality for Python2](https://pypi.python.org/pypi/funcsigs) – sytech Mar 05 '18 at 22:11
  • `import inspect` is strictly prohibited in my production code – Adirio Mar 07 '18 at 09:08
  • How unfortunate @Adirio. I can identify with the sentiment, but there are plenty of production-suitable use cases for `inspect`. Everybody has unique needs, do what's right for you. That said, I should have probably made more clear that, if the only goal is to determine the calling instance, that is accomplished in the first two lines of the `__get__` method and `inspect` is not required. I was just trying to apply the idea in some meaningful way in the following lines. – sytech Mar 07 '18 at 13:44
1

When you try you example, you get an error saying AttributeError: type object 'A' has no attribute 'y', because in constructor, you assigned y as an attribute of the object and not of the class.

The trivial fix:

class A(object):

    def __init__(self, y):
        A.y = y

    @classmethod
    def f(cls, x, y=None):

        # if y is unspecified, retrieve it from cls which is presumably an instance 
        # (should throw an error if its a class because y is not set
        if y is None:
            y = cls.y

        return x + y

Would indeed solve the error, but as the class will only know one single object at a time, you would get weird result as soon as you use more than one:

>>> A.f(3,5)
8
>>> a = A(5)
>>> a.f(3)             # fine till there...
8
>>> b = A(7)
>>> a.f(3)             # last created object wins here!
10

So the only foolproof way is to create an attribute with the name of the class function in each object. As you only call a class method, a lamdba is enough:

class A(object):

    def __init__(self, y):
        self.y = y
        self.f = lambda x: A.f(x, y)   # declare a shortcut for the class method

    @classmethod
    def f(cls, x, y=None):
        return x + y

You can then safely do:

>>> A.f(3,5)
8
>>> a = A(5)
>>> a.f(3)
8
>>> b = A(7)
>>> a.f(3)
8
>>> b.f(3)
10
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • Thank you, this is what I was working towards as well, I'm wondering if its possible to get this rebinding behavior via the decorator (for python coolness) – The Dude Mar 05 '18 at 15:43
  • 1
    @TheDude decorators are executed when evaluating the class not when creating an instance, so I expect the answer to your question to be no. You may be able to get this kind of behaviour with meta-classes though – Adirio Mar 05 '18 at 17:09
  • 1
    Actually don't pay attentio to my comment, it may be doable with decorators, as `classmethod` can be implemented in pure Python. https://docs.python.org/3/howto/descriptor.html – Adirio Mar 05 '18 at 17:13
  • @Adirio: it can certainly be implemented with decorators, but without a clear specification of how the class method can be declared (what about the number of parameters, whether they can have keywords or be keyword only, whether an aggregate list like `*args` is allowed) and how the instance one will be called (how many parameters associated to the instance, where they should go), a fully general decorator will be hard to implement in Python3 and even harder in Python2 where you will at least lose any signature information. – Serge Ballesta Mar 05 '18 at 17:24
  • Thank you for the link, see the post for what I came up with. – The Dude Mar 05 '18 at 17:36
1

Do not forget to handle error cases.

class InstanceAndClassMethod(object):

    def __init__(self, f):
        self.f = f

    def __get__(self, instance, owner=None):
        if instance is None:
            instance = owner
        def newfunc(*args, **kwargs):
            return self.f(instance, *args, **kwargs)
        return newfunc

class A(object):

    def __init__(self, y):
        self.y = y

    @InstanceAndClassMethod
    def f(cls, x, y=None):
        try:
            y = cls.y if y is None else y
        except AttributeError:
            raise TypeError("f() missing 1 required positional argument: 'y'")
        return x + y
Adirio
  • 5,040
  • 1
  • 14
  • 26
0

With the help of docs.python.org/3/howto/descriptor.html I came up with this, seems to work:

class CoolerClassMethod(object):

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass):
        if obj is None:
            self_ = klass
        else:
            self_ = obj
        def newfunc(*args, **kwargs):
            return self.f(self_, *args, **kwargs)
        return newfunc

class A(object):

    def __init__(self, y):
        self.y = y

    @CoolerClassMethod
    def f(cls, x, y=None):
        y = cls.y if y is None else y
        return x + y

Testing:

>>> a = A(5)
>>> A.f(3, 5)
8
>>> a.f(3)
8
>>> A.f(3, 5)
8
The Dude
  • 330
  • 6
  • 16
  • I gave an other answer proposing some modifications to your code to handle error cases so that it shows the appropiate error. – Adirio Mar 06 '18 at 11:25