Reputation: 3243
I have a use case where I would like a Python @property
to act differently if called as a method (i.e, with parenthesis at the end) than it would if I called it without the parenthesis. Is anything like this possible.
class Sequence:
@property
def first(self):
return self._first
@first.setter
def first(self, v):
self._first = v
# This won't work
@first.method
def first(self):
# Do something different than the setter and the getter since
# `first` is being called as a method.
return 4321
seq = Sequence()
seq.first = 1234
# Setting and getting the first property works fine
assert seq.first == 1234
# Calling the first property as a function fails
assert seq.first() == 4321
Upvotes: 2
Views: 303
Reputation: 152587
Well, you could make the getter return a Proxy that either behaves like the value returned from the getter
or behaves like a callable in case you call it. That will be ambiguous if you return something that's callable from the getter but in your case you do return integers (uncallable). It's still inadvisable but you could make it work (based on the property
emulator mentioned in the Python descriptor how-to):
def make_callable_proxy(val, call_func):
class CallableProxy(type(val)): # subclass the class of value
__call__ = call_func
return CallableProxy(val)
class CallableProperty(object):
def __init__(self, fget=None, fset=None, fdel=None, fcall=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.fcall = fcall
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
# The following implements the "callable part".
if self.fcall is None:
return self.fget(obj)
value = make_callable_proxy(self.fget(obj), self.fcall)
return value
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.fcall, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.fcall, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.fcall, self.__doc__)
def method(self, fcall):
return type(self)(self.fget, self.fset, self.fdel, fcall, self.__doc__)
class Sequence(object):
@CallableProperty
def first(self):
return self._first
@first.setter
def first(self, v):
self._first = v
# THIS WILL WORK NOW
@first.method
def first(self):
# Do something different than the setter and the getter since
# `first` is being called as a method.
return 4321
seq = Sequence()
seq.first = 1234
# Setting and getting the first property works fine
assert seq.first == 1234
# Calling the first property as a function fails
assert seq.first() == 4321
This could be further refined by utilizing a real proxy class (like wrapt.ObjectProxy
) instead of the CallableProxy
class. But that depends on the availability of such packages. Just in case you have wrapt
this is how it would look like:
from wrapt import ObjectProxy
def make_callable_proxy(val, call_func):
class CallableProxy(ObjectProxy):
__call__ = call_func
__repr__ = val.__repr__ # just for a nicer representation
return CallableProxy(val)
Upvotes: 1
Reputation: 10863
I don't think there's a way to have @property
work this way, but depending on your use case, one possible alternative would be to have the property
return something that ducktypes as your value, but is actually a callable returning the value of your function:
class CallableWrapper:
def __init__(self, value):
self._value = value
def __call__(self):
return 4321
def __eq__(self, other):
return self._value == other
def __getattr__(self, name):
return getattr(self._value, name)
class Sequence:
@property
def first(self):
return CallableWrapper(self._first)
@first.setter
def first(self, v):
self._first = v
seq = Sequence()
seq.first = 1234
# Setting and getting the first property works fine
assert seq.first == 1234
# Calling the first property
assert seq.first() == 4321
There is likely additional magic you can do to get a syntax quite similar to the syntax you've described for assigning the __call__()
method to the wrapper. This is only useful to the extent that you are willing to have the property return a wrapper of a different type rather than the original value itself, of course.
That said, it's probably not a great idea to try to overload a property like this, as it will likely make your code harder to read and understand.
Upvotes: 0
Reputation: 530853
You need to look at how decorator syntax desugars:
@dec
def foo():
pass
becomes
def foo():
pass
foo = dec(foo)
while
@dec(1)
def foo():
pass
becomes
d = dec(1)
def foo():
pass
foo = d(foo)
In the first case, the decorator is a function that is called with the decorated function as an argument. In the second, the decorator is called with 1
as an argument and returns a function that takes the decorated function as a argument.
In other words, you are asking if there is a way to define a function dec
whose behavior depends on how its return value is used. The function can't look that far into the future.
Upvotes: 0