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.xprint(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.__xAttributeError: 'A' object has no attribute '_A__x'7Example - 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.698.60 F = 37.00 CEnter the celsius temperature (float or int): 3798.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 errorOutput
123456 - John123456 - WilliamsTraceback (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 attributeAnother 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'