Python 高手编程系列一百零八:在事件循环中使用 executors

Executor.submit()方法返回的 Future 类实例在概念上非常接近异步编程中
使用的协程。这就是为什么我们可以使用执行器在协同多任务和多进程或多线程之间进
行混合。
此解决方法的核心是事件循环类的 BaseEventLoop.run_in_executor(executor,
func, * args)方法。它会在进程池或线程池中调度执行由 executor 参数表示的 func
函数。这个方法最重要的是它返回一个新的 awaitable(一个可以用 await 语句的等待
的对象)。所以正因为如此,你可以执行一个阻塞函数,它不是一个协程,正如它是一个协
程,它不会阻塞,无论完成需要多长时间。它将只停止等待来自这样的调用的结果的函数,
但是整个事件循环将仍然保持旋转。
事实上,你甚至不需要创建你的执行者实例。如果你传递 None 作为执行器参数,
ThreadPoolExecutor 类将使用默认线程数(对于 Python 3.5,它是处理器数乘以 5)。
所以,让我们假设我们不想重写 python-gmaps 软件包的问题部分,这是造成我们
头痛的原因。我们可以很容易地使用 loop.run_in_executor()调用将阻塞调用推迟到
一个单独的线程,同时仍然将 fetch_place()函数作为一个可以等待的协程:
async def fetch_place(place):
coro = loop.run_in_executor(None, api.geocode, place)
result = await coro
return result[0]
对于这个工作,这样的解决方案不如一个完全的异步库,但你应该知道有总比没有好。
小结
这是一个漫长的旅程,最终我们成功地奋力学习了并发编程的最基本的方法,这是
Python 程序员应该掌握的知识。
在解释了什么是并发之后,我们付诸行动,并在多线程的帮助下仔细分析了一个典型
的并发问题。在确定我们的代码的基本缺陷并修复它们之后,我们转向多进程以了解它在
我们的例子中是如何工作的。
我们发现基于 multiprocessing 模块使用多进程比基于 threading 使用基本线程
更容易。但是在那之后,我们意识到,由于 multiprocessing.dummy,我们可以使用
与线程相同的 API。因此,多进程和多线程之间的选择现在只是一个问题,哪个解决方案
更适合问题,而不是哪个解决方案有更好的接口。
谈到问题适合,我们终于尝试异步编程,这应该是 I/O 密集型应用程序的最佳解决方
案,只有意识到我们不能完全忘记线程和进程。所以我们做了一个圈子,回到我们开始的
地方!
这使我们得到这一章的最终结论。没有高招。有一些方法,你可能喜欢或更喜欢。有
一些方法可能更适合给定的一组问题,但你需要知道他们都是为了成功。在现实的情况下,
你可能会发现自己在单个应用程序中使用了全部并发工具和模式,这并不少见。
前面的结论是对下一章主题的一个很好的介绍。这是因为没有一个单一的模式能解决
所有的问题。你应该尽可能多的了解,因为最终你会在日常工作中使用它们。
有用的设计模式
针对软件设计的常见问题,设计模式是可复用的且有点语言相关的解决方案。关于这个主
题的最流行的书是 Design Patterns: Elements of Reusable Object-Oriented Software,这本书由
Gamma、Helm、Johnson 和 Vlissides 编写,它们也被称为四人组(Gang of Four)或 GoF。这
本书是这个领域中的重要著作,它收录了 23 种设计模式,并使用 SmallTalk 和 C++编写了示例。
在设计应用程序的代码时,这些模式有助于解决常见问题。所有开发人员对它们都似
曾相识,因为它们描述了已验证的开发范例。但是在学习这些模式时应该考虑使用的语言,
因为其中一些在某些语言中没有意义或者语言中已经内置了。
本章介绍了 Python 中最有用的以及最值得大家讨论的模式,以及实现示例。以下 3 个
部分对应于 GoF 定义的设计模式类别。
• 创建型模式(creational patterns):这些模式用于生成具有特定行为的对象。
• 结构型模式(structural patterns):这些模式有助于为特定用例构建代码。
• 行为模式(behavioral patterns):这些模式有助于分配责任和封装行为。
创建型模式
创建型模式处理对象实例化机制。这样的模式可以定义如何创建对象实例或者甚至如
何构造类的方式。
这些是编译型语言(如 C 或 C ++)中非常重要的模式,因为在运行时难以生成需要的类型。
但是在运行时创建新类型在 Python 中是相当简单的。使用内置的 type 函数可以通过
代码定义一个新类型的对象如下:

MyType = type(‘MyType’, (object,), {‘a’: 1})
ob = MyType()
type(ob)
<class ‘main.MyType’>
ob.a
1
isinstance(ob, object)
True
类和类型是内置工厂。我们已经处理了创建新的类对象,并且可以使用元类与类和对
象生成进行交互。这些特性是实现工厂(factory)设计模式的基础,但我们不会在本节进一步
描述它,因为我们在第 3 章广泛地讨论过类和对象创建的主题。
除了工厂设计模式,在 Python 中,GoF 中唯一值得注意的其他创建型设计模式是单例。
单例
单例(Singleton)限制类的实例化,只能实例化一个对象。
单例模式确保给定类在应用程序中始终只有一个存活的实例。例如,当你想要将资源访问
限制为该进程中的一个且仅一个内存上下文时,可以使用此方法。例如,数据库连接器类可以
是一个单例,它处理同步并且在内存中管理其数据。假设在此期间没有其他实例与数据库交互。
这种模式可以简化很多在应用程序中处理并发的方式。提供应用程序范围的功能的通
用程序通常被声明为单例。例如,在 Web 应用程序中,负责保留唯一文档 ID 的类将受益
于单例模式。应该有且只有一个通用程序做这个工作。
在 Python 中,一个常用的方法是通过覆写__new__()方法创建单例如下:
class Singleton:
_instance = None
def new(cls, *args, **kwargs):
if cls._instance is None:
cls.instance = super().new(cls, *args, **kwargs)
return cls.instance
如果你尝试创建该类的多个实例并比较它们的 ID,你会发现它们都表示同一个对象,
如下所示:
instance_a = Singleton()
instance_b = Singleton()
id(instance_a) == id(instance_b)
True
instance_a == instance_b
True
我将其称之为半习语,因为它是一个非常危险的模式。如果你已经创建了一个基类的
实例,那么当你尝试对你的单例基类进行子类化并创建一个新的子类的实例时,问题就开始了,如下所示:
class ConcreteClass(Singleton): pass
Singleton()
<Singleton object at 0x000000000306B470>
ConcreteClass()
<Singleton object at 0x000000000306B470>
当你注意到此行为受实例创建的顺序影响时,这可能会变得更加有问题。根据你的类
的使用顺序,你可能会也可能不会得到相同的结果。如果你先创建子类的实例,然后,再
创建基类的实例,让我们看看结果如下:
class ConcreteClass(Singleton): pass
ConcreteClass()
<ConcreteClass object at 0x00000000030615F8>
Singleton()
<Singleton object at 0x000000000304BCF8>
正如你所看到的,行为是完全不同的,而且难以预测。在大型应用中,它可能导致非
常危险并且难以调试的问题。根据运行时上下文,你可能使用或不使用你的目标类。因为
这样的行为真的难以预测和控制,所以应用可能由于导入顺序的改变或甚至用户输入而中
断。如果你的单例不是被子类化的,那么实现这种方式可能是相对安全的。无论如何,这
是一个定时炸弹。如果未来有人忽略这个风险,并决定使用单例对象创建一个子类,一切
都可能爆炸。避免这种特定实现的更安全的方式是使用替代方案。
更安全的方式是使用更先进的技术—元类(metaclasses)。通过覆写元类的__call
()
方法,可以影响自定义类的创建。这样可以创建一个可重用的单例代码如下所示:
class Singleton(type):
_instances = {}
def call(cls, *args, **kwargs):
if cls not in cls._instances:
cls.instances[cls] = super().call(*args,**kwargs)
return cls.instances[cls]
通过使用这个 Singleton 作为你的自定义类的元类,你可以获得可以安全子类化并
且与实例创建顺序无关的单例如下所示:
ConcreteClass() == ConcreteClass()
True
ConcreteSubclass() == ConcreteSubclass()
True
ConcreteClass()
<ConcreteClass object at 0x000000000307AF98>
ConcreteSubclass()
<ConcreteSubclass object at 0x000000000307A3C8>
另一种克服琐碎单例实现问题的方法是由 Alex Martelli 提出的。他提出的这种方式在
行为上类似于单例,但结构上完全不同。这不是来自 GoF 书的经典设计模式,但它似乎在
Python 开发人员中很常见。它被称为博格(Borg)或单态(Monostate)。
这个想法很简单。在单例模式中真正重要的不是类的存活实例的数量,而是它们在任
何时候都共享相同的状态的事实。因此,Alex Martelli 想出了一个类,使类的所有实例共
享同一个__dict

class Borg(object):
state = {}
def new(cls, *args, **kwargs):
ob = super().new(cls, *args, **kwargs)
ob.dict = cls.state
return ob
这修复了子类化问题,但仍然依赖于子类代码如何工作。例如,如果覆写__getattr

则模式可能会被破坏。
然而,单例不应该有几个层级的继承。标记为单例的类已经是特定的。
也就是说,这种模式被许多开发者认为是一种处理应用程序唯一性的重要方式。如果需
要单例,为什么不使用带有函数的模块,因为 Python 模块已经是单例了?最常见的模式是将
模块级变量定义为需要单例的类的实例。这样,你也不会在你的初始设计中限制开发人员。

  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值