0

I have the following example:

class A:
    value = None
    @property
    def value(self):
        if not value:
            result = <do some external call>
            self.value = result
        return self.value

However, this is not working since I'm getting the exception:

AttributeError: can't set attribute

And that makes sense, but what would be the idiomatic way of doing something like this? I don't just want to use different names.

user3139545
  • 6,882
  • 13
  • 44
  • 87
  • Possible duplicate of https://stackoverflow.com/questions/4037481/caching-attributes-of-classes-in-python – bdesham Jun 19 '18 at 17:46
  • This code makes zero sense. Why do you place a `value=None` as a class variable, only to immediately replace it with a `property`? You can't set the attribute because you haven't defined a setter. Also, `if not value:` would throw a `NameError` since `value` is not defined, so this code wouldn't reach teh `AttributeError`. – juanpa.arrivillaga Jun 19 '18 at 17:46

1 Answers1

8

You're trying to use self.value as both the name of the property, and the name of the attribute used under the covers to store that property. That doesn't make sense; you need to give it a different name. Since that attribute is meant to be "private", and accessed only through the property, you normally want to use an underscore prefix.

In other words, do exactly the same thing done in the example in the docs for property.

And of course that if not value: has to be changed to access the same attribute via self, not some local variable with the same name.

class A:
    _value = None
    @property
    def value(self):
        if not self._value:
            result = <do some external call>
            self._value = result
        return self._value

Another option is to use a library that does this for you. As pointed out by Martijn Pieters in the comments, cached-property gives you exactly what you want. And it's tested and robust in all kinds of edge cases you likely never thought about, like threads and asyncio. And it's also got great documentation, and readable source code, that explains how it works.


Anyway, it may not be immediately obvious why that _value = None is correct.

_value = None in the class definition creates a class attribute named _value, shared by all instances of the class.

self._value = result in a method body doesn't change that class attribute, it creates an instance attribute with the same name.

Instance attributes shadow class attributes. That is, when you try to access self._value in the if and return statements, that looks for an instance attribute, then falls back to a class attribute if one doesn't exist. So, the class attribute's None value works as a default value for the instance attribute.

For providing default attribute values, this is a pretty common idiom—but it can be confusing in some cases (e.g., with mutable values).

Another way to do the same thing is to just force the instance to always have a _value attribute, by setting it in your __new__ or __init__ method:

class A:
    def __init__(self):
        self._value = None
    @property
    def value(self):
        if not self._value:
            result = <do some external call>
            self._value = result
        return self._value

While this is a bit more verbose and complex at first glance (and requires knowing about __init__), it does avoid the potential for confusion about class vs. instance attributes, so if you're sharing your code with novices, it's more likely they'll be able to follow it.

Alternatively, you can use getattr to see whether the attribute exists, so you don't need either a fallback class attribute or an __init__, but this is usually not what you want to do.


Taking a step back, you can even replace property with a descriptor that replaces itself on first lookup—but if you don't know what that means (even after reading the HowTo), you probably don't want to do that either.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • 1
    @juanpa.arrivillaga I've added some explanation. – abarnert Jun 19 '18 at 17:53
  • Another option would be to use a descriptor without `__set__` or `__delete__` methods, so you can simply set `self.__dict__['name'] = value` and never invoke the descriptor again on subsequent accesses. – Martijn Pieters Jun 19 '18 at 17:54
  • 2
    @MartijnPieters Yeah, but I don't know if I want to try to explain that to someone who's having trouble with the fundamentals, and I don't like giving people magic invocations that they can copy and paste without any hope of understanding and never be able to debug or modify. If you think you can explain it, write a separate answer. – abarnert Jun 19 '18 at 17:57
  • Thanks for this, I learn so much from this answer that has confused me! – user3139545 Jun 19 '18 at 17:57
  • 2
    @abarnert: fair enough; just wanting to put that out there too. Do write for a future audience too, not just the current OP, and then consider if you can make the non-data descriptor option clear enough. Or just point to [PyDanny's `cached_property` project](https://pypi.org/project/cached-property/) with a short intro. – Martijn Pieters Jun 19 '18 at 18:00
  • 2
    @MartijnPieters I didn't know about `cached_property`; that does seem worth including. But as for writing for a future audience—honestly, I don't think anyone who understands descriptors is going to be looking for anything in this question, and I don't think trying to teach descriptors is doable in the middle of an already-long answer about the basics of using `@property`, `_private` attributes, etc. (I think even explaining class vs. instance attributes it pushing it…) – abarnert Jun 19 '18 at 18:05