Python Topics : property(): Add Managed Attributes
Managing Attributes in Your Classes

in an OOP language a class some instance and class attributes
can access the attributes
  • directly
  • via methods
The Getter and Setter Approach in Python
class Point:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    def get_x(self):
        return self._x

    def set_x(self, value):
        self._x = value

    def get_y(self):
        return self._y

    def set_y(self, value):
        self._y = value
both _x and _y can be accessed directly as well as by getter and setter

The Pythonic Approach
the getter and setter methods don't perform any further processing with the values of ._x and ._y
could just have plain attributes instead of methods

a more Pythonic approach

>>> class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...

>>> point = Point(12, 5)
>>> point.x
12
>>> point.y
5

>>> point.x = 42
>>> point.x
42
a better approach is to turn attributes into properties
properties represent an intermediate functionality between a plain attribute, or field, and a method
properties allow the creation of methods that behave like attributes
with properties can change how the target attribute is calculated whenever a change is needed

properties allow exposing attributes as part of your classes' public APIs
if a change an attribute's underlying implementation is needed, can conveniently turn it into a property at any time

Getting Started With Python's property()

use property() to avoid getter and setter methods in classes
built-in function allows turning class attributes into properties or managed attributes
property() is implemented in C, which ensures optimized performance

with property() can attach implicit getter and setter methods to given class attributes
can also specify a way to handle attribute deletion
can provide an appropriate docstring for the properties
full signature

property([fget=None, fset=None, fdel=None, doc=None])
the first two arguments take function objects that will play the role of getter (fget) and setter (fset) methods
calls to these function objects are made automatically when the attribute is manipulated

ArgumentDescription
fget a function object that returns the value of the managed attribute
called when the attributes value is to be read
fset a function object that allows you to set the value of the managed attribute
called when the attributes value is to be assigned
fdel a function object that defines how the managed attribute handles deletion
called from statement obj.del
doca string representing the property's docstring

the return value of property() is the managed attribute itself
can use property() either as a function or decorator to build properties

Creating Attributes With property()
all the arguments to property() are optional
typically provide at least a getter function

circle_v1.py

class Circle:
    def __init__(self, radius):
        self._radius = radius

    def _get_radius(self):
        print("Get radius")
        return self._radius

    def _set_radius(self, value):
        print("Set radius")
        self._radius = value

    def _del_radius(self):
        print("Delete radius")
        del self._radius

    radius = property(
        fget=_get_radius,
        fset=_set_radius,
        fdel=_del_radius,
        doc="The radius property."
    )
example usage
>>> from circle_v1 import Circle

>>> circle = Circle(42.0)

>>> circle.radius
Get radius
42.0

>>> circle.radius = 100.0
Set radius
>>> circle.radius
Get radius
100.0

>>> del circle.radius
Delete radius
>>> circle.radius
Get radius
Traceback (most recent call last):
    ...
AttributeError: 'Circle' object has no attribute '_radius'

>>> help(circle)
Help on Circle in module __main__ object:

class Circle(builtins.object)
    ...
 |  radius
 |      The radius property.
the .radius property hides the non-public instance attribute ._radius
._radius is now a managed attribute in the example
can access and assign .radius directly
internally Python automatically calls ._get_radius() and ._set_radius() when needed
when del circle.radius is executed Python calls ._del_radius(), which deletes the underlying ._radius

the raw methods provided as the fget, fset, and fdel arguments

>>> Circle.radius.fget
<function Circle._get_radius at 0x7fba7e1d7d30>

>>> Circle.radius.fset
<function Circle._set_radius at 0x7fba7e1d78b0>

>>> Circle.radius.fdel
<function Circle._del_radius at 0x7fba7e1d7040>

>>> dir(Circle.radius)
[..., '__get__', ..., '__set__', ...]
properties are class attributes which manage instance attributes

Using property() as a Decorator
Python's property() can work as a decorator
can use the @property syntax to create properties quickly
# circle_v2.py
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        """The radius property."""
        print("Get radius")
        return self._radius

    @radius.setter
    def radius(self, value):
        print("Set radius")
        self._radius = value

    @radius.deleter
    def radius(self):
        print("Delete radius")
        del self._radius
the @property decorator declares the .radius() method as a getter attribute
the .setter and .deleter decorators complete the property
usage with the decorators
>>> from circle_v2 import Circle

>>> circle = Circle(42.0)

>>> circle.radius
Get radius
42.0

>>> circle.radius = 100.0
Set radius
>>> circle.radius
Get radius
100.0

>>> del circle.radius
Delete radius
>>> circle.radius
Get radius
Traceback (most recent call last):
    ...
AttributeError: 'Circle' object has no attribute '_radius'

>>> help(circle)
Help on Circle in module __main__ object:

class Circle(builtins.object)
    ...
 |  radius
 |      The radius property.
parentheses are not need when calling the .radius property
after deleting the radius a new one can be set using .set
important points
  • the @property decorator must decorate the getter method
  • the docstring must go in the getter method
  • the setter and deleter methods must be decorated with the name of the getter method plus .setter and .deleter, respectively
Deciding When to Use Properties
generally should avoid using properties for attributes which don't require extra functionality or processing
  • unnecessarily verbose
  • confusing to other developers
  • slower than code based on regular attributes
Read-Only, Write-Only & Read-Write Attributes
@property is the simplest way to provide a class with a read-only attribute
class WriteCoordinateError(Exception):
    pass

class Point:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        raise WriteCoordinateError("x coordinate is read-only")

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        raise WriteCoordinateError("y coordinate is read-only")
add a diameter read-write attribute
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        self._radius = float(value)

    @property
    def diameter(self):
        return self.radius * 2

    @diameter.setter
    def diameter(self, value):
        self.radius = value / 2
can also create write-only attributes by tweaking the getter method of properties
hypothetical example of handling passwords with a write-only property
# users.py
import hashlib
import os

class User:
    def __init__(self, name, password):
        self.name = name
        self.password = password

    @property
    def password(self):
        raise AttributeError("Password is write-only")

    @password.setter
    def password(self, plaintext):
        salt = os.urandom(32)
        self._hashed_password = hashlib.pbkdf2_hmac("sha256", plaintext.encode("utf-8"), salt, 100_000)
Putting Python's property() Into Action
Validating Input Values
validating input is one of the most common use cases of property() and managed attributes
data validation is a common requirement in code that takes input from users or other sources which could be considered untrusted

validate Point coordinates are entered as numbers

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        try:
            self._x = float(value)
            print("Validated!")
        except ValueError:
            raise ValueError('"x" must be a number') from None

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        try:
            self._y = float(value)
            print("Validated!")
        except ValueError:
            raise ValueError('"y" must be a number') from None
a DRY version of Circle
class Coordinate:
    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, instance, owner):
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        try:
            instance.__dict__[self._name] = float(value)
            print("Validated!")
        except ValueError:
            raise ValueError(f'"{self._name}" must be a number') from None

class Point:
    x = Coordinate()
    y = Coordinate()

    def __init__(self, x, y):
        self.x = x
        self.y = y
Providing Computed Attributes
if an attribute that builds its value dynamically is needed, then use a property can be a great choice
these kinds of attributes are commonly known as computed attributes
something that works like an eager attribute but is lazy
# rectangle.py
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height
another use case of properties is to provide a formatted value for a given attribute
# product.py
class Product:
    def __init__(self, name, price):
        self._name = name
        self._price = float(price)

    @property
    def price(self):
        return f"${self._price:,.2f}"
Caching Computed Attributes
sometimes a given computed attribute is used frequently
repeatedly running the same computation may be unnecessary and expensive
can cache the computed value for later reuse

if a property which computes its value from constant input values, the result will never change
compute the value every time

from time import sleep

class Circle:
    def __init__(self, radius):
        self.radius = radius
        self._diameter = None

    @property
    def diameter(self):
        if self._diameter is None:
            sleep(0.5)  # Simulate a costly computation
            self._diameter = self.radius * 2
        return self._diameter
if the radius is changed, the diameter will be incorrect as it's only calculated once
make the diameter be recalculated only when the radius has changed
from time import sleep

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        self._diameter = None
        self._radius = value

    @property
    def diameter(self):
        if self._diameter is None:
            sleep(0.5)  # Simulate a costly computation
            self._diameter = self._radius * 2
        return self._diameter
caching calculated values avoids costly computations
from functools import cache
from time import sleep

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    @cache
    def diameter(self):
        sleep(0.5) # Simulate a costly computation
        return self.radius * 2
Logging Attribute Access and Mutation
sometimes need to keep track of what the code does and how the program flows
use logging
can constantly watch the code and generate useful information about how it works

track of how and when a given attribute is accessed and mutated

import logging

logging.basicConfig(
    format="%(asctime)s: %(message)s",
    level=logging.INFO,
    datefmt="%H:%M:%S"
)

class Circle:
    def __init__(self, radius):
        self._msg = '"radius" was %s. Current value: %s'
        self.radius = radius

    @property
    def radius(self):
        logging.info(self._msg % ("accessed", str(self._radius)))
        return self._radius

    @radius.setter
    def radius(self, value):
        try:
            self._radius = float(value)
            logging.info(self._msg % ("mutated", str(self._radius)))
        except ValueError:
            logging.info('validation error while mutating "radius"')
usage
>>> from circle_v8 import Circle

>>> circle = Circle(42.0)

>>> circle.radius
14:48:59: "radius" was accessed. Current value: 42.0
42.0

>>> circle.radius = 100
14:49:15: "radius" was mutated. Current value: 100

>>> circle.radius
14:49:24: "radius" was accessed. Current value: 100
100

>>> circle.radius = "value"
15:04:51: validation error while mutating "radius"
Managing Attribute Deletion
can create properties that implement deletion functionality
might be a rare use case but having a way to delete an attribute can be needed in some situations

example implements a tree node which uses property() to provide most of its functionality
includes the ability to clear the node's list of children

class TreeNode:
    def __init__(self, data):
        self._data = data
        self._children = []

    @property
    def children(self):
        return self._children

    @children.setter
    def children(self, value):
        if isinstance(value, list):
            self._children = value
        else:
            del self.children
            self._children.append(value)

    @children.deleter
    def children(self):
        self._children.clear()

    def __repr__(self):
        return f'{self.__class__.__name__}("{self._data}")'
in use
>>> from tree import TreeNode

>>> root = TreeNode("root")
>>> child1 = TreeNode("child 1")
>>> child2 = TreeNode("child 2")

>>> root.children = [child1, child2]

>>> root.children
[TreeNode("child 1"), TreeNode("child 2")]

>>> del root.children
>>> root.children
[]
Overriding Properties in Subclasses
when creating Python classes consider they could be subclassed to customize their functionalities
if a property is partially overridden by a subclass then the subclass loses the non-overridden functionality

example

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    # Person implementation...

class Employee(Person):
    @property
    def name(self):
        return super().name.upper()

    # Employee implementation...
Employee overrides .name to make the employee name is in uppercase
>>> from persons import Employee, Person

>>> person = Person("John")
>>> person.name
'John'
>>> person.name = "John Doe"
>>> person.name
'John Doe'

>>> employee = Employee("John")
>>> employee.name
'JOHN'

>>> employee.name = "John Doe"
Traceback (most recent call last):
    ...
AttributeError: can't set attribute
index