Python gevent 是如何 patch 標準庫的 ?

浮生若夢的程式設計發表於2018-02-25

前言

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

相關文章