descriptor

张开发
2026/6/9 15:24:41 15 分钟阅读
descriptor
在 Python 的世界里coroutine 这个词听起来有点学术但其实它离日常开发并不遥远。很多开发者第一次接触这个概念时可能会把它和线程、进程这些并发概念混在一起但实际上 coroutine 走的是一条不太一样的路。简单来说coroutine 是一种可以暂停和恢复执行的函数。这听起来可能有点抽象可以想象一下读书的场景。你正在读一本小说突然有人敲门你顺手把书签夹在当前页去开门处理事情回来之后翻开书签那一页继续往下读。coroutine 就有点像这个“带书签的阅读过程”——它可以在某个点暂停保存当前的状态等到合适的时机再回来接着执行而不用从头开始。在 Python 里coroutine 的实现经历了一些演变。早期有基于生成器的 coroutine使用yield关键字来实现暂停和传值后来随着asyncio库的引入async和await语法让 coroutine 写起来更直观。现在提到 coroutine大多数时候指的是用async def定义的异步函数。和线程相比coroutine 的轻量是一个显著特点。启动一个线程需要分配一定的内存和内核资源而 coroutine 本质上是在单个线程内通过调度来实现并发切换成本低很多。这就像在一个厨房里只有一个厨师但他可以在煮汤的时候去切菜而不是请好几个厨师同时挤在厨房里——人多了反而可能互相妨碍。但 coroutine 并不是万能的。它最适合 I/O 密集型的场景比如网络请求、文件读写这些操作往往需要等待外部响应利用等待的时间去执行其他 coroutine 可以显著提高效率。如果是计算密集型的任务coroutine 可能并不会带来性能提升因为 CPU 一直在忙着计算没有空闲时间去切换。在实际使用中coroutine 常常和事件循环配合工作。事件循环就像一个调度中心负责在合适的时机唤醒该执行的 coroutine。asyncio库提供了这套机制让开发者不用太关心底层的调度细节可以更专注于业务逻辑。写 coroutine 风格的代码思维需要一点转变。传统的同步代码是一步一步往下执行而 coroutine 允许你在等待的时候去做别的事情这需要更仔细地考虑哪些操作是可以并发的哪些又有依赖关系。有时候一个地方忘了写await就可能让整个并发逻辑失效这种错误调试起来不太直观。在大型项目中coroutine 可以很好地组织高并发的逻辑比如同时处理成千上万个网络# 在Python的世界里descriptor这个概念乍一听可能会让人觉得有点抽象甚至有点“高级”得不太常用。但如果你仔细翻看一些经典库的源码或者想真正理解属性访问背后的魔法它几乎无处不在。它不是那种每天都要手动去写的东西但理解了它就像弄清楚了家里总电闸的工作原理虽然平时不碰但哪天电路出了问题你心里是有底的。简单来说descriptor描述符是一个实现了特定协议的对象。这个协议就是定义了__get__、__set__或__delete__这些特殊方法的类。当一个类的属性被定义为一个描述符实例时对这个属性的访问获取、设置、删除就不再是直接操作那个实例变量而是会转而去调用描述符对象对应的那些特殊方法。这听起来可能还是有点绕。举个更生活化的例子。想象一下你家里的智能电表。你没法直接看到或修改用了多少度电你只能通过电表上的屏幕或者手机APP来读取读数也只能通过供电局的系统来充值或设置。这里的“电表读数”这个属性就是一个被描述符管理的属性。你每次去看__get__背后是电表在计算和返回当前值你每次充值__set__背后是电表在验证金额并更新余额。电表本身就是这个描述符。在代码里最常见的例子可能就是property。当我们用property装饰一个方法时Python就在背后为我们创建了一个描述符。这样你可以用obj.x这样的属性语法去访问但实际上执行的是一个方法从而可以加入计算、验证等逻辑。property是描述符最友好、最直接的一个应用。但描述符的威力不止于此。比如你想给一个类的多个属性都加上类型检查。如果没有描述符你可能会在每个属性的setter里写重复的检查代码。用描述符你可以创建一个TypedAttribute描述符类在它的__set__方法里做类型检查。然后在目标类里你只需要像name TypedAttribute(str)这样声明属性就行了。所有类型检查的逻辑都集中在了描述符类里干净又利落。Django的ORM字段定义、SQLAlchemy的列定义底层都是这个思路——用一个描述符对象来代表数据库里的一个字段当你操作模型实例的属性时描述符在背后帮你处理与数据库的交互。还有一类是所谓的“非数据描述符”它只实现了__get__方法。这有什么用呢方法类中定义的函数其实就是非数据描述符。当你调用obj.method()时obj.method这个访问动作触发了函数对象的__get__方法它返回了一个绑定了实例obj的“绑定方法”。这就是为什么方法能自动知道是哪个实例在调用它的原因之一。理解描述符还有一个很关键的细节就是属性访问的查找顺序。当你在一个实例上访问一个属性时Python解释器会按照一个固定的链条去寻找它先看这个对象的实例字典__dict__里有没有。如果没有它会去看这个对象的类以及类的父类。在查看类的过程中如果发现某个属性是一个数据描述符即实现了__set__或__delete__的描述符那么无论实例字典里有没有同名属性都会优先调用这个数据描述符。这个规则赋予了数据描述符很高的优先级让它能够有效地拦截和控制属性的访问。而非数据描述符的优先级则低于实例字典这也就是为什么你可以给实例动态赋值来覆盖掉一个类方法的原因。所以下次当你优雅地使用一个property或者看到某个库中那些声明起来很简洁但功能强大的类成员时可以想到这背后很可能站着一个默默工作的描述符。它把复杂的逻辑封装起来提供了一个干净、直观的接口。它不是Python日常编程的必需品但它是构建清晰、强大且符合直觉的API的重要基石之一。理解它能让你从“使用魔法”的人变成“创造魔法”的人。连接。但也要注意错误处理、超时控制这些在异步环境下变得更复杂需要借助asyncio提供的工具比如asyncio.gather、asyncio.wait等来管理多个 coroutine 的执行。总的来说coroutine 是 Python 处理并发的一种重要工具它通过协作式的多任务在适当的场景下能带来很好的性能和资源利用率。理解它的本质和适用场景才能更好地用它来构建高效的应用。

更多文章