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.