Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Remove a property (getter/setter) from an attribute in subclass in python

This may have been answered somewhere else, but I was wondering if there was any way to remove an attribute/method decorated with @property in a subclass.

Example:

from datetime import datetime

class A():
    def __init__(self, num):
        self._num = num

    @property
    def id(self):
        return self._num * datetime.now().timestamp()

class B(A):
    def __init__(self, id, num):
        super().__init__(num)
        self.id = id

The above code does not run if you attempt to create an instance of class B. AttributeError: can't set attribute

The base class uses a property because it needs to evaluate its ID on the fly, while my sub class is able to know its ID when it is created. The id attribute is accessed OFTEN, and I am seeing a significant performance hit because I have to use a property to serve this attribute, instead of just accessing it directly. (From what I have read, properties increase time-to-access by 5x). My application is currently spending around 10% of runtime getting this property.

Is there any way I can short-circuit the property in a sub class?

like image 978
wakey Avatar asked Mar 05 '23 18:03

wakey


1 Answers

I'm going to go through several possibilities here. Some of them do what you literally asked. Some of them don't, but they may be better options anyway.

First, your example base class changes the value of obj.id on every access due to the passage of time. That's really bizarre and doesn't seem like a useful concept of "ID". If your real use case has a stable obj.id return value, then you can cache it to avoid the expense of recomputation:

def __init__(self):
    ...
    self._id = None
@property
def id(self):
    if self._id is not None:
        return self._id
    retval = self._id = expensive_computation()
    return retval

This may mitigate the expense of the property. If you need more mitigation, look for places where you access id repeatedly, and instead, access it once and save it in a variable. Local variable lookup outperforms attribute access no matter how the attribute is implemented. (Of course, if you actually do have weird time-variant IDs, then this sort of refactoring may not be valid.)

Second, you can't override a property with a "regular" attribute, but you can create your own version of property that can be overridden this way. Your property blocks attribute setting, and takes priority over "regular" attributes even if you force an entry into the instance __dict__, because property has a __set__ method (even if you don't write a setter). Writing your own descriptor without a __set__ would allow overriding. You could do it with a generic LowPriorityProperty:

class LowPriorityProperty(object):
    """
    Like @property, but no __set__ or __delete__, and does not take priority
    over the instance __dict__.
    """
    def __init__(self, fget):
        self.fget = fget
    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        return self.fget(instance)

class Foo(object):
    ...
    @LowPriorityProperty
    def id(self):
        ...

class Bar(Foo):
    def __init__(self):
        super(Bar, self).__init__()
        self.id = whatever
    ...

Or with a role-specific descriptor class:

class IDDescriptor(object):
    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        # Remember, self is the descriptor. instance is the object you're
        # trying to compute the id attribute of.
        return whatever(instance)

class Foo(object):
    id = IDDescriptor()
    ...

class Bar(Foo):
    def __init__(self):
        super(Bar, self).__init__()
        self.id = whatever
        ...

The role-specific descriptor performs better than the generic LowPriorityProperty, but both perform worse than property due to implementing more logic in Python instead of C.

Finally, you can't override a property with a "regular" attribute, but you can override it with another descriptor, such as another property, or such as the descriptors created for __slots__. If you're really, really pressed for performance, __slots__ is probably more performant than any descriptor you could implement manually, but the interaction between __slots__ and the property is weird and obscure and you'll probably want to leave a comment explaining what you're doing.

class Foo(object):
    @property
    def id(self):
        ...

class Bar(Foo):
    __slots__ = ('id',)
    def __init__(self):
        super(Bar, self).__init__()
        self.id = whatever
    ...
like image 59
user2357112 supports Monica Avatar answered Apr 06 '23 13:04

user2357112 supports Monica