6. Implementing a Custom Property Class
By Bernd Klein. Last modified: 02 Dec 2023.
In the previous chapter of our tutorial, we learned how to create and use properties in a class. The main objective was to understand them as a way to get rid of explicit getters and setters and have a simple class interface. This is usually enough to know for most programmers and for practical use cases and they will not need more.
If you want to know more about how 'property' works, you can go one step further with us. By doing this, you can improve your coding skills and get a deeper insight and understanding of Python. We will have a look at the way the "property" decorator could be implemented in Python code. (It is implemented in C code in reality!) By doing this, the way of working will be clearer. Everything is based on the descriptor protocol, which we will explain later.
We define a class with the name 'our_property' so that it will not be mistaken for the existing 'property' class. This class can be used like the 'real' property class.
class our_property:
""" emulation of the property class
for educational purposes """
def __init__(self,
fget=None,
fset=None,
fdel=None,
doc=None):
"""Attributes of 'our_decorator'
fget
function to be used for getting
an attribute value
fset
function to be used for setting
an attribute value
fdel
function to be used for deleting
an attribute
doc
the docstring
"""
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
We need another class to use the previously defined class and to demonstrate how the property class decorator works. To continue the tradition of the previous chapters of our Python tutorial we will again write a Robot class. We will define a property in this example class to demonstrate the usage of our previously defined property class or better 'our_decorator' class. When you run the code, you can see __init__
of 'our_property' will be called 'fget' set to a reference to the 'getter' function of 'city'. The attribute 'city' is an instance of the 'our_property' class. The 'our'property' class provides another decorator 'setter' for the setter functionality. We apply this with '@city.setter'
class Robot:
def __init__(self, city):
self.city = city
@our_property
def city(self):
print("The Property 'city' will be returned now:")
return self.__city
@city.setter
def city(self, city):
print("'city' will be set")
self.__city = city
'Robot.city' is an instance of the 'our_property' class as we can see in the following:
type(Robot.city)
OUTPUT:
__main__.our_property
If you change the line '@our_property' to '@property' the program will behave totally the same, but it will be using the original Python class 'property' and not our 'our_property' class. We will create instances of the Robot class in the following Python code:
print("Instantiating a Root and setting 'city' to 'Berlin'")
robo = Robot("Berlin")
print("The value is: ", robo.city)
print("Our robot moves now to Frankfurt:")
robo.city = "Frankfurt"
print("The value is: ", robo.city)
OUTPUT:
Instantiating a Root and setting 'city' to 'Berlin' 'city' will be set The Property 'city' will be returned now: The value is: Berlin Our robot moves now to Frankfurt: 'city' will be set The Property 'city' will be returned now: The value is: Frankfurt
Let's make our property implementation a little bit more talkative with some print functions to see what is going on. We also change the name to 'chatty_property' for obvious reasons:
class chatty_property:
""" emulation of the property class
for educational purposes """
def __init__(self,
fget=None,
fset=None,
fdel=None,
doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
print("\n__init__ called with:)")
print(f"fget={fget}, fset={fset}, fdel={fdel}, doc={doc}")
if doc is None and fget is not None:
print(f"doc set to docstring of {fget.__name__} method")
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
print(type(self))
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
class Robot:
def __init__(self, city):
self.city = city
@chatty_property
def city(self):
""" city attribute of Robot """
print("The Property 'city' will be returned now:")
return self.__city
@city.setter
def city(self, city):
print("'city' will be set")
self.__city = city
OUTPUT:
__init__ called with:) fget=<function Robot.city at 0x000002825C9420D8>, fset=None, fdel=None, doc=None doc set to docstring of city method <class '__main__.chatty_property'> __init__ called with:) fget=<function Robot.city at 0x000002825C9420D8>, fset=<function Robot.city at 0x000002825C9425E8>, fdel=None, doc= city attribute of Robot
robo = Robot("Berlin")
OUTPUT:
'city' will be set
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Upcoming online Courses