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
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With