How to Use Backing Properties (_foo_property) in the Initializer to achieve Lazy Loading of Python Properties
The aim of this page📝 is to explain the concept of Python properties with the focus on the use of backing attributes based on the particular example of a Python class for automating GCP IAM access configurations. The usecase is to print a URL of GCP Dashboard which involves a project_id. For that project_id, I need to make a networking call to Consul KV. However, I just want to call once, not each time the property is needed.
2 min readOct 25, 2023
Note: yes, naming sucks. backing property is a regular attribute/field in Python’s world and its concept is taken from elsewhere (for more, see On Backing Property in Kotlin and Java)
EXPLANATION
- For more on properties in general see https://medium.com/p/3151c7131c04
- In short, Python properties (the ones declared via
@property
decorator) are a way to define special methods that get called when an attribute is accessed (getter
) or modified (setter
). - These methods allow you to control how the attribute is accessed or modified, and they can include any logic you want.
- A common use case for properties is to implement ‘lazy loading’, where the value of an attribute is not computed until it’s actually needed.
- This can be useful for values that are expensive to compute, or that depend on some external resource (like a database or API).
- In this case, you can store the value in a ‘backing attribute’ once it’s computed, and reuse this value in subsequent accesses to avoid unnecessary computations.
- The backing attribute is usually a regular attribute with a similar name to the property, but prefixed with an underscore (e.g.,
_my_prop
for a propertymy_prop
). - This naming convention signals that the backing attribute is intended for internal use within the class, and should not be accessed directly from outside the class.
- However, Python doesn’t enforce any access restrictions based on attribute names, so this is just a convention and not a hard rule.
- When defining a property, you start by defining a method for the getter, and decorate it with
@property
. - This method should take no arguments (other than
self
), and return the value of the attribute. - If you want to define a setter for the property, you define another method with the same name, and decorate it with
@<property_name>.setter
. - This method should take one argument (in addition to
self
), which is the new value for the attribute.
CODE
Here is a particular example I have experienced:
class AccessConfig:
def __init__(self, client_name: str, env: str):
self.client_name = client_name
self.env = env
self._project_id = None
@property
def project_id(self):
if self._project_id is None:
self._project_id = get_consul_key_for_client(key=f"gcp_setup_{self.env}/output/project",
client=self.client_name)
return self._project_id
In this code, project_id
is a property that's lazily loaded from an external source using get_consul_key_for_client
. The value is stored in _project_id
once it's retrieved, and reused in subsequent accesses.