首页 > 解决方案 > 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.

标签: pythonstrategy-pattern

解决方案


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.


推荐阅读