python-course.eu

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.

property

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

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here