Search code examples
pythonstrategy-pattern

Strategy pattern in python


I am coming from C# background and in order to implement a strategy pattern, we would always use an interface, for example: ILoggger. Now as I understand, in duck-typed languages such as Python, we can avoid this base class/contract.

My question is, is this the best way to implement a strategy pattern by taking advantage of duck typing? And, does this duck typing way of doing this make it clear to the next user of my code that this is an "point of extension"? Also, I think it is better to use type hints to help the next person looking at your code to see what the type of the strategy should be, but with duck-typing without base class/contract, which type do you use? One of the already concrete classes?

Here is some code:

class FileSystemLogger():
    def log(self, msg):
        pass

class ElasticSearchLogger():
    def log(self, msg):
        pass

# if i wanted to use type hints, which type should logger be here?
class ComponentThatNeedsLogger():
    def __init__(self, logger):
        self._logger = logger

# should it be this?
class ComponentThatNeedsLogger():
    def __init__(self, logger : FileSystemLogger):
        self._logger = logger

Could someone please advise what is the most standard/Pythonic/readable way to handle this?

I am not looking for the "here is answer in 2 lines of code" answer.


Solution

  • If you really wanted to go classes all the way and enforce your base class usage create an ABC: abstract base class / method and some implementations of it:

    Attributation: used Alex Vasses answer here for lookup purposes

    from abc import ABC, abstractmethod
    
    class BaseLogger(ABC):
        """ Base class specifying one abstractmethod log - tbd by subclasses."""
        @abstractmethod
        def log(self, message):
            pass
    
    class ConsoleLogger(BaseLogger):
        """ Console logger implementation."""
        def log(self, message):
            print(message)
    
    class FileLogger(BaseLogger):
        """ Appending FileLogger (date based file names) implementation."""
        def __init__(self):
            import datetime 
            self.fn = datetime.datetime.now().strftime("%Y_%m_%d.log")
    
        def log(self,message):
            with open(self.fn,"a") as f:
                f.write(f"file: {message}\n")
    
    class NotALogger():
        """ Not a logger implementation."""
        pass
    

    Then use them:

    # import typing # for other type things
    
    class DoIt:
        def __init__(self, logger: BaseLogger):
            # enforce usage of BaseLogger implementation
            if isinstance(logger, BaseLogger):
                self.logger = logger
            else:
                raise ValueError("logger needs to inherit from " + BaseLogger.__name__)
    
        def log(self, message):
            # use the assigned logger
            self.logger.log(message)
    
    # provide different logger classes
    d1 = DoIt(ConsoleLogger())
    d2 = DoIt(FileLogger())
    
    for k in range(5):
        d1.log(str(k))
        d2.log(str(k))
    
    with open(d2.logger.fn) as f:
        print(f.read())
    
    try:
        d3 = DoIt( NotALogger())
    except Exception as e:
        print(e)
    

    Output:

    0
    1
    2
    3
    4 
    file: 0
    file: 1
    file: 2
    file: 3
    file: 4
    
    logger needs to inherit from BaseLogger
    

    As a sidenote: python already has quite sophisticated abilities to log. Look into Logging if that is the sole purpose of your inquiry.