Various styles for managing or accessing your attributes/properties of your class in Python
It is an important issue to directly or indirectly access,modify or share, etc. the attributes/properties of the classes you create in Python. You can do/manage attributes/properties with different approaches.
Let’s look at these step by step.
In this article contains:
- attributes vs properties
- public / non-public properties
- Encapsulation
- Getter/Setter methods
- @property decorator
- Descriptor
What is attribute and property? What is differences?
Attributes in Python
Attributes are defined by the data variables such as name, age, width, height, etc.
There is 2 types in Python
- Class attributes
- This attribute is created inside class.
- Shared with other methods/objects inside class.
Example:
class TestObject:
# class attribute
variable = 10
# define a method
def increment(self):
TestObject.variable += 1
t1 = TestObject() # t1 instance
t2 = TestObject(); # t2 instance
print(t1.variable)
print(t2.variable)
t1.increment()
print(t1.variable)
print(t2.variable)
Output is
10
10
11
11 # t2 and t1 object share variable attribute
2. Instance attributes
- Instance Attributes are unique to each instance
- Every object/instance contains its attributes and can be changed without modifying other instances
Example :
class Example:
mycount = 2 # class attrİbute
def __init__(self,total): # total instance attrİbute
self.total = total
def method1(self):
return self.total + self.mycount
e1 = Example(10)
e2 = Example(20)
print(e1.method1())
print(e2.method1())
Output is :
12
22
Properties in Python
Properties are unique attributes that contain getter, setter, and deleter methods such as __get__, __set__, and __delete__ methods.
Example :
# create a class
class ExampleClass:
# constructor
def __init__(self, variable):
self._variable = variable # non-public
# getting the variable
@property
def variable(self):
print('Getting variable')
return self._variable
# setting the variable
@variable.setter
def variable(self, variable):
print('Setting variable to ' + variable)
self._variable = variable
# deleting the variable
@variable.deleter
def variable(self):
print('Deleting variable')
del self._variable
e1 = ExampleClass("test")
print(e1.variable)
e1.variable = "change text"
print(e1.variable)
Output is
Getting variable
test
Setting variable to change text
Getting variable
change text
Various styles for managing or accessing your attributes/properties
- Direct access
For example in below class, name and age are properties.In constructor set values, later in showInfo print values of properties.
class ExampleClass:
# constructor
def __init__(self, name, age):
self.name = name
self.age = age
def showInfo(self):
print("Name : {} \nAge: {} ".format(self.name,self.age))
e1 = ExampleClass("Test",30)
print(e1.showInfo())
Output is :
Name : Test
Age: 30
In this example you can direct access/modify your properties
e1.age = 40
e1.name = "change text"
print(e1.showInfo())
Output is :
Name : change text
Age: 40
2. Encapsulation
Now we change our class and we define properties as non-public with “_” or “__” keyword
- Encapsulation with “_” keyword
class ExampleClass:
# constructor
def __init__(self, name, age):
self._name = name # _name is non public prop
self._age = age # _age is non public prop
def setName(self, name): # setter method for non-public _name prop
self._name = name
def setAge(self,age): # setter method for non-public _age prop
self._age = age
def showInfo(self):
print("Name : {} \nAge: {} ".format(self._name,self._age))
If you direct access like :
e1 = ExampleClass("Test",30)
print(e1.age) # direct acccess
You get error :
print(e1.age)
^^^^^^
AttributeError: 'ExampleClass' object has no attribute 'age'
Modify value of property like :
e1.age = 40
print(e1.showInfo())
Output is :
Name: Test
Age: 30
can not change age properties.
If you use setter method for age property, you can modify value of age property.
e1.setAge(40)
print(e1.showInfo())
Output is :
Name: Test
Age: 40
You can modify value of _age property. In actually, your properties are _name and _age
e1._age = 40
print(e1.showInfo())
Output is :
Name: Test
Age: 40
- Encapsulation with __ keyword
For example :
class Employee:
def __init__(self):
self.__name = "default" # not override instance properties
self.__age = 0
def changename(self, name):
self.__name = name
def getname(self):
return self.__name
def __assignName__(self, name):
self.__name = name
Test it :
e1 = Employee()
print(e1.getname())
Output is :
default
If you want to direct access property
print(e1.__name)
You get error:
print(e1.__name)
^^^^^^^^^
AttributeError: 'Employee' object has no attribute '__name'
If you override property with direct access like :
e1__name = "change text"
print(e1.getname())
Output is :
default
not overriding.
If you override value;
e1.changename("change text")
print(e1.getname())
e1.__assignName__("assign text")
print(e1.getname())
Output is :
change text # override
assign text # override
3. Getter & Setter
In here, value assign to setter method of property instead of assigning to property in __init__ method. getter method return value of non-public property.
For example :
class ExampleClass:
def __init__(self, x, y):
self.set_x(x) # move to setter method set_x
self.set_y(y) # move to setter method set_y
def get_x(self):
return self._x # get_x return non-public _x prop
def set_x(self, x):
self._x = self.validate(x) # change behavior of method. check validate with validate methods
def get_y(self):
return self._y
def set_y(self, y):
self._y = self.validate(y)
def validate(self, value):
if not isinstance(value, int | float):
raise ValueError("properties must be numbers")
return value
If you direct access like :
p1 = ExampleClass(1,2)
p1.x
You get error:
AttributeError: 'ExampleClass' object has no attribute 'x'.
Note :
- If you write code like :
def __init__(self, v1, v2):
self.v1 = v1
self.v2 = v2
Your properties are public and access directly:
classInstance.prop
- If you write code like :
def __init__(self,v1,v2):
self._v1 = v1
self._v2 = v2
Your properties are non-public and can not access directly :
classInstance.prop
You get error like :
AttributeError: 'xxxClass' object has no attribute 'xxxProperties'.
4. @property decorator
This decorator is used
- managing to access
- adding to extra behaviour (like validation)
- managing to getter and setter methods
- read-only or write-only features
- lazy load process
etc.
In shortly,
You don’t want to write getter / setter method to access your properties and also you don’t want to direct access your properties.
You can use @property decorator in 2 ways:
- via property(….)
For example :
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
def get_x(self):
print("Getter x")
return self._x
def set_x(self, value):
print("Setter x")
self._x = value
def get_y(self):
print("Getter y")
return self._y
def set_y(self, value):
print("Setter y")
self._y = value
def del_x(self):
print("Delete x")
del self._x
def del_y(self):
print("Delete y")
del self._y
x = property(
fget=get_x,
fset=set_x,
fdel=del_x,
doc="x properties"
)
y = property(
fget=get_y,
fset=set_y,
fdel=del_y,
doc="y properties"
)
x and y properties are non-public. we defined setter, getter and deleter methods and inside property(), we defined fget for getter method, fset for setter method, fdel for deleter method and doc is for documentation.
Test it :
pointCls = Point(2,5) # init values for properties
print(pointCls.x)
Output is :
Getter x
2
Now, you may think direct access for x properties. But in actually, Python runs get_x method in background.
pointCls.x = 7 # modify
print(pointCls.x)
Output is :
Setter x
Getter x
7
you may think direct set new value for x properties. But in actually, Python runs set_x method in background.
- via @property decorator
You can use @property decorator instead of get_prop, set_prop, del_prop for every properties. And also you don’t need to write property(….) definitation.
For example :
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
"""The x property."""
print("Get x")
return self._x
@x.setter
def x(self, value):
print("Set x")
self._x = value
@property
def y(self):
"""The y property."""
print("Get y")
return self._y
@y.setter
def y(self, value):
print("Set y")
self._y = value
@x.deleter
def x(self):
print("Delete x")
del self._x
@y.deleter
def y(self):
print("Delete y")
del self._y
- define property with @property keyword.
- @x.setter is setter for x property
- @x.deleter is deleter for x property
Test it :
pointCls = Point(2, 5)
print(pointCls.x)
Output is :
Get x
2
pointCls.x = 7 # modify
print(pointCls.x)
Output is :
Set x
Get x
7
Note : If you don’t need extra behaviour for your properties, you don’t need use this style. Because this style causes extra complexities for other team-mates developers. In addition, causes extra CPU consumption. This is slower.
5. Read-only & Write-only
- Read-only
If you remove setter methods or throw exception for setter method, only use @property methods. You make object read-only
For example :
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
"""The x property."""
print("Get x")
return self._x
@property
def y(self):
"""The y property."""
print("Get y")
return self._y
If you want, you can write exception for setter
class WriteCoordinateError(Exception):
pass
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
"""The x property."""
print("Get x")
return self._x
@x.setter
def x(self, value):
raise WriteCoordinateError("x coordinate is read-only")
@property
def y(self):
"""The y property."""
print("Get y")
return self._y
Test it :
p1 = Point(2,5)
p1.x = 7
You get error :
WriteCoordinateError: x coordinate is read-only
- Write-only
@property
def x(self):
raise AttributeError("x prop is write-only")
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
raise AttributeError("x prop is write-only")
@x.setter
def x(self, value):
print("Set x")
self._x = value
@property
def y(self):
"""The y property."""
print("Get y")
return self._y
Test it :
p1 = Point(2,5)
p1.x = 7
print(p1.x)
You get error when reading value
AttributeError: x prop is write-only.
6. Descriptor
You can use same operations/processes for different properties or add extra features/behaviours for getter, setter and deleter operations.
- Non-data descriptor (Read-only descriptor)
class ReadOnly_attribute(): # read-only descriptor
def __get__(self, obj, type=None) -> object:
print("accessing the attribute to get the value")
return 42
def __set__(self, obj, value) -> None:
print("accessing the attribute to set the value")
raise AttributeError("Cannot change the value")
# object definitation
class TestObject():
attribute1 = ReadOnly_attribute()
Test it
myObj = TestObject()
print(myObj.attribute1)
Output is :
accessing the attribute to get the value
42
Modify attribute1
myObj.attribute1 = 10
You get error :
raise AttributeError("Cannot change the value")
AttributeError: Cannot change the value
Same operation/behaviour example:
For example, I have a object (ExampleObject)
from datetime import date
class ExampleObject:
def __init__(self, name, start_date, end_date):
self.name = name
self.start_date = start_date
self.end_date = end_date
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value.upper()
@property
def start_date(self):
return self._start_date
@start_date.setter
def start_date(self, value):
self._start_date = date.fromisoformat(value)
@property
def end_date(self):
return self._end_date
@end_date.setter
def end_date(self, value):
self._end_date = date.fromisoformat(value)
we defined 2 properties with @property : start_date and end_date property. We write same date format code in both setter method. Now we can refactor this code blocks via descriptor.
from datetime import date, datetime, timedelta
class DateDescriptor:
def __set_name__(self, owner, name):
self._name = name
def __get__(self, instance, owner):
return instance.__dict__[self._name]
def __set__(self, instance, value):
instance.__dict__[self._name] = date.fromisoformat(value)
class TestObject:
start_date = DateDescriptor()
end_date = DateDescriptor()
def __init__(self, name, start_date, end_date):
self.name = name
self.start_date = start_date
self.end_date = end_date
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value.upper()
DateDescriptor is a descriptor. start_date and end_date properties are defined as DateDescriptor. __set__ methods format value.
In general, Three methods are defined in a descriptor. If you want to define just one of them. If you want to define extra methods.
- __get__(self, instance, owner). Accesses the attribute. It returns the value
- __set__(self, instance, value). Sets the attribute. Does not return anything
- __delete__(self, instance). Deletes the attribute. Does not return anything
Note : Python descriptors are instantiated just once per class. That means that every single instance of a class containing a descriptor shares that descriptor instance.
For example :
class MyNumericDescriptor():
def __init__(self):
self.value = 0
def __get__(self, obj, type=None) -> object:
return self.value
def __set__(self, obj, value) -> None:
if value > 9 or value < 0 or int(value) != value:
raise AttributeError("The value is invalid")
self.value = value
class TestObject():
number = MyNumericDescriptor()
I create two instance from TestObject
myObj1 = TestObject() # instance myObj1
myObj2 = TestObject() # instance myObj2
myObj1.number = 3 # modify myObj1's prop
print(myObj1.number)
print(myObj2.number)
Output is :
3 #share
3 #share
Create another instance
myObj3 = TestObject()
print(myObj3.number)
Output is :
3
Now change value of number property of myObj3 instance
myObj3.number = 5 # modify myObj3 instance prop value
Test it :
print(myObj1.number) # myObj1
print(myObj2.number) # myObj2
print(myObj3.number) # myObj3
Output is :
5
5
5
How to fix it?
You can use dictionary
class MyNumericDescriptor():
def __init__(self):
self.value = {} # dictionary
def __get__(self, obj, type=None) -> object:
try:
return self.value[obj] # get data from dict
except:
return 0
def __set__(self, obj, value) -> None:
if value > 9 or value < 0 or int(value) != value:
raise AttributeError("The value is invalid")
self.value[obj] = value # set to dict
class TestObject():
number = MyNumericDescriptor()
Test it
myObj1 = TestObject() # instance myObj1
myObj2 = TestObject() # instance myObj2
myObj1.number = 3 # modify myObj1 instance prop value
print(myObj1.number)
print(myObj2.number)
Output is :
3
0
Create another instance
myObj3 = TestObject()
print(myObj3.number)
Output is :
0
Now change value of number property of myObj3 instance
myObj3.number = 8 # modify myObj3 instance prop value
print(myObj1.number) # myObj1 instance
print(myObj2.number) # myObj2 instance
print(myObj3.number) # myObj3 instance
Output is :
3
0
8
You can use different styles to
- manage
- access control
- adding extra features/behaviours
- validation
for your properties of your classes.
In this article, I want to show you these styles.
I hope it was useful for you