Search code examples
pythoninheritancedesign-patternsinterfaceabstract-class

How Python being a dynamically-typed language makes it unnecessary to use abstract classes/inheritance?


I've looked up design patterns in python and saw a the following sentence in some article:

"We implement our Abstract Factory without using inheritance, mainly because Python is a dynamically typed language and therefore does not require abstract classes."

Code example:

class Dog:
    """One of the objects to be returned"""

    def speak(self):
        return "Woof!"

    def __str__(self):
        return "Dog"


class DogFactory:
    """Concrete Factory"""

    def get_pet(self):
        """Returns a Dog object"""
        return Dog()

    def get_food(self):
        """Returns a Dog Food object"""
        return "Dog Food!"


class PetStore:
    """ PetStore houses our Abstract Factory """

    def __init__(self, pet_factory=None):
        """ pet_factory is our Abstract Factory """

        self._pet_factory = pet_factory


    def show_pet(self):
        """ Utility method to display the details of the objects retured by the DogFactory """

        pet = self._pet_factory.get_pet()
        pet_food = self._pet_factory.get_food()

        print("Our pet is '{}'!".format(pet))
        print("Our pet says hello by '{}'".format(pet.speak()))
        print("Its food is '{}'!".format(pet_food))


#Create a Concrete Factory
factory = DogFactory()

#Create a pet store housing our Abstract Factory
shop = PetStore(factory)

#Invoke the utility method to show the details of our pet
shop.show_pet()

I understand the example I provided, but I don't get this statement, and I've seen so many other examples implementing design patterns in python using inheritance and abstract classes (e.g.: the abstract factory in this case) like how almost every other languages do such as java.

I would really appreciate an explanation on the statement and how the two approaches (with and without abstract-classes/interfaces/inheritance) differ using code examples.

Edit:

Question: To be specific, I want to understand how dynamic-typing makes life eaiser and compensates the use of inheritance/interfaces in the mentioned code example, by illustrating how would the code example (abstract factory) look like if inheritance/interfaces were used to implement the same functionality of the given example.

Thank you.


Solution

  • The key here is the "PetFactory". In the above code, there is no technical meaning to "PetFactory". But conceptually, there is, what in practice acts as, an "interface", that I'm going to refer to as a "PetFactory". A "PetFactory" must have the functions get_pet and get_food.

    If one were to write a new "PetFactory", regardless of whether they named it in their head, they would have to identify this interface and its requirements.

    The very fact that it has no true name makes it harder to identify that it even exists or to wrap one's head around it (this becomes more relevant when the "interface" is just one of many things you're working with).

    But besides that, to identify the requirements of a "PetFactory" one has to trace through multiple functions. In this case, it's not too hard, but with a couple more functions it gets complicated fast.

    Instead one could use inheritance and do something along the lines of:

    class PetFactory():
        def get_pet(self):
            """returns a pet"""
            pass
    
        def get_food(self):
            """returns a string defining the pet's food"""
            pass
    
    class DogFactory(PetFactory):
    
        def get_pet(self):
            """Returns a Dog object"""
            return Dog()
    
        def get_food(self):
            """Returns a Dog Food object"""
            return "Dog Food!"
    

    From a technical standpoint, the addition of this class really doesn't do anything. But conceptually, the existence of the "PetFactory" is now obvious, its requirements are clear, and it has a technical name.


    With the implementation of PEP 3119, python now also has form abstract base classes. Using these, the new code would look something like this:

    from abc import ABC, abstractmethod
    
    class PetFactory(ABC):
    
        @abstractmethod
        def get_pet(self):
            """returns a pet"""
            pass
    
        @abstractmethod
        def get_food(self):
            """returns a string defining the pet's food"""
            pass
    
    class DogFactory(PetFactory):
    
        def get_pet(self):
            """Returns a Dog object"""
            return Dog()
    
        def get_food(self):
            """Returns a Dog Food object"""
            return "Dog Food!"
    

    The benefit of this, is it clearly marks the purpose of the the PetFactory class and forces any subclasses to implement all abstractmethods before they can be instantiated. It also allows for type checking: isinstance(PetFactory).