Concepts - Descriptor in Python

Published on
4 mins read
––– views
Photo by Chris Ried on Unsplash
python-notes

Descriptors

Python descriptors allow a programmer to create managed attributes.

This design helps us to create a data modeling.

In other object-oriented languages, you will find getter and setter methods to manage attributes.

However, Python allows a programmer to manage the attributes simply with the attribute name, without losing their protection.

This is achieved by defining a descriptor class, that implements any of __get__, __set__, __delete__ methods.

Properties

Descriptors can also be created using property() type.

It is easy to create a descriptor for any attribute using property().

Syntax of defining a Property

property(fget=None, fset=None, fdel=None, doc=None)

where,

fget : attribute get method

fset : attribute set method

fdel : attribute delete method

doc : docstring

Property Decorators

Descriptors can also be created with property decorators.

While using property decorators, an attribute's get method will be same as its name and will be decorated with property.

In a case of defining any set or delete methods, they will be decorated with respective setter and deleter methods.

Syntax

Example - 1

class A:
def __init__(self, val):
self.x = val
@property
def x(self):
return self.__x
@x.setter
def x(self, val):
self.__x = val
@x.deleter
def x(self):
del self.__x
a = A(7)
print(a.x)
del a.x
print(a.x)

Output

Traceback (most recent call last):
File "C:\Users\hai\AppData\Roaming\JetBrains\PyCharmCE2021.2\scratches\desciptor.py", line 64, in <module>
print(a.x)
File "C:\Users\hai\AppData\Roaming\JetBrains\PyCharmCE2021.2\scratches\desciptor.py", line 49, in x
return self.__x
AttributeError: 'A' object has no attribute '_A__x'
7

Example - 2

__get___ and __set___ usages

# !/bin/python3
# Add Celsius class implementation below.
class Celsius:
def __get__(self, obj, owner):
""" converts to celsius """
return 5 * (obj.fahrenheit - 32) / 9
def __set__(self, obj, value):
""" converts to fahrenheit """
obj.fahrenheit = 32 + 9 * value / 5
class Temperature:
# variable
celsius = Celsius()
def __init__(self, fahrenheit):
self.fahrenheit = fahrenheit
# Add temperature class implementation below.
if __name__ == "__main__":
res_lst = list()
t1 = Temperature(float(input("Enter the fahrenheit temperature (float or int): ")))
print("{:.2f} F = {:.2f} C".format(t1.fahrenheit, t1.celsius))
t1.celsius = float(input("Enter the celsius temperature (float or int): "))
print("{:.2f} F = {:.2f} C".format(t1.fahrenheit, t1.celsius))

Output

Enter the fahrenheit temperature (float or int): 98.6
98.60 F = 37.00 C
Enter the celsius temperature (float or int): 37
98.60 F = 37.00 C

__get___ and __set___ usages

class EmpNameDescriptor:
def __get__(self, obj, owner):
return self.__empname
def __set__(self, obj, value):
if not isinstance(value, str):
raise TypeError("'empname' must be a string.")
self.__empname = value
class EmpIdDescriptor:
def __get__(self, obj, owner):
return self.__empid
def __set__(self, obj, value):
if hasattr(obj, 'empid'):
raise ValueError("'empid' is read only attribute")
if not isinstance(value, int):
raise TypeError("'empid' must be an integer.")
self.__empid = value
class Employee:
empid = EmpIdDescriptor()
empname = EmpNameDescriptor()
def __init__(self, emp_id, emp_name):
self.empid = emp_id
self.empname = emp_name
e1 = Employee(123456, 'John')
print(e1.empid, '-', e1.empname)
e1.empname = 'Williams'
print(e1.empid, '-', e1.empname)
e1.empid = 76347322 # Raises Read only error

Output

123456 - John
123456 - Williams
Traceback (most recent call last):
File "C:\Users\hai\AppData\Roaming\JetBrains\PyCharmCE2021.2\scratches\desciptor.py", line 133, in <module>
e1.empid = 76347322 # Raises Read only error
File "C:\Users\hai\AppData\Roaming\JetBrains\PyCharmCE2021.2\scratches\desciptor.py", line 112, in __set__
raise ValueError("'empid' is read only attribute")
ValueError: 'empid' is read only attribute

Another way to implement property

class Employee:
def __init__(self, emp_id, emp_name):
self.empid = emp_id
self.empname = emp_name
def getEmpID(self):
return self.__empid
def setEmpID(self, value):
if not isinstance(value, int):
raise TypeError("'empid' must be an integer.")
self.__empid = value
empid = property(getEmpID, setEmpID)
def getEmpName(self):
return self.__empname
def setEmpName(self, value):
if not isinstance(value, str):
raise TypeError("empname' must be a string.")
self.__empname = value
def delEmpName(self):
del self.__empname
empname = property(getEmpName, setEmpName, delEmpName)
e1 = Employee(123456, 'John')
print(e1.empid, '-', e1.empname) # -> '123456 - John'
del e1.empname # Deletes 'empname'
# print(e1.empname) # Raises 'AttributeError'

Yet Another final version with decorators

class Employee:
def __init__(self, emp_id, emp_name):
self.empid = emp_id
self.empname = emp_name
@property
def empid(self):
return self.__empid
@empid.setter
def empid(self, value):
if not isinstance(value, int):
raise TypeError("'empid' must be an integer.")
self.__empid = value
@property
def empname(self):
return self.__empname
@empname.setter
def empname(self, value):
if not isinstance(value, str):
raise TypeError("'empname' must be a string.")
self.__empname = value
@empname.deleter
def empname(self):
del self.__empname
e1 = Employee(123456, 'John')
print(e1.empid, '-', e1.empname) # -> '123456 - John'
del e1.empname # Deletes 'empname'
# print(e1.empname) # Raises 'AttributeError'