Jesse Hogan
Jesse Hogan

Reputation: 3243

Use Python @property's as propreties and methods

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

Answers (3)

MSeifert
MSeifert

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

Paul
Paul

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

chepner
chepner

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

Related Questions