Search code examples
pythonabstract-class

How do I correctly code an abstract class and its subclasses to have getter/setter behavior?


In Python (3.11) I have this abstract class:

from abc import ABCMeta, abstractmethod
from copy import deepcopy

class BookPrototype(metaclass=ABCMeta):
  
    @property
    def title(self):
        pass

    @title.setter
    @abstractmethod
    def title(self, val):
        pass

    @abstractmethod
    def clone(self):
        pass

I create this subclass:

class ScienceFiction(BookPrototype):

    def title(self, val):
        print("this never gets called without decorator")

    def __init__(self):
        pass

    def clone(self):
        return deepcopy(self)

And use it this way:

science1 = ScienceFiction()
science1.title = "From out of nowhere"
science2 = science1.clone()
print(science2.title)
science2.title = "New title"
print(science2.title)
print(science1.title)

This code does exactly what I want, that is it creates an instance of ScienceFiction class, it clones it, it prints the title of the cloned object and again the title of the first one. So, my prints here are "From out of nowhere", "New Title", "From out of nowhere".

Problem is when, following the docs, I add the @BookPrototype.title.setter decorator to the title setter, this way:

@BookPrototype.title.setter
    def title(self, val):
        print("this gets called now")
    

In this case the print inside the title method works, BUT I can't assign any value, so that the code prints three None.

What am doing wrong? How do I correctly code an abstract class and its subclasses to have getter/setter behavior?


Solution

  • The title value should be stored in a variable. For instance it could be stored in self._title in the base class. I think what you are looking for is something like:

    from abc import ABCMeta, abstractmethod
    from copy import deepcopy
    
    class BookPrototype(metaclass=ABCMeta):
    
        def __init__(self):
            self._title: str = ""
    
        @property
        def title(self):
            return self._title
    
        @title.setter
        @abstractmethod
        def title(self, val):
            ...
    
        @abstractmethod
        def clone(self):
            ...
    
    class ScienceFiction(BookPrototype):
        @BookPrototype.title.setter
        def title(self, val):
            self._title = val
    
        def clone(self):
            return deepcopy(self)
    
    science1 = ScienceFiction()
    science1.title = "From out of nowhere"
    science2 = science1.clone()
    print(science2.title)
    science2.title = "New title"
    print(science2.title)
    print(science1.title)
    
    # From out of nowhere
    # New title
    # From out of nowhere
    

    When you remove the decorator @BookPrototype.title.setter as in :

    class ScienceFiction(BookPrototype):
    
        def title(self, val):
            print("this never gets called without decorator")
    
        def __init__(self):
            pass
    
        def clone(self):
            return deepcopy(self)
    

    setting the variable title with a string just override the method with a str object. This can be seen with the following :

    science = ScienceFiction()
    print(type(getattr(science, 'title')))
    science.title = "From out of nowhere"
    print(type(getattr(science, 'title')))
    
    # <class 'method'>
    # <class 'str'>  
    

    Note that in python you can dynamically add / modify / delete attributes.