Python 设计模式:用于时尚时尚的代码
已发表: 2022-03-11再说一遍:Python 是一种具有动态类型和动态绑定的高级编程语言。 我会将其描述为一种功能强大的高级动态语言。 许多开发人员都爱上了 Python,因为它的语法清晰、结构良好的模块和包,以及它巨大的灵活性和现代特性的范围。
在 Python 中,没有什么要求您编写类并从中实例化对象。 如果您的项目中不需要复杂的结构,您可以只编写函数。 更好的是,您可以编写一个平面脚本来执行一些简单而快速的任务,而无需构建代码。
同时 Python 是 100% 面向对象的语言。 怎么样? 嗯,简单地说, Python 中的一切都是对象。 函数是对象,第一类对象(无论这意味着什么)。 函数是对象这一事实很重要,所以请记住这一点。
因此,您可以在 Python 中编写简单的脚本,或者直接打开 Python 终端并在此处执行语句(这非常有用!)。 但同时,您可以创建复杂的框架、应用程序、库等。 你可以在 Python 中做很多事情。 当然有一些限制,但这不是本文的主题。
然而,由于 Python 如此强大和灵活,我们在使用它进行编程时需要一些规则(或模式)。 那么,让我们看看什么是模式,以及它们与 Python 的关系。 我们还将继续实现一些基本的 Python 设计模式。
为什么 Python 对模式有好处?
任何编程语言都适用于模式。 事实上,模式应该在任何给定编程语言的上下文中考虑。 模式、语言语法和性质都对我们的编程施加了限制。 来自语言语法和语言性质(动态、功能、面向对象等)的限制可能不同,它们存在的原因也可能不同。 来自模式的限制是有原因的,它们是有目的的。 这是模式的基本目标; 告诉我们如何做某事以及如何不做。 稍后我们将讨论模式,尤其是 Python 设计模式。
Python 的哲学建立在经过深思熟虑的最佳实践的理念之上。 Python 是一种动态语言(我已经说过了吗?),因此,它已经实现了或使其易于实现,只需几行代码即可实现许多流行的设计模式。 一些设计模式是内置在 Python 中的,所以我们甚至在不知道的情况下使用它们。 由于语言的性质,不需要其他模式。
例如, Factory是一种结构化 Python 设计模式,旨在创建新对象,对用户隐藏实例化逻辑。 但是在 Python 中创建对象是动态设计的,因此不需要像 Factory 这样的添加。 当然,如果你愿意,你可以自由地实现它。 可能在某些情况下它会非常有用,但它们是一个例外,而不是常态。
Python 的哲学有什么好的? 让我们从这个开始(在 Python 终端中探索它):
> >> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
这些可能不是传统意义上的模式,但这些规则以最优雅和最有用的方式定义了“Pythonic”编程方法。
我们还有 PEP-8 代码指南来帮助构建我们的代码。 当然,这对我来说是必须的,当然也有一些适当的例外。 顺便说一句,PEP-8 本身鼓励这些例外:
但最重要的是:知道何时不一致——有时风格指南并不适用。 如有疑问,请使用您的最佳判断。 查看其他示例并决定什么看起来最好。 不要犹豫,问!
将 PEP-8 与 The Zen of Python(也是 PEP - PEP-20)结合起来,您将拥有创建可读和可维护代码的完美基础。 添加设计模式,您就可以创建具有一致性和可演化性的各种软件系统。
Python 设计模式
什么是设计模式?
一切都始于四人帮 (GOF)。 如果您不熟悉 GOF,请进行快速在线搜索。
设计模式是解决众所周知的问题的常用方法。 GOF 定义的设计模式的基础有两个主要原则:
- 编程到接口而不是实现。
- 优先考虑对象组合而不是继承。
让我们从 Python 程序员的角度来仔细看看这两个原则。
编程到接口而不是实现
想想鸭子打字。 在 Python 中,我们不喜欢根据这些接口定义接口和程序类,是吗? 但是,听我说! 这并不意味着我们不考虑接口,实际上使用 Duck Typing 我们一直都在这样做。
让我们对臭名昭著的 Duck Typing 方法说几句,看看它是如何适应这种范式的:程序到接口。
我们不关心对象的性质,我们不必关心对象是什么; 我们只想知道它是否能够做我们需要的事情(我们只对对象的接口感兴趣)。
物体会嘎嘎叫吗? 所以,让它嘎嘎叫吧!
try: bird.quack() except AttributeError: self.lol()
我们是否为我们的鸭子定义了一个接口? 不! 我们是否针对接口而不是实现进行编程? 是的! 而且,我觉得这很好。
正如 Alex Martelli 在他关于 Python 中的设计模式的著名演讲中指出的那样, “教鸭子打字需要一段时间,但之后会为您节省大量工作!”
优先考虑对象组合而不是继承
现在,这就是我所说的Pythonic原则! 与在另一个类中包装一个类(或更常见的是几个类)相比,我创建了更少的类/子类。
而不是这样做:
class User(DbObject): pass
我们可以这样做:
class User: _persist_methods = ['get', 'save', 'delete'] def __init__(self, persister): self._persister = persister def __getattr__(self, attribute): if attribute in self._persist_methods: return getattr(self._persister, attribute)
优势是显而易见的。 我们可以限制要暴露的包装类的哪些方法。 我们可以在运行时注入持久化实例! 例如,今天它是一个关系数据库,但明天它可以是任何我们需要的接口(同样是那些讨厌的鸭子)。
组合对 Python 来说是优雅而自然的。
行为模式
行为模式涉及对象之间的通信,对象如何交互和完成给定任务。 根据 GOF 原则,Python 中共有 11 种行为模式:责任链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板、访问者。
我发现这些模式非常有用,但这并不意味着其他模式组没有。
迭代器
迭代器内置在 Python 中。 这是该语言最强大的特征之一。 多年前,我在某处读到迭代器使 Python 变得很棒,我认为情况仍然如此。 对 Python 迭代器和生成器有足够的了解,你就会知道关于这个特定的 Python 模式所需的一切。
责任链
这种模式为我们提供了一种使用不同方法处理请求的方法,每种方法都针对请求的特定部分。 你知道,优秀代码的最佳原则之一是单一职责原则。
每段代码都必须做一件事,而且只能做一件事。
这个原则深深地融入了这个设计模式。
例如,如果我们想要过滤一些内容,我们可以实现不同的过滤器,每个过滤器都执行一种精确且明确定义的过滤类型。 这些过滤器可用于过滤冒犯性词语、广告、不合适的视频内容等。
class ContentFilter(object): def __init__(self, filters=None): self._filters = list() if filters is not None: self._filters += filters def filter(self, content): for filter in self._filters: content = filter(content) return content filter = ContentFilter([ offensive_filter, ads_filter, porno_video_filter]) filtered_content = filter.filter(content)
命令
这是我作为程序员实现的第一个 Python 设计模式之一。 这提醒了我:模式不是发明的,而是被发现的。 它们存在,我们只需要找到并使用它们。 我在多年前实施的一个惊人项目中发现了这个:一个特殊用途的 WYSIWYM XML 编辑器。 在代码中大量使用这种模式之后,我在一些网站上阅读了更多关于它的信息。
命令模式在某些情况下很方便,因为某种原因,我们需要首先准备将要执行的内容,然后在需要时执行它。 这样做的好处是,以这种方式封装动作使 Python 开发人员能够添加与已执行动作相关的附加功能,例如撤消/重做,或保留动作历史记录等。
让我们看看一个简单且经常使用的示例是什么样的:
class RenameFileCommand(object): def __init__(self, from_name, to_name): self._from = from_name self._to = to_name def execute(self): os.rename(self._from, self._to) def undo(self): os.rename(self._to, self._from) class History(object): def __init__(self): self._commands = list() def execute(self, command): self._commands.append(command) command.execute() def undo(self): self._commands.pop().undo() history = History() history.execute(RenameFileCommand('docs/cv.doc', 'docs/cv-en.doc')) history.execute(RenameFileCommand('docs/cv1.doc', 'docs/cv-bg.doc')) history.undo() history.undo()
创作模式
让我们首先指出创建模式在 Python 中并不常用。 为什么? 因为语言的动态特性。
比我更聪明的人曾经说过,Factory 是内置在 Python 中的。 这意味着语言本身为我们提供了以足够优雅的方式创建对象所需的所有灵活性; 我们很少需要在上面实现任何东西,比如 Singleton 或 Factory。
在一个 Python 设计模式教程中,我找到了对创建设计模式的描述,其中指出这些设计“模式提供了一种在隐藏创建逻辑的同时创建对象的方法,而不是直接使用new运算符实例化对象。”
这几乎总结了问题:我们在 Python 中没有新的运算符!
尽管如此,让我们看看我们如何实现一些,如果我们觉得我们可能会通过使用这些模式获得优势。
辛格尔顿
当我们想要保证在运行时只存在给定类的一个实例时,使用单例模式。 我们真的需要 Python 中的这种模式吗? 根据我的经验,故意创建一个实例然后使用它而不是实现单例模式更容易。
但是如果你想实现它,这里有一些好消息:在 Python 中,我们可以改变实例化过程(以及几乎任何其他东西)。 还记得我之前提到的__new__()
方法吗? 开始了:

class Logger(object): def __new__(cls, *args, **kwargs): if not hasattr(cls, '_logger'): cls._logger = super(Logger, cls ).__new__(cls, *args, **kwargs) return cls._logger
在这个例子中, Logger是一个 Singleton。
这些是在 Python 中使用 Singleton 的替代方法:
- 使用一个模块。
- 在应用程序的顶层某处创建一个实例,可能在配置文件中。
- 将实例传递给需要它的每个对象。 这是一种依赖注入,它是一种强大且易于掌握的机制。
依赖注入
我不打算讨论依赖注入是否是一种设计模式,但我会说它是一种非常好的实现松散耦合的机制,它有助于使我们的应用程序可维护和可扩展。 将它与 Duck Typing 结合使用,原力将与您同在。 总是。
我在这篇文章的创建模式部分列出了它,因为它处理了何时(或者更好:在哪里)创建对象的问题。 它是在外面创建的。 最好说对象根本不是在我们使用它们的地方创建的,因此依赖关系不是在使用它的地方创建的。 消费者代码接收外部创建的对象并使用它。 如需进一步参考,请阅读此 Stackoverflow 问题的最受好评的答案。
这是对依赖注入的一个很好的解释,让我们对这种特殊技术的潜力有了一个很好的了解。 基本上,答案通过以下示例解释了问题:不要自己从冰箱里拿东西喝,而是说明需要。 告诉你的父母你需要在午餐时喝点东西。
Python 为我们提供了轻松实现它所需的一切。 想想它在 Java 和 C# 等其他语言中的可能实现,你会很快意识到 Python 的美妙之处。
让我们考虑一个简单的依赖注入示例:
class Command: def __init__(self, authenticate=None, authorize=None): self.authenticate = authenticate or self._not_authenticated self.authorize = authorize or self._not_autorized def execute(self, user, action): self.authenticate(user) self.authorize(user, action) return action() if in_sudo_mode: command = Command(always_authenticated, always_authorized) else: command = Command(config.authenticate, config.authorize) command.execute(current_user, delete_user_action)
我们在 Command 类中注入验证器和授权器方法。 Command 类所需要的只是成功执行它们,而不用担心实现细节。 这样,我们可以将 Command 类与我们决定在运行时使用的任何身份验证和授权机制一起使用。
我们已经展示了如何通过构造函数注入依赖项,但我们可以通过直接设置对象属性轻松注入它们,从而释放更多潜力:
command = Command() if in_sudo_mode: command.authenticate = always_authenticated command.authorize = always_authorized else: command.authenticate = config.authenticate command.authorize = config.authorize command.execute(current_user, delete_user_action)
还有更多关于依赖注入的知识; 例如,好奇的人会搜索 IoC。
但在您这样做之前,请阅读另一个 Stackoverflow 答案,这是对该问题最受好评的答案。
同样,我们刚刚演示了如何在 Python 中实现这种美妙的设计模式只是使用该语言的内置功能的问题。
让我们不要忘记这一切意味着什么:依赖注入技术允许非常灵活和简单的单元测试。 想象一个架构,您可以在其中动态更改数据存储。 模拟数据库成为一项微不足道的任务,不是吗? 有关更多信息,您可以查看 Toptal 的 Python 模拟简介。
您可能还想研究Prototype 、 Builder和Factory设计模式。
结构模式
正面
这很可能是最著名的 Python 设计模式。
想象一下,您有一个包含大量对象的系统。 每个对象都提供一组丰富的 API 方法。 你可以用这个系统做很多事情,但是简化界面怎么样? 为什么不添加一个接口对象来公开所有 API 方法的经过深思熟虑的子集? 门面!
Python Facade 设计模式示例:
class Car(object): def __init__(self): self._tyres = [Tyre('front_left'), Tyre('front_right'), Tyre('rear_left'), Tyre('rear_right'), ] self._tank = Tank(70) def tyres_pressure(self): return [tyre.pressure for tyre in self._tyres] def fuel_level(self): return self._tank.level
没有惊喜,没有技巧, Car
类是Facade ,仅此而已。
适配器
如果使用Facades来简化界面,那么Adapters就是为了改变界面。 就像系统期待鸭子时使用牛一样。
假设您有一种将信息记录到给定目的地的工作方法。 您的方法期望目标具有write()
方法(例如,每个文件对象都有)。
def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message))
我会说这是一个写得很好的依赖注入方法,它具有很好的可扩展性。 假设你想记录到某个 UDP 套接字而不是文件,你知道如何打开这个 UDP 套接字,但唯一的问题是socket
对象没有write()
方法。 你需要一个适配器!
import socket class SocketWriter(object): def __init__(self, ip, port): self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._ip = ip self._port = port def write(self, message): self._socket.send(message, (self._ip, self._port)) def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message)) upd_logger = SocketWriter('1.2.3.4', '9999') log('Something happened', udp_destination)
但为什么我发现适配器如此重要? 好吧,当它与依赖注入有效结合时,它为我们提供了巨大的灵活性。 当我们只需实现一个适配器,将新接口转换为众所周知的接口时,为什么要更改我们经过良好测试的代码以支持新接口?
您还应该检查并掌握桥接和代理设计模式,因为它们与适配器相似。 想一想在 Python 中实现它们有多么容易,并想一想在项目中使用它们的不同方式。
装饰器
哦,我们是多么幸运! 装饰器非常好,我们已经将它们集成到语言中。 我最喜欢 Python 的地方在于,使用它可以教会我们使用最佳实践。 并不是说我们不必意识到最佳实践(尤其是设计模式),但是对于 Python,我觉得我无论如何都在遵循最佳实践。 就我个人而言,我发现 Python 的最佳实践是直观的和第二天性的,这是新手和精英开发人员都喜欢的东西。
装饰器模式是关于引入额外的功能,特别是在不使用继承的情况下进行。
那么,让我们看看我们如何在不使用 Python 内置功能的情况下装饰方法。 这是一个简单的例子。
def execute(user, action): self.authenticate(user) self.authorize(user, action) return action()
这里不太好的地方在于, execute
函数的作用远不止执行某些东西。 我们没有严格遵守单一责任原则。
最好只写以下内容:
def execute(action): return action()
我们可以在另一个地方,在装饰器中实现任何授权和身份验证功能,如下所示:
def execute(action, *args, **kwargs): return action() def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizeddError return decorated execute = authenticated_only(execute) execute = authorized_only(execute)
现在execute()
方法是:
- 简单易读
- 只做一件事(至少在查看代码时)
- 装饰有认证
- 装饰有授权
我们使用 Python 的集成装饰器语法编写相同的代码:
def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs ) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizedError return decorated @authorized_only @authenticated_only def execute(action, *args, **kwargs): return action()
重要的是要注意,您不仅限于作为装饰器的功能。 装饰器可能涉及整个类。 唯一的要求是它们必须是callables 。 但是我们对此没有任何问题; 我们只需要定义__call__(self)
方法。
您可能还想仔细查看 Python 的 functools 模块。 那里有很多发现!
结论
我已经展示了使用 Python 的设计模式是多么自然和容易,但我也展示了 Python 编程应该如何轻松进行。
“简单胜于复杂”,还记得吗? 也许您已经注意到,没有一个设计模式被完整而正式地描述。 没有显示复杂的全面实施。 您需要以最适合您的风格和需求的方式“感受”并实施它们。 Python 是一门很棒的语言,它为您提供了生成灵活和可重用代码所需的所有功能。
然而,它给你的远不止这些。 它给了你编写非常糟糕的代码的“自由”。 不要这样做! 不要重复自己 (DRY) 并且永远不要编写超过 80 个字符的代码行。 并且不要忘记在适用的情况下使用设计模式; 这是向他人学习并免费从他们的丰富经验中获益的最佳方式之一。