Explaining Properties in OOP Python
The aim of this page📝 is to simplistically illustrate Python’s concept of properties — the way to maintain/modify an object’s attributes, different from the usual getters and setters.
Here, I focus only on getter functions, also known as accessors, which are methods, conventionally named as verbs starting with get. What Python properties do is offer a different use of functionally similar implementation. For the object user/client, there is no difference between a simple instance attribute and a getter (which may compute any additional behavior before returning the value)
Our example object is a refrigerated container containing a class invariant for maximum temperature
- First, let’s look at the code
class RefrigeratedShippingContainer(ShippingContainer):
MAX_CELSIUS = 4.0
def __init__(self, owner_code, contents, *, celsius):
super().__init__(owner_code, contents)
if celsius > RefrigeratedShippingContainer.MAX_CELSIUS:
#^^^^^^^^^^^^^^^^^^^^^establishing a class_invariant: temperature must be below max
raise ValueError("Temperature too hot!")
#^^^^^^^^^^^^^^^^^^^^^break if arriving celsius argument has a value > 4.0
self.celsius = celsius
- the initializer is establishing an important class invariant that the temperature should not be below some specified maximum
- at this moment, the creation of an instance of the container object is the only way of defining the temperature
- there are 2 major handicaps:
- => 1. the temperature can be accessed only in its initial form
- => 2. the temperature cannot be changed later
In a digression, let me note the concept of invariant is a type of condition
- the term invariant is also used in the context of iteration/loops
- it is a constant condition that must hold for the iteration to proceed or object to be instantiated
- it is a constant condition for computing to proceed
- breaking the loop invariant means breaking out of the loop
- breaking the class invariant means breaking the program and raising an exception
Traditionally, the customized access to object values is implemented via Java-style getters methods (aka accessors)
- as is common, you need to create what is commonly known as backing field/property
- for more — on the etymology and comparative approach — see Explaining Backing Property in Kotlin and Java < Stack Overflow)
- in any case, Python uses underscore in this point as an implementation detail
- in addition, the underscore signifies that it is no longer to be considered to be part of the public interface
- also known as “discouraging meddling”
- for the sake of illustration of the value of the accessor, I am returning a dictionary containing both the max and current temperature
- …in other words, you may return the value in a customized/enriched/transformed format impossible with a simple instance attribute
""" THIS IS CLASSICAL JAVA IMPLEMENTATION """
class RefrigeratedShippingContainer(ShippingContainer):
MAX_CELSIUS = 4.0
def __init__(self, owner_code, contents, *, celsius):
super().__init__(owner_code, contents)
if celsius > RefrigeratedShippingContainer.MAX_CELSIUS:
raise ValueError("Temperature too hot!")
self._celsius = celsius
def get_celsius(self):
return {"max_temperature": RefrigeratedShippingContainer.MAX_CELSIUS,
"current_temperature": self._celsius}
>>> r = oop_11_17_test.RefrigeratedShippingContainer.create_with_items("MRP", ["fish"], celsius=3)
>>> r.get_celsius()
{'max_temperature': 4.0, 'current_temperature': 3}
- theoretically, you could make a mess by changing the value of an implementation detail
Python properties provide an alternative to Java’s accessor/mutators (getters/setters)
- setters (mutators) are not discussed in this post
- properties allow getters and setters to be exposed as seemingly regular attributes
- ..in the Java world, attributes are called fields and there are no properties
- ..in C#, there are properties similar to Python, see Properties Vs Fields In C#
- anyways, properties have elevated capabilities compared to attributes
- same as with static and class methods — in Python — decorators are the basis of the property system
- instead of getters and setters, we can do something like this
class RefrigeratedShippingContainer(ShippingContainer):
MAX_CELSIUS = 4.0
def __init__(self, owner_code, contents, *, celsius):
super().__init__(owner_code, contents)
if celsius > RefrigeratedShippingContainer.MAX_CELSIUS:
raise ValueError("Temperature too hot!")
self._celsius = celsius
@property
def celsius(self):
return {"max_temperature": RefrigeratedShippingContainer.MAX_CELSIUS,
"current_temperature": self._celsius}
>>> r = oop_11_17_test.RefrigeratedShippingContainer.create_with_items("MRP", ["fish"], celsius=3)
>>> r.celsius
{'max_temperature': 4.0, 'current_temperature': 3}
- see, the access of a property is identical to the access to an attribute
- … → no
()
is required as opposed to the JAVA implementation of a getter - … → importantly/unusually, the name of a function within the implementation is a noun
- … yet, within the implementation, property acts as a fully-fledged function
You will get an AttributeError trying to change the value of a Python property in a way you’d change the value of an instance attribute
- which may be a useful object feature, in case you are looking for creating a read-only attribute ⟹ define a property for accessing a value, but without a setter
>>> r.celsius = 5Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
can't set attribute