Python中可以同步和异步执行的模板函数

图片



现在,几乎每个开发人员都熟悉编程中的“异步”概念。在信息产品需求旺盛的时代,它们被迫同时处理大量请求,并且还与大量其他服务并行交互(而没有异步编程),这无处不在。需求如此之大,以至于甚至创建了一种独立的语言,该语言的主要功能(除了极简主义之外)是使用并行/并发代码(即Golang)进行了非常优化和便捷的工作。尽管事实上这篇文章与他无关,但我还是经常进行比较和参考。但是在Python中,将在本文中进行讨论-我将描述一些问题,并为其中一个问题提供解决方案。如果您对此主题感兴趣-请在cat下。






碰巧,我最喜欢用来工作,实施宠物项目甚至休息和放松的语言是Python我无休止地被它的美丽和朴素,它的明显性所吸引,在此之后,借助各种语法糖,对于描述人类想象力所能实现的几乎任何逻辑,都有巨大的机会。我什至在某个地方读到Python被称为超高级语言,因为它可以用来描述抽象,而用其他语言来描述将非常麻烦。



但有一个严重的细微差别-的Python要实现并行/并发逻辑,很难适应现代语言概念。这种语言的思想起源于80年代,与Java有着相同的时代直到某个时候并不意味着竞争性地执行任何代码。如果JavaScript最初需要并发才能在浏览器中进行非阻塞工作,而Golang是一种完全新鲜的语言,并且真正了解现代需求,那么Python之前就没有面临这样的任务。



当然,这是我个人的看法,但是在我看来,Python的异步实现很晚,因为内置了asyncio而是对Python并发代码执行的其他其他实现的反应。基本上,asyncio的构建为了支持现有的实现,并且不仅包含其自己的事件循环实现,而且还包含其他异步库的包装器,从而提供了用于编写异步代码的通用接口。由于上面列出的所有因素Python最初被创建为最简洁和易读的语言,当编写异步代码时Python成为了装饰器,生成器和函数的混杂。通过添加特殊指令asyncawait (如在JavaScript中,这很重要),这种情况已得到稍微纠正(已纠正,这要感谢用户mn),但仍然存在常见问题。



我不会列出所有这些内容,而是将重点放在我尝试解决的问题上:这是异步和同步执行的一般逻辑的描述。例如,如果我想在Golang中并行运行一个函数,那么我只需要使用go指令调用该函数



Golang中函数的并行执行
package main

import "fmt"

func function(index int) {
    fmt.Println("function", index)
}

func main() {
    for i := 0; i < 10; i++ { 
        go function(i)
    }
    fmt.Println("end")
}




话虽这么说,在Golang中,我可以同步运行此功能:



Golang中函数的串行执行
package main

import "fmt"

func function(index int) {
    fmt.Println("function", index)
}

func main() {
    for i := 0; i < 10; i++ { 
        function(i)
    }
    fmt.Println("end")
}




Python中,所有协程(异步函数)都是基于生成器的,它们之间的切换是在调用阻塞函数期间发生的,使用yield指令将控制权返回给事件循环。老实说,我不知道并发/并发在Golang中是如何工作的,但是如果我说它不能像在Python中那样工作,不会弄错。尽管Golang编译器CPython解释器的内部实现方式存在差异,并且不允许在其中比较并行/并发性,但我仍然会这样做,并且不仅要注意执行本身,还要注意语法。在Python中我不能使用一个函数并与一个运算符并行/并行运行它。为了使我的函数异步工作,我必须在声明它之前显式地异步编写它,此后它不再只是一个函数,而已是协程。而且我不能在没有其他动作的情况下将他们的调用混合在一个代码中,因为尽管声明中的相似之处,Python的函数和协程是完全不同的东西。



def func1(a, b):
    func2(a + b)
    await func3(a - b)  # ,   await     


我的主要问题是需要开发可以同步和异步运行的逻辑。一个简单的示例是我Instagram交互的,我很久以前就放弃了该,但现在又重新使用了它(这促使我寻找解决方案)。我想在其中实现不仅可以同步使用API​​,还可以异步使用API​​的能力,这不只是一种愿望-在Internet上收集数据时,您可以异步发送大量请求并更快地获得所有请求的答案,但与此同时,海量数据收集并不能一直需要。目前,该库实现了以下功能:与Instagram合作有2个类,一个用于同步工作,另一个用于异步工作。每个类具有相同的方法集,仅在第一种方法是同步的,在第二种方法是异步的。每种方法都做同样的事情-除了如何将请求发送到Internet。而且仅由于一个阻塞动作的差异,我不得不几乎完全复制每种方法中的逻辑。看起来像这样:



class WebAgent:
    def update(self, obj=None, settings=None):
        ...
        response = self.get_request(path=path, **settings)
        ...

class AsyncWebAgent:
    async def update(self, obj=None, settings=None):
        ...
        response = await self.get_request(path=path, **settings)
        ...


更新 方法更新协程中的其他所有内容都完全相同。众所周知,代码重复会带来很多问题,尤其是在修复错误和测试时。



我编写了自己的pySyncAsync库来解决此问题。想法如下-代替了普通的函数和协程,实现了一个生成器,将来我将其称为模板。为了执行模板,需要将其作为常规函数或协程生成。该模板在需要在其内部执行异步或同步代码的时刻执行时,将使用yield返回一个特殊的Call对象。,它指定要调用的内容和参数。根据模板的生成方式(作为函数还是协程),将以这种方式执行Call对象中描述的方法



我将展示一个模板的小示例,该示例假定能够向google发出请求



使用pySyncAsync的Google请求示例
import aiohttp
import requests

import pysyncasync as psa

#       google
#          Call
@psa.register("google_request")
def sync_google_request(query, start):
    response = requests.get(
        url="https://google.com/search",
        params={"q": query, "start": start},
    )
    return response.status_code, dict(response.headers), response.text


#       google
#          Call
@psa.register("google_request")
async def async_google_request(query, start):
    params = {"q": query, "start": start}
    async with aiohttps.ClientSession() as session:
        async with session.get(url="https://google.com/search", params=params) as response:
            return response.status, dict(response.headers), await response.text()


#     100 
def google_search(query):
    start = 0
    while start < 100:
        #  Call     ,        google_request
        call = Call("google_request", query, start=start)
        yield call
        status, headers, text = call.result
        print(status)
        start += 10


if __name__ == "__main__":
    #   
    sync_google_search = psa.generate(google_search, psa.SYNC)
    sync_google_search("Python sync")

    #   
    async_google_search = psa.generate(google_search, psa.ASYNC)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(async_google_search("Python async"))




我会告诉您一些有关库的内部结构的信息。有一个Manager,其中注册了要使用Call调用的函数和协程。也可以注册模板,但这是可选的。Manager具有方法registergeneratetemplate。上面示例中的相同方法是直接从pysyncasync调用的,只是它们使用了Manager类的全局实例,该实例已在一个库模块中创建。实际上,您可以创建自己的实例并从中调用registergeneratetemplate方法。因此,例如在可能发生名称冲突的情况下,将经理彼此隔离。register



方法充当装饰器,并允许您注册函数或协程以从模板中进一步调用。寄存器装饰器接受在管理器中注册函数或协程的名称作为参数。如果未指定名称,则该函数或协程将以其自己的名称注册。模板 方法允许您在管理器中将生成器注册为模板。为了能够按名称获取模板,这是必需的。产生 方法







允许您基于模板生成函数或协程。它有两个参数:第一个是模板的名称或模板本身,第二个是“同步”或“异步”(如何生成模板)到函数或协程。在输出处,generate方法给出一个现成的函数或协程。



我将举一个在协程中生成模板的示例:



def _async_generate(self, template):
    async def wrapper(*args, **kwargs):
        ...
        for call in template(*args, **kwargs):
            callback = self._callbacks.get(f"{call.name}:{ASYNC}")
            call.result = await callback(*call.args, **call.kwargs)
        ...
    return wrapper


在内部,生成一个协程,该协程仅在生成器上进行迭代并接收Call类的对象,然后按名称获取先前注册的协程(该名称取自call),并使用参数对其进行调用(它也取自call),执行此协程的结果也存储在call中Call



类的对象只是用于存储有关调用方式和方式的信息的容器,还可以使您将结果存储在自身中。包装器还可以返回模板执行的结果;为此,模板被包装在特殊的Generator类中,此处未显示。



我已经省略了一些细微差别,但我希望我已大致传达了实质。



老实说,这篇文章是我写的,而不是分享我在Python中使用异步代码解决问题的想法最重要的是,听听哈布拉夫居民的意见。也许我会让某人寻求另一种解决方案,也许某人会不同意此特定的实现,并告诉您如何使其变得更好,也许某人会告诉您为什么根本不需要这种解决方案,并且您不应该将同步和异步代码,你们每个人的意见对我都很重要。另外,在本文开始时,我并不假装我的所有推理都正确。我对其他YP的主题进行了广泛的思考,可能会弄错,另外,如果您突然发现任何不一致之处,请把这些概念弄混,请在评论中进行描述。如果对语法和标点进行了修改,我也将感到高兴。



并感谢您对这个问题的关注,尤其是对本文的关注!



All Articles