On Python's @property decorator
Tomislav Maricevic
Posted on September 30, 2024
@property
decorator is an excellent way to reduce the readability of Python code. It obfuscates a perfectly good
function call and tricks readers into thinking they're performing a regular attribute access or assignment.
Unless there's a really good and explicit reason to do this, don't.
List of good and explicit reasons:
- Refactoring
That's pretty much it.
If you need to turn something that (rightfully so) started out as a simple attribute, but with time accrued some more
complex logic, @property is a good way to gracefully transition from attributes to function calls.
Version 1
We start out with a simple attribute. You can get it, you can set it. As a consenting adult, you're free to do with it
whatever you want.
class Client:
def __init__(self, value):
self.value = value
Version 2:
The project gains traction. You need to add two new features:
- Emit an event whenever the
Client.value
attribute is accessed, so other parts of the code can listen to it and do their own thing - You want a central place to validate values being assigned, to avoid littering the rest of your codebase with error handling
Because we're a self-aware smol brain developer, we like plain old functions. We craft a plan
to change the class interface to use getter/setter functions instead of direct attribute access. But since
we're also responsible and respectful to our colleagues/clients, we don't just change the API abruptly. No, we will be
emitting a deprecation warning for some time, and only introduce breaking changes in the API after we've given everyone
ample time to migrate.
import warnings
class Client:
def __init__(self, value):
# We add a private attribute to hold the value
self._value = None
self.set_value(value)
@property
def value(self):
# We can now emit a deprecation warning on
# each access, urging our users to migrate to the new API
warnings.warn("A.value is deprecated, use A.get_value() instead!", DeprecationWarning)
# ... and offload the act of retrieving the value
# to the newly-introduced function
value = self.get_value()
return value
@property.setter
def value(self, new_value):
warnings.warn("A.value is deprecated, use A.set_value() instead!", DeprecationWarning)
self.set_value(new_value)
# We add getter/setter functions with the new logic
def get_value(self):
self._emit_event('value_access')
return self._value
def set_value(self, new_value):
self._validate_value(new_value)
self._value = new_value
Version 3:
Time has passed, and people have migrated to the new API. We're ready to make our lives easier, and simplify the codebase
by removing the dirty @property
. Life is good again.
class Client:
def __init__(self, value):
self._value = None
self.set_value(value)
def get_value(self):
self._emit_event('value_access')
return self._value
def set_value(self, new_value):
self._validate_value(new_value)
self._value = new_value
Going a bit deeper
@property
is an example of a descriptor. Descriptors are a neat Python construct that "lets objects customize attribute
lookup, storage, and deletion". Some of the nicer things in life I
enjoy are made using descriptors, namely Django's ORM.
But just because you can doesn't mean you should. We always strive for the least complex option, and if you're certain
descriptors will make everyone's (not just yours!) lives easier, then go for it. Most of the time, though, plain
functions are the way to go.
Stop worrying and learn to love the function call.
Posted on September 30, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024