前言
使用 Python 的人都知道, Python 世界有 gevent 这么个协程库, 既优雅 (指: 接口比较不错), 性能又不错, 在对付 IO bound 的程序时, 不失为一个比较好的解决方案
在使用 gevent 时, 有一步是 patch 标准库, 即: gevent 对标准库中一些同步阻塞调用的接口, 自己进行了重新实现, 并且让应用层对标准库的相关接口调用, 全部重定向 gevent 的实现, 以达到全异步的效果 这一步比较有意思, 让人不禁对其实现感到好奇, 因为这种 patch 完全是在后台默默进行的, 应用层根本不知道如果我们想实现看某个接口不惯, 自己想替换它, 但是又不想应用层代码感知到 的效果, 完全可以借鉴 gevent 的做法
先是 Google 了一番, 没有搜到满意的结果, 看来还得自己亲自看代码这篇文章即是记录了对应的探索历程
我们的简单猜想推测
gevent 有个接口的签名如下:
- def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=True, ssl=True, httplib=False,
- subprocess=True, sys=False, aggressive=True, Event=False,
- builtins=True, signal=True):
可见 gevent 做了相当多的事情但是标准库代码很庞大, gevent 必然只会替换其中部分接口, 其余的接口仍然是使用标准库所以当应用层 import socket 时, 有些接口使用的是标准库的实现, 有些则是使用 gevent 的实现
按照这种推测, 理论上可以对所有看不惯的库动手脚, 不管是标准库, 还是第三方库
源码剖析
我们由入口进, 首先便看到如下代码 (为了便于观看, 去掉了注释和一些边缘逻辑代码):
- def patch_all(socket = True, dns = True, time = True, select = True, thread = True, os = True, ssl = True, httplib = False, subprocess = True, sys = False, aggressive = True, Event = False, builtins = True, signal = True) : #Check to see
- if they 're changing the patched list
- _warnings, first_time = _check_repatching(**locals())
- if not _warnings and not first_time:
- # Nothing to do, identical args to what we just
- # did
- return
- # 显然, 主逻辑在这里
- # 无非是对每个模块实现对应的 patch 函数, 因此, 我们只需要看一个就够了
- # order is important
- if os:
- patch_os()
- if time:
- patch_time()
- if thread:
- patch_thread(Event=Event)
- # sys must be patched after thread. in other cases threading._shutdown will be
- # initiated to _MainThread with real thread ident
- if sys:
- patch_sys()
- if socket:
- patch_socket(dns=dns, aggressive=aggressive)
- if select:
- patch_select(aggressive=aggressive)
- if ssl:
- patch_ssl()
- if httplib:
- raise ValueError('gevent.httplib is no longer provided,
- httplib must be False ')
- if subprocess:
- patch_subprocess()
- if builtins:
- patch_builtins()
- if signal:
- if not os:
- _queue_warning('Patching signal but not os will result in SIGCHLD handlers'
- 'installed after this not being called and os.waitpid may not'
- '
- function correctly
- if gevent.subprocess is used.This may raise an ''error in the future.',
- _warnings)
- patch_signal()
- _process_warnings(_warnings)'
patch_os 的逻辑如下:
- def patch_os():
- patch_module('os') # 看来这个接口才是真正干活的
patch_module 的逻辑如下:
- def patch_module(name, items=None):
- # name 应该是模块名, items 应该是需要替换的接口 (命名为 interface_names 更合适 :) )
- # 先 __import__ , 然后马上取到对应的 module object
- gevent_module = getattr(__import__('gevent.' + name), name)
- # 取到模块名
- module_name = getattr(gevent_module, '__target__', name)
- # 根据模块名, 加载标准库, 比如, 如果 module_name == 'os', 那么 os 标准库便被加载了
- module = __import__(module_name)
- # 如果外部没有指定需要替换的接口, 那么我们自己去找
- if items is None:
- # 取到对应的接口
- # 看 gevent 对应的模块 比如 gevent.os
- # 果然有对应的变量
- # __implements__ = ['fork']
- # __extensions__ = ['tp_read', 'tp_write']
- items = getattr(gevent_module, '__implements__', None)
- if items is None:
- raise AttributeError('%r does not have __implements__' % gevent_module)
- # 真正干活的地方! 开始真正的替换
- for attr in items:
- patch_item(module, attr, getattr(gevent_module, attr))
- return module
真正干活的 patch_item :
- def patch_item(module, attr, newitem):
- # module: 目标模块
- # attr: 需要替换的接口
- # newitem: gevent 的实现
- NONE = object()
- olditem = getattr(module, attr, NONE)
- if olditem is not NONE: # 旧实现
- saved.setdefault(module.__name__, {}).setdefault(attr, olditem)
- # 替换为 gevent 的实现, 原来这么简单! 简单到不能再简单!
- setattr(module, attr, newitem)
总结
根据上面的描述, 核心代码就一行, 简单且优雅:
setattr(target_module, interface_name, gevent_impl)
这也让我们再次领略到了动态语言为框架 / 库设计者带来的便利, 即: 可以比较容易地去 hack 整个语言具体到 gevent, 我们只需要有如下知识储备, 便可比较容易地了解整个 patch 过程:
__import__ 给定一段字符串, 会根据这个字符串, 将对已经 module 加载进来
一切皆对象 在 Python 中, module 是对象, int 是对象, 一切都是对象, 而且可以动态地添加属性
setattr/getattr/hasattr 三大工具函数, 动态去操纵每一个 object
来源: https://juejin.im/post/5a92478bf265da4e8b2ffb5d