| Managing Attributes in Your Classes | ||||||||||
|
in an OOP language a class some instance and class attributes can access the attributes
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 ._ycould 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 42a 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
can use property() either as a function or decorator to build properties Creating Attributes With property()
all the arguments to property() are optionaltypically 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 decoratorcan 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 attributethe .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 propertyafter deleting the radius a new one can be set using .set important points
|
||||||||||
| Deciding When to Use Properties | ||||||||||
generally should avoid using properties for attributes which don't require extra functionality
or processing
|
||||||||||
| 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 propertieshypothetical 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 attributesdata 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 choicethese 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 frequentlyrepeatedly 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 oncemake 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 flowsuse 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 functionalitymight 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
|