前言
使用 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
複製程式碼