python 中的策略模式

Strategy pattern in python

我来自 C# 背景,为了实现策略模式,我们总是会使用一个接口,例如:ILoggger。据我了解,在 Python 等鸭子类型的语言中,我们可以避免使用此基数 class/contract。

我的问题是,这是利用鸭子类型实现策略模式的最佳方式吗?而且,这种鸭子打字的方式是否让我的代码的下一个用户清楚这是一个 "point of extension"?另外,我认为最好使用类型提示来帮助下一个查看您的代码的人了解策略的类型应该是什么,但是对于没有 base class/contract 的 duck-typing,您使用哪种类型?其中一个已经具体 类?

这是一些代码:

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

有人能告诉我什么是最 standard/Pythonic/readable 处理这个问题的方法吗?

我不是在寻找 "here is answer in 2 lines of code" 答案。

如果您真的想要 classes 并执行您的基础 class 用法,请创建一个 ABC: abstract base class / method 和一些实现其中:

属性:用于 Alex Vasses answer here 查找目的

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

然后使用它们:

# 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)

输出:

0
1
2
3
4 
file: 0
file: 1
file: 2
file: 3
file: 4

logger needs to inherit from BaseLogger

作为旁注:python 已经具有相当复杂的日志记录能力。如果这是您查询的唯一目的,请查看 Logging

在 Python 中,通常不需要具有 compile-time 类型执行的 full-scale 策略模式,这要归功于运行时安全性和优雅终止。

如果您希望自定义现有代码的某些部分,通常的做法是:

  • 替换适当的方法 -- 是否通过 subclassing,包括。使用 mix-in,或只是赋值(方法是活动对象的属性,就像任何其他对象一样,可以重新分配;替换甚至可以不是函数,而是具有 __call__ 的对象) ;
    • 请注意,在 Python 中,您可以通过将其定义放在函数中来动态创建包含代码(包括函数和 classes)的对象。然后在执行封闭函数时评估定义,您可以使用可访问变量(又名闭包)作为 ad-hoc 参数;或
  • 在某个时候接受回调(或一个对象,您将在适当的时候调用其方法,有效地充当一组回调);或
  • 接受一个字符串参数,该参数是来自特定集合的常量,然后代码在 if/else 中测试或在某些注册表中查找(无论是全局的模块还是局部的class 或对象);
    • 自 3.4 以来有 enum 但对于简单的情况,它被认为有太多缺点而不是好处(调试时不可读并且需要样板文件)因为 Python 相比之下更灵活在 flexibility-vs-scalability 规模上到 C#。

据我所知,在 Python 中实现策略模式的最常见方法是传递一个函数(或可调用函数)。函数是 Python 中的第一个 class 对象,因此如果所有消费者需要的只是一个函数,那么您不需要给它更多。当然,如果您愿意,可以对其进行注释。假设您只想记录字符串:

class ComponentThatNeedsLogger:
    def __init__(self, log_func: Callable[[str], None]):
        self._log_func = log_func

这允许您即时创建一个简单的记录器:

ComponentThatNeedsLogger(
    log_func=print
)

但您也可以利用 classes 的所有功能来创建复杂的记录器并仅传递相关方法。

class ComplexLogger:
    def __init__(self, lots_of_dependencies):
        # initialize the logger here

    def log(self, msg: str): None
        # call private methods and the dependencies as you need.

    def _private_method(self, whatever):
        # as many as you need.

ComponentThatNeedsLogger(
    log_func= ComplexLogger(lots_of_dependencies).log
)