pythonoopsingle-responsibility-principlepyright

Achieving single-responsibility principle with python abstract classes


I want to separate the DB models from the actual classes. But i need two static functions for fetching data from the DB regardless of the subclass type. the implementation for both functions are the same across all DB models.

pyright showing an error that cls inside get() and get_all() functions doesn't have a db property.

from abc import ABC, abstractstaticmethod


class DogsDB:
    lists = ["DOG1", "DOG2", "DOG3"]

    @classmethod
    def get(cls, id):
        return cls.lists[id]


class CatsDB:
    lists = ["CAT1", "CAT2", "CAT3"]

    @classmethod
    def get(cls, id):
        return cls.lists[id]


class Animal(ABC):
    def __init__(self, name):
        self.name = name

    @abstractstaticmethod
    def save(m):
        pass

    @abstractstaticmethod
    def _from_model(obj):
        pass

    @classmethod
    def get(cls, id):
        obj = cls.db.get(id)
        return cls._from_model(obj)

    @classmethod
    def get_all(cls):
        objs = cls.db.lists

        lists = []
        for obj in objs:
            e = cls._from_model(obj)
            lists.append(e)
        return lists

    def __repr__(self):
        return self.name


class DogSound:
    def __init__(self, name):
        self.name = name

    def sound(self):
        print(self.name, ": DOG SOUND!!")


class Dog(Animal, DogSound):
    db = DogsDB

    def __init__(self, name, age):
        super(Dog, self).__init__(name)
        self.age = age

    @staticmethod
    def save(m):
        print(m)

    @staticmethod
    def _from_model(obj):
        return Dog(obj, 4)


class Cat(Animal):
    db = CatsDB

    def __init__(self,  name, age):
        super().__init__(name)
        self.age = age

    @staticmethod
    def save(m):
        print(m)

    @staticmethod
    def _from_model(obj):
        return Cat(obj, 4)


print(Cat.get(1))
print(Dog.get(1))
print(Cat.get_all())
print(Dog.get_all())
Dog.get(1).sound()


Solution

  • I cannot duplicate your first error.

    Your second issue is a result of method sound implicitly returning None since it has no return statement and you have print(Dog.get(1).sound()), which will print out the return value from that method. You either want to change this to just Dog.get(1).sound() or modify the sound method to return what it is currently being printed and remove the print statement (my choice).

    As an aside, I found this class structure a bit difficult to follow. Why do you need a separate DogSound class with a name attribute which should belong to Animal? Also, it seems to me that age could/should be an attribute of Animal since both cats and dogs have an age.

    from abc import ABC, abstractstaticmethod
    
    
    class DogsDB:
        lists = ["DOG1", "DOG2", "DOG3"]
    
        @classmethod
        def get(cls, id):
            return cls.lists[id]
    
    
    class CatsDB:
        lists = ["CAT1", "CAT2", "CAT3"]
    
        @classmethod
        def get(cls, id):
            return cls.lists[id]
    
    
    class Animal(ABC):
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        @abstractstaticmethod
        def save(m):
            pass
    
        @abstractstaticmethod
        def _from_model(obj):
            pass
    
        @classmethod
        def get(cls, id):
            obj = cls.db.get(id)
            return cls._from_model(obj)
    
        @classmethod
        def get_all(cls):
            objs = cls.db.lists
    
            lists = []
            for obj in objs:
                e = cls._from_model(obj)
                lists.append(e)
            return lists
    
        def __repr__(self):
            return self.name
    
    class Dog(Animal):
        db = DogsDB
    
        def __init__(self, name, age):
            super().__init__(name, age)
    
        def sound(self):
            return f"{self.name}: DOG SOUND!!"
    
        @staticmethod
        def save(m):
            print(m)
    
        @staticmethod
        def _from_model(obj):
            return Dog(obj, 4)
    
    
    class Cat(Animal):
        db = CatsDB
    
        def __init__(self,  name, age):
            super().__init__(name, age)
            self.age = age
    
        @staticmethod
        def save(m):
            print(m)
    
        @staticmethod
        def _from_model(obj):
            return Cat(obj, 4)
    
    
    print(Cat.get(1))
    print(Dog.get(1))
    print(Cat.get_all())
    print(Dog.get_all())
    print(Dog.get(1).sound())
    

    Prints:

    CAT2
    DOG2
    [CAT1, CAT2, CAT3]
    [DOG1, DOG2, DOG3]
    DOG2: DOG SOUND!!
    

    If for some reason you want DogSound to be a separate class, then there is no need for the name attribute to be duplicated:

    ...
    class DogSound: # A "Mixin" class
        def sound(self):
            return f"{self.name}: DOG SOUND!!"
    
    
    class Dog(Animal, DogSound):
        db = DogsDB
    
        def __init__(self, name, age):
            super().__init__(name, age)
    
        @staticmethod
        def save(m):
            print(m)
    
        @staticmethod
        def _from_model(obj):
            return Dog(obj, 4)
    ...