标签归档:设计模式

Python 想了解EventLoop?这篇文章就够了

原文来自 python-parallel-programming-cookbook-cn

Python的Asyncio模块提供了管理事件、协程、任务和线程的方法,以及编写并发代码的原语。此模块的主要组件和概念包括:

  • 事件循环: 在Asyncio模块中,每一个进程都有一个事件循环。
  • 协程: 这是子程序的泛化概念。协程可以在执行期间暂停,这样就可以等待外部的处理(例如IO)完成之后,从之前暂停的地方恢复执行。
  • Futures: 定义了 Future 对象,和 concurrent.futures 模块一样,表示尚未完成的计算。
  • Tasks: 这是Asyncio的子类,用于封装和管理并行模式下的协程。

本节中重点讨论事件,事实上,异步编程的上下文中,事件无比重要。因为事件的本质就是异步。

1. 什么是事件循环

在计算系统中,可以产生事件的实体叫做事件源,能处理事件的实体叫做事件处理者。此外,还有一些第三方实体叫做事件循环。它的作用是管理所有的事件,在整个程序运行过程中不断循环执行,追踪事件发生的顺序将它们放到队列中,当主线程空闲的时候,调用相应的事件处理者处理事件。最后,我们可以通过下面的伪代码来理解事件循环::

while(1) {
  events = getEvents();
  for (e in events)
    processEvent(e);
}

所有的事件都在 while 循环中捕捉,然后经过事件处理者处理。事件处理的部分是系统唯一活跃的部分,当一个事件处理完成,流程继续处理下一个事件。

2. 准备工作

Asyncio提供了一下方法来管理事件循环:

  • loop = get_event_loop(): 得到当前上下文的事件循环。
  • loop.call_later(time_delay, callback, argument): 延后 time_delay 秒再执行 callback 方法。
  • loop.call_soon(callback, argument): 尽可能快调用 callbackcall_soon() 函数结束,主线程回到事件循环之后就会马上调用 callback 。
  • loop.time(): 以float类型返回当前事件循环的内部时间。
  • asyncio.set_event_loop(): 为当前上下文设置事件循环。
  • asyncio.new_event_loop(): 根据此策略创建一个新的事件循环并返回。
  • loop.run_forever(): 在调用 stop() 之前将一直运行。

3. 如何做…

下面的代码中,我们将展示如何使用Asyncio库提供的事件循环创建异步模式的应用。

import asyncio
import datetime
import time

def function_1(end_time, loop):
    print("function_1 called")
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, function_2, end_time, loop)
    else:
        loop.stop()

def function_2(end_time, loop):
    print("function_2 called ")
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, function_3, end_time, loop)
    else:
        loop.stop()

def function_3(end_time, loop):
    print("function_3 called")
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, function_1, end_time, loop)
    else:
        loop.stop()

def function_4(end_time, loop):
    print("function_5 called")
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, function_4, end_time, loop)
    else:
        loop.stop()

loop = asyncio.get_event_loop()

end_loop = loop.time() + 9.0
loop.call_soon(function_1, end_loop, loop)
# loop.call_soon(function_4, end_loop, loop)
loop.run_forever()
loop.close()

运行结果如下::

python3 event.py
function_1 called
function_2 called
function_3 called
function_1 called
function_2 called
function_3 called
function_1 called
function_2 called
function_3 called

在这个例子中,我们定义了三个异步的任务,相继执行,入下图所示的顺序。

首先,我们要得到这个事件循环::

loop = asyncio.get_event_loop()

然后我们通过 call_soon 方法调用了 function_1() 函数。

end_loop = loop.time() + 9.0
loop.call_soon(function_1, end_loop, loop)

让我们来看一下 function_1() 的定义::

def function_1(end_time, loop):
    print("function_1 called")
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, function_2, end_time, loop)
    else:
        loop.stop()

这个函数通过以下参数定义了应用的异步行为:

  • end_time: 定义了 function_1() 可以运行的最长时间,并通过 call_later 方法传入到 function_2() 中作为参数
  • loop: 之前通过 get_event_loop() 方法得到的事件循环

function_1() 的任务非常简单,只是打印出函数名字。当然,里面也可以写非常复杂的操作。

print("function_1 called")

任务执行结束之后,它将会比较 loop.time() +1s和设定的运行时间,如果没有超过,使用 call_later 在1秒之后执行 function_2() 。

if (loop.time() + 1.0) < end_time:
    loop.call_later(1, function_2, end_time, loop)
else:
    loop.stop()

function_2() 和 function_3() 的作用类似。

如果运行的时间超过了设定,事件循环终止。

loop.run_forever()
loop.close()

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 简化函数调用的3种技巧

假设有一个函数,这个函数需要接收4个参数,并返回这4个参数的和:

def sum_four(a, b, c, d):
    return a + b + c + d

如果需要固定最后前三个参数,仅改变最后一个参数的值,这时候可能需要这么调用:

>>> a, b, c = 1, 2, 3

>>> sum_four(a=a, b=b, c=c, d=1)
7

>>> sum_four(a=a, b=b, c=c, d=2)
8

>>> sum_four(a=a, b=b, c=c, d=3)
9

>>> sum_four(a=a, b=b, c=c, d=4)
10

这样写实在是太丑了,如果用 Map 函数,是否能简化代码?

答案是肯定的,但是Map函数只能接受单一元素,如果你强行使用的话,它会报这样的错:

>>> list(map(sum_four, [(1, 2, 3, 4)]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sum_four() missing 3 required positional arguments: 'b', 'c', and 'd'

怎么解决?

方案1: itertools.starmap

我们可以使用 itertools 的函数 starmap 替换Map.

它与Map不同,允许接受一个元组作为传入sum_four的参数。

>>> import itertools
>>> list(itertools.starmap(sum_four, [(1, 2, 3, 4)]))
[10]

非常棒,这样的话,上述问题就可以使用starmap函数解决:

>>> import itertools

>>> ds = [1, 2, 3, 4]

>>> items = ((a, b, c, d) for d in ds)

>>> list(items)
 [(1, 2, 3, 1), (1, 2, 3, 2), (1, 2, 3, 3), (1, 2, 3, 4)]

>>> list(itertools.starmap(sum_four, items))
 [7, 8, 9, 10]

请注意 items 是一个生成器,这是为了避免 items 过大导致内存消耗量过大。平时开发的时候注意这些细节,能够使你和普通的开发者拉开差距。

方案2: functools.partial

第二种解决方案是使用 partial 函数固定前三个参数。

根据文档,partial 将“冻结”函数的参数的某些部分,从而生成简化版的函数。

因此上述问题的解决方案就是:

>>> import functools
>>> partial_sum_four = functools.partial(sum_four, a, b, c)
>>> partial_sum_four(3)
9
>>> # 这样就可以使用map函数了:
>>> list(map(partial_sum_four, ds))
[7, 8, 9, 10]

方案3: itertools.repeat()

事实上,Map 函数是允许传递可迭代参数的,但是有一个有趣的特点,他会用每个可迭代对象里的项作为传入函数的不同参数。这样说可能太过于抽象了,来看看实际的例子:

>>> list(map(sum_four, [1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [1,2,3,4]))
 [7, 8, 9, 10]

明白了吧,每次都使用了不同数组中对应下标的项传入函数进行计算。

这样,我们可以使用这个特点进行优化。

itertools.repeat() 函数能够根据参数产生一个迭代器,该迭代器一次又一次返回对象。不指定times参数,它将无限期运行。

而 Map 函数会在最短的可迭代对象被迭代完后,就会自动停止运行。

结合这两个特点,上述问题的解决方案就出来了:

>>> import itertools
>>> list(map(sum_four, itertools.repeat(a), itertools.repeat(b), itertools.repeat(c), ds))
 [7, 8, 9, 10]

这招还是非常巧妙的。缺点是能读懂的人不多。不过没关系,计算机世界中某些东西知道就好,你并不一定需要去使用它。

比如本文中的这几种解决方案,日常生活工作中一般用不到,所以你不需要死记硬背,但你需要知道【有这样的问题】和【有这些解决方案】,万一遇到了相似的场景,你就可以回忆起这篇文章并快速找到解决的方法。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

利用 Python 理解设计模式之委托模式

有时候,我们想通过一个类来调用另一个类里的方法来处理请求,即这两个类对象参与处理同一个请求对象,只不过一个是委托者,一个是处理者

比如我们现在有一个名为Dog的类对象,我们希望通过它调用 voice 类产生“狗吠 ” 声,这时候就可以采用委托模式。下面就用Python来理解这个设计模式。

Python里,在委托者类对象中,需要这么设计:

1. 重写__getattr__方法,使得委托者获得处理者的属性。

2. 判断该属性是否为可调用函数,如果不是则直接返回,如果是,则用 wrapper 封装为可调用对象。

如下所示:

class Dog:
    def __init__(self, voice):
        self.voice = voice

    def __getattr__(self, name):
        """
        重写__getattr__方法
        获得相应的属性

        Arguments:
            name {str} -- 目标变量/函数名
        
        Returns:
            attr -- 处理者的变量/函数
        """

        attr = getattr(self.voice, name)

        if not callable(attr):
            return attr

        def wrapper(*args, **kwargs):
            return attr(*args, **kwargs)
        return wrapper 

这样做的好处是,处理者(被委托者)不需要做太多的更改,一般是一个公用类。我们的处理者如下:

class voice:
    def __init__(self):
        self.p1 = 'test'

    def words(self, something):
        print("voice: %s" % something)
        return "voice: %s" % something

这样就可以通过委托者来调用另一个类的方法来对请求进行处理:

if __name__ == '__main__':
    John = Dog(voice())
    John.words('汪汪')

实际上,如果你不重写__getattr__,一样可以用以下的方式调用到voice类:

if __name__ == '__main__':
    John = Dog(voice())
    John.voice.words('汪汪') 

这两种有什么区别呢?使用委托模式,可以简化代码,优化可读性,你不需要再调用voice对象, 委托者自己会利用 __getattr__ 找到相应的对象里的方法。

不过,在Python里,委托模式这样的写法其实是 un-pythonic 的,因为它将调用的方法隐藏在了执行者中, 可读性比较差。如果不是特殊需要,一般不会这么做,这里只是给大家展示如何用Python来理解这个设计模式。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典