Python中的3个顶级设计模式:单子、装饰器和迭代器

93 阅读5分钟

什么是软件设计模式?

在软件设计中,设计模式是一种 在一个特定的环境中,对一个经常发生的问题的一般的、可重复使用的解决方案.

它们就像预先制作好的蓝图,你可以定制它来解决你代码中的问题。

你不可能像使用一个新导入的中的函数那样去应用一个设计模式(该模式不是一个代码片段,而是一个描述如何解决一个特定的重复出现的问题的一般概念)。

相反,你应该遵循模式的细节,实现一个适合你程序要求的解决方案。

设计模式的分类

最初,设计模式有两种基本分类--根据模式解决的是什么问题,以及根据模式是否涉及类或对象。考虑到第一种分类,模式可以被分为三组。

  1. 创造性的--提供根据所需的标准并以可控的方式创建、初始化和配置对象和数据类型的能力。
  2. 结构性-- 帮助组织相关对象和类的结构,提供新的功能。
  3. 行为性的-- 是关于识别对象之间的共同通信模式。

后来,出现了新的设计模式,从中可以区分出另一个类别。

  1. 并发-- 那些处理多线程编程范式的设计模式类型。

模式1:Singleton

Singleton是一种创建模式,其目的是将创建一个给定类的对象的可能性限制为一个实例,并确保对所创建对象的全局访问。

使用案例

  • 在你的程序中,一个类只有一个实例可供所有客户使用,如程序的不同部分共享的一个数据库对象。
  • 你需要对全局变量进行更严格的控制。

代码示例

第一种天真烂漫的方法

class Logger:
   @staticmethod
   def get_instance():
       if '_instance' not in Logger.__dict__:
           Logger._instance = Logger()
       return Logger._instance

   def write_log(self, path):
       pass


if __name__ == "__main__":
   s1 = Logger.get_instance()
   s2 = Logger.get_instance()
   assert s1 is s2

这段代码有什么问题?

它违反了单一责任原则,并且有非标准的类访问(你必须记住只通过get_instance() 方法来访问类的实例)--我们试图在另一个代码例子中解决这些问题。

class Singleton:
   _instances = {}

   def __new__(cls, *args, **kwargs):
       if cls not in cls._instances:
           instance = super().__new__(cls)
           cls._instances[cls] = instance
       return cls._instances[cls]


class Logger(Singleton):
   def write_log(self, path):
       pass


if __name__ == "__main__":
   logger1 = Logger()
   logger2 = Logger()
   assert logger1 is logger2

那么,前面的例子中的问题已经解决了,但我们能不能采取更好的方法(没有继承)?

让我们来试试。

class Singleton(type):
   _instances = {}

   def __call__(cls, *args, **kwargs):
       if cls not in cls._instances:
           instance = super().__call__(*args, **kwargs)
           cls._instances[cls] = instance
       return cls._instances[cls]


class Logger(metaclass=Singleton):
   def write_log(self, path):
       pass


if __name__ == "__main__":
   logger1 = Logger()
   logger2 = Logger()
   assert logger1 is logger2

很好,可以了,但是我们应该再做一个调整--为我们的程序在多线程环境下运行做准备。

from threading import Lock, Thread


class Singleton(type):
   _instances = {}
   _lock: Lock = Lock()

   def __call__(cls, *args, **kwargs):
       with cls._lock:
           if cls not in cls._instances:
               instance = super().__call__(*args, **kwargs)
               cls._instances[cls] = instance
       return cls._instances[cls]


class Logger(metaclass=Singleton):
   def __init__(self, name):
       self.name = name

   def write_log(self, path):
       pass


def test_logger(name):
   logger = Logger(name)
   print(logger.name)


if __name__ == "__main__":
   process1 = Thread(target=test_logger, args=("FOO",))
   process2 = Thread(target=test_logger, args=("BAR",))
   process1.start()
   process2.start()

输出

FOO
FOO

两个进程都用两个不同的参数调用了构造函数,但只创建了一个Logger 类的实例--我们的辛苦工作终于结束了!

后果

  • 你知道,一个类只有一个实例。
  • 你获得了一个指向该实例的全局访问点。
  • 单例只有在第一次被请求时才会被初始化。
  • 在一定程度上掩盖了糟糕的设计。例如,当程序中的各个组件对彼此的了解太多。因此,许多人认为它是一种 反模式.

模式2:装饰器

装饰器是一种结构模式,其目的是在运行时为类/对象提供新的功能(与继承不同,它允许你实现类似的效果,但在编译时)。

装饰器通常是一个抽象类,它在构造函数中接受一个对象,我们想扩展它的功能 - 但在 Python 中,也有一个内置的装饰器机制,我们可以使用。

使用案例

  • 你想在运行时给对象分配额外的责任,而不破坏使用这些对象的代码。
  • 由于某些原因,你不能通过继承来扩展一个对象的职责。

代码示例

使用装饰器,你可以多次包装对象,因为目标和装饰器都实现了相同的接口。

产生的对象将具有所有包装器的组合和堆叠的功能。

from abc import ABC, abstractmethod


class Component(ABC):
   @abstractmethod
   def operation(self):
       pass


class ConcreteComponent(Component):
   def operation(self):
       return "ConcreteComponent"


class Decorator(Component):
   def __init__(self, component):
       self.component = component

   @abstractmethod
   def operation(self):
       pass


class ConcreteDecoratorA(Decorator):
   def operation(self):
       return f"ConcreteDecoratorA({self.component.operation()})"


class ConcreteDecoratorB(Decorator):
   def operation(self):
       return f"ConcreteDecoratorB({self.component.operation()})"


if __name__ == "__main__":
   concreteComponent = ConcreteComponent()
   print(concreteComponent.operation())
   decoratorA = ConcreteDecoratorA(concreteComponent)
   decoratorB = ConcreteDecoratorB(decoratorA)
   print(decoratorB.operation())

输出

ConcreteComponent
ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))

还有一个使用内置装饰器机制的稍微实用的例子。

import sys


def memoize(f):
   cache = dict()

   def wrapper(x):
       if x not in cache:
           cache[x] = f(x)
       return cache[x]

   return wrapper


@memoize
def fib(n):
   if n <= 1:
       return n
   else:
       return fib(n - 1) + fib(n - 2)


if __name__ == "__main__":
   sys.setrecursionlimit(2000)
   print(fib(750))

输出

2461757021582324272166248155313036893697139996697461509576233211000055607912198979704988704446425834042795269603588522245550271050495783935904220352228801000

如果不使用函数(递归计算斐波那契数列的第n项)的缓存装饰器,我们可能在一生中都不会计算出值100的结果。

后果

  • 在不创建子类的情况下扩展一个对象的行为。
  • 在运行时添加或删除对象的责任。
  • 通过对一个对象应用多个装饰器来组合多个行为。
  • 将一个实现了许多行为变体的单体类划分为更小的类。
  • 很难从包装器堆栈的中心取出一个特定的包装器。
  • 很难实现一个装饰器,使其行为不依赖于包装器的堆叠顺序。

模式3:迭代器

迭代器是一种行为模式,其目的是允许你在不暴露其底层表示的情况下遍历一个集合中的元素。

为了在Python中实现你的迭代器,我们有两种可能的选择。

  • 实施 __iter__[__next__](https://blog.finxter.com/python-__next__-magic-method/)类中的特殊方法。
  • 使用生成器

使用案例

  • 集合有一个复杂的结构,出于方便或安全的原因,你想把它从客户端隐藏起来。
  • 你想减少整个应用程序中的遍历代码的重复。
  • 你希望你的代码能够遍历不同数据结构的元素,或者当你事先不知道其结构的细节时。

代码示例

在下面的例子中,我们将看到如何创建一个带有字母顺序迭代器的自定义集合。

from collections.abc import Iterator, Iterable


class AlphabeticalOrderIterator(Iterator):
   _position: int = None
   _reverse: bool = False

   def __init__(self, collection, reverse=False):
       self._collection = sorted(collection)
       self._reverse = reverse
       self._position = -1 if reverse else 0

   def __next__(self):
       try:
           value = self._collection[self._position]
           self._position += -1 if self._reverse else 1
       except IndexError:
           raise StopIteration()
       return value


class WordsCollection(Iterable):
   def __init__(self, collection):
       self._collection = collection

   def __iter__(self):
       return AlphabeticalOrderIterator(self._collection)

   def get_reverse_iterator(self):
       return AlphabeticalOrderIterator(self._collection, True)


if __name__ == "__main__":
   wordsCollection = WordsCollection(["Third", "First", "Second"])
   print(list(wordsCollection))
   print(list(wordsCollection.get_reverse_iterator()))

输出

['First', 'Second', 'Third']
['Third', 'Second', 'First']

下一个例子是关于生成器的,它是一种特殊的函数,可以暂停并从暂停的地方恢复。

根据存储的状态,可以在后续调用生成器的过程中返回不同的值。

def prime_generator():
   yield 2
   primes = [2]
   to_check = 3
   while True:
       sqrt = to_check ** 0.5
       is_prime = True
       for prime in primes:
           if prime > sqrt:
               break
           if to_check % prime == 0:
               is_prime = False
               break
       if is_prime:
           primes.append(to_check)
           yield to_check
       to_check += 2


generator = prime_generator()
print([next(generator) for _ in range(20)])

输出

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]

后果

  • 你可以通过将遍历代码提取到单独的类中来清理客户端代码和集合。
  • 你可以实现新的集合类型和迭代器,并将它们传递到现有的代码中,而不会破坏任何东西。
  • 你可以用多个迭代器并行地迭代同一个集合,因为每个迭代器都存储了关于其迭代状态的信息。
  • 出于这个原因,你可以延迟迭代并在需要时继续迭代。
  • 如果你的应用程序只处理简单的集合,使用这种模式将是多余的。
  • 使用迭代器可能比直接遍历一些专门的集合的项目效率低。

结语

底线是即使你从未遇到文章中提到的设计模式所能解决的问题,了解模式仍然是有用的,因为它教你用面向对象的设计原则来解决问题。