Python设计模式:为了圆滑时尚的代码


/**
 * 谨献给可爱的小黑
 *
 * 原文出处:https://www.toptal.com/python/python-design-patterns
 * @author dogstar.huang <chanzonghuang@gmail.com> 2016-04-02
 */

再次说明:Python是一门有动态类型和动态绑定的高级编程语言。我会把它描述为一门强大的高级动态语言。很多 开发人员深爱Python是因为它清晰的语法,良好的结构模块和包,以及它巨大的灵活性和众多的现代特性。

在Python,不会有任何东西强迫你去写类或者实例化对象。如果在项目中不需要复杂的结构,你可以只写函数。 甚至乎,你可以针对某些简单和快速的任务编写一个无须任何结构化代码的小脚本。

同时Python是一门100%面向对象的语言。为什么这么说?好吧,简单来说,在Python中的所有东西都是对象。
函数是对象,第一类对象(不管这意味着什么)。函数是对象这一事实很重要,所以请记住这一点。

那么,你可以用Python编写简单的脚本,或者只是打开Python终端并且在那执行声明(那样正是太酷了!)。但 与此同时,你可以创建复杂框架,应用,类库等等。用Python你可以做很多事情。当然也有很多限制,但这不是本 次的主题。

然而,因为Python是如此强大和灵活,我们需要一些编程的规则(或者模式)。那么,让我们来看一下这些模式是 什么,他们又是如何与Python关联的。我们也会继续实现一些必要的Python设计模式。

为什么Python有利于模式?

任何语言都利于模式的。事实上,在给定的任何编程语言的上下文都应该考虑到模式。这两种模式,语言语法和 本质上强加给我们编程的限制。来自语言语法和语言本质(诸如动态、功能性的、面向对象的此类)的限制是不 同的,因为他们存在的背后是有原因的。来自模式的限制就是其中一个原因,他们太强大了。这是模式的基本目 标:告诉我们怎么做一些事情以及如何去做。稍候我们会讨论模式,尤其是Python设计模式。

Python是动态灵活的语言。Python设计模式是使用它巨大潜力的好方法。

Python的理念是建立在最佳实践的想法之上的。Python是一门动态语言(我是不是已经说过了?)并且正因
如此,大量流行的设计模式通过几行的代码就已经被实现了,或者很容易实现。一些设计模式构成了Python,所 以我们甚至不知道已经在使用了。由于语言的本质其他模式并不是必需的。

例如,工厂方法 是一个致力于创建新对象、向用户隐藏安装逻辑的结构化Python设计模式。但是在Python里对 象的创建被设计成是动态的,所以像工厂方法这样的添加是没有必要的。当然,你想的话也是可以实现的。有可 能这样会很有用,但只是个例外,而不是常态。

Python的理念有哪些好的地方?让我们从this (在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!  

在传统的感知上这可能不是模式,但是这有定义了如何编写最优雅、有用、时尚的代码的“Python化”方式。

我们也还有PEP-8代码方针来帮助组织代码。这对于我来说是必须的,当然也会有一些例外。顺便说一下,这些例 外也是PEP-8本身所鼓励的:

但更为重要的是:知道何时不符 -- 有时风格规范并不适用。当有困惑时,使用您最佳的判断。看下其他的例子
然后决定怎样看起来最好。并且不要犹豫来提问!

把PER-8和Python禅道(也称为PEP - PEP-20)结合起来,你将会得到一个创建可读和可维护代码的完美的依据。
再加以设计模式你就能创建各种各样有一致性和演进能力的系统了。

Python设计模式

设计模式是什么?

所在这一切都始于四人帮(GOF)。如果不熟悉四人帮 可以快速在网上搜索一下。

设计模式是一种解决常见问题的通用方式。在设计模式里的两条基本主要原则由GOF定义:
+ 针对接口编程,而不是针对实现编程。 + 优先使用对象组合,而不是类继承。

让我们通过透视Python编程来更近一步了解这两个原则。

针对接口编程,而不是针对实现编程

想一下鸭子类型。在Python我们不喜欢定义接口以及根据这些 接口编写类,不是吗?但是,听我说!这不意味着我们不会思考接口,事实上我们一直在用鸭子类型这么做。

再来说一下关于鲜为人知的鸭子类型方式,以便看下它在此范例中是如何满足的:针对接口编程。

如果它看起来像个鸭子并且像一个鸭子一样嘎嘎,那它就是一个鸭子 !

我们不用纠结于对象的本质,我们不用关心对象是什么;我们只是想知道它可以做哪些我们要想的事情(我们只 是对对象的接口感兴趣)。

对象可以嘎嘎吗?可以的话,那就让它嘎一下!

try:  
    bird.quack()
except AttributeError:  
    self.lol()

我们有为鸭子定义接口了吗?没有!我们有针对接口编程而不是针对实现编程了吗?有!并且,我发现这样很好。

正如亚历克斯·马尔泰利在他关于Python设计模式的著名陈述中指出的那样:“教会鸭子输入需要花一点时间, 但毕竟可以节省你大量的工作!”

优先使用对象组合,而不是类继承

现在,这就是我称之为 Python化 的原则!相比于把一个类(或者经常更多是若干个类)封装到另一个类,我创 建了更少的类/子类。

为了取代这样的做法:

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来说更自然。

行为模式

行为模式涉及对象间的通信,对象如何交互以及履行一个既定的任务。根据四人帮的原则,在Python中总共有11个行为 模式:Chain of responsibility(职能链),Command(命令),Interpreter(解释器),Iterator(迭代器), Mediator(中介者),Memento(备忘录),Observer(观察者),State(状态),Strategy(策略),Template(模板方法),Visitor(访问者)

行为模式处理跨对象通信,控制大量对象如何进行交互以及执行不同的任务。

我发现这些模式非常有用,但这不是意味着另一组模式作用没那么大。

Iterator(迭代器)

迭代器被构建进了Python。它是这个语言最强大的特性之一。多年前,我在某处读到迭代器使得Python变得很棒, 到现在我仍然是这么认为的。对Python迭代器和生成器了解足够多的话你会知道关于这些独特Python模式需要知道的一切。

Chain of responsibility(职能链)

这个模式为我们提供了一种使用不同方法对待一个请求的方式,每一个都占据了此请求某个指定的部分。你知道的, 对于好的代码最好的原则之一是 单一职责原则

每一块代码应该做一件事,并且只做一件事。

在此设计模式里深深地集成了这个原则。

例如,如果想过滤某些内容我们可以实现不同的过滤器,每一个过滤器做一件精确的事情且清晰地定义了过滤的类型。 这些过滤器可用于过滤有攻击性的词语,广告,不良的视频内容等。

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)  

Command(命令)

这是我作为程序员实现的Python设计模式之一。这提醒了我:模式不是被发明的,而是被发现的。 他们存在那 里,我们只需要发现和把他们用起来。很多年前为了实现我们的一个惊人的项目我发现了这一点:一个特别目的的 所见即所得(WYSIWYM) XML编辑器。在代码中集中使用这个模式后,我在另外一些站点上看到了它更多的身影。

出于某些原因,当我们需要准备好什么将会执行并且随后在需要时执行它时,命令模式是在这种情况下是便利的。 这样的好处是在这种方式下封装的动作允许Python开发人员添加额外与执行 的动作相关的功能,例如undo/redo,或者保持历史动作诸如此类。

让我们看下一个简单但经常使用的示例:

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

Creational Patterns(创造者模式)

首先需要指出的是创建者模式在Python中并不常用。为什么?因为Python语言本身的动态本质。

某位比我更有智慧的人曾经说过工厂方法纳入到了Python。这意味着这门语言本身为我们提供了需要以一种足够 优雅时尚的方式创造对象的全部灵活性;我们少之甚少需要在这之上实现任何东西,如单例和工厂方法。

在某个Python设计模式向导里,我发现了一个创造者模式描述这样声明设计 “模式提供了一种隐藏创造逻辑的方 式来创建对象,而不是直接使用new 操作符来初始化”。

它恰到好处地总结了这个问题:我们不需要在Python中使用new 操作符!

不管怎样,让我们来看下如何实现其中的某一小部分,看下通过这样的模式我们能否感觉到从中得到好处。

Singleton(单例)

单例模式用于当我们想保证对于某个指定的类在运行时只存在一个实例时。在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 是一个单例。

在Python中使用单例模式有以下这些备选方案:
+ 使用模块
+ 在应用中某处高级别的地方创建一个实例,可以是配置文件
+ 把实例传给每一个需要它的实例。这是依赖注入并且它是一个强大且简单的管理机制。

Dependency Injection(依赖注入)

我不打算陷入依赖注入是不是一个设计模式的讨论,但我想说的是它是一个非常好的机制来实现更少耦合,并且 它有助于让我们的应用变更可维护和可扩展。把它和鸭子类型结合起来然后原力会与你同在。阿门。

鸭子?人类?Python都不关心。它灵活得很!

我之所在这篇文章的创造者模式部分列出来是因为它处理了何时(或者更好是:何地)创建对象这个问题。它是 在外部创造的。更准确来说对象并不是在我们使用它们的地方才创建的,所以依赖不是在消费它那里才创建的。 客户代码接收这些外部的对象并使用它。更多参考,请看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这个类中注入了authenticator _ 和authorizer _ 方法。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(工厂方法) 设计模式。

结构化模式

Facade(外观模式)

这可能是最为出名的Python设计模式。

假设你有一个系统有着大量的对象。每一个对象都提供了一系列丰富的接口方法 。你可以使用这个系统做很多很 多事情,但是如何简化这个接口呢?为什么不添加一个接口接口来暴露一个更好的深思熟虑的API方法子集呢?用Facade(外观模式)!

Facade(外观模式)是一个优雅的Python设计模式。它是精简接口的一个完美方式。

Python外观模式示例:

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类是一个外观模式 ,就是这样。

Adapter(适配器)

如果说外观模式 用于精简接口,那么适配器 则是关于修改接口。就像当系统期望想要一个鸭子时却只有奶牛。

假设你有一个可工作的方法把日志信息纪录到给定的目的地。你的方法期望目的地有write()方法(例如, 因为每个文件对象都有)。

def log(message, destination):  
    destination.write('[{}] - {}'.format(datetime.now(), message))

我会说这是一个写得很好的带依赖注入的方法,它允许巨大的扩展能力。假设你想要纪录到某些UDP socket而不是 某个文件,你知道如何打开这个UDP socket但唯一的问题是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中实现他们是 多么的简单,并且想一下在你的项目中可能通过哪些不同的方式来使用。

Decorator(装饰者模式)

噢我们真是太幸运了!装饰者模式 真的很好,而且我们也已经集成到了这门语言中。在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)  

现在这个excute方法是:
+ 简单易读
+ 只做一件事(至少看到这些代码时)
+ 带认证装饰
+ 带授权装饰

使用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()

注意到你没有限制像装饰者那样活动 是重要的。一个装饰者可能涵盖了整个类。唯一的要求是他们必须 是可以调用的 。但对此我们毫无问题;我们只需要定义__call__(self)即可。

你可能也想再进一步了解Python的函数工具(functools)模块。有待发现的东西多着哩!

结论

我已经展示了使用Python的设计模式是多么的自然和容易,但我也展示了如何在Python中编程要来得容易。

“简单胜于复杂,” 还记得吗?也许你已经注意到没有一个设计模式是完全且正式描述的。没有展示复杂全面的
实现。你需要去“感觉”并且以最适合你的风格和需要的方式实现他们。Python是一门伟大的语言,它给了你创作 灵活、可重用代码的全部力量。

然而,它给你的远胜于这些。它给了你编写真正坏代码 的“自由”。不要这么做!不要重复自己(DRY)并且不 要写超过80个字符的代码行。还有不要忘了在合适的地方使用设计模式;它是向他人学习以及免费获取他们丰富 经验的最好方式之一。

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州