Python gevent 是如何 patch 標準庫的 ?

前言

使用 Python 的人都知道,Python 世界有 gevent 這麼個協程庫,既優雅(指:接口比較不錯),性能又不錯,在對付 IO bound 的程序時,不失爲一個比較好的解決方案。bash

在使用 gevent 時,有一步是 patch 標準庫,即:gevent 對標準庫中一些同步阻塞調用的接口,本身進行了從新實現,而且讓應用層對標準庫的相關接口調用,所有重定向 gevent 的實現,以達到全異步的效果。 這一步比較有意思,讓人不由對其實現感到好奇,由於這種 patch 徹底是在後臺默默進行的,應用層根本不知道。若是咱們想實現看某個接口不慣,本身想替換它,可是又不想應用層代碼感知到 的效果,徹底能夠借鑑 gevent 的作法。框架

先是 Google 了一番,沒有搜到滿意的結果,看來還得本身親自看代碼。這篇文章便是記錄了對應的探索歷程。異步

咱們的簡單猜測推測

gevent 有個接口的簽名以下:socket

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 的實現。ide

按照這種推測,理論上能夠對全部看不慣的庫動手腳,無論是標準庫,仍是第三方庫。函數

源碼剖析

咱們由入口進,首先便看到以下代碼(爲了便於觀看,去掉了註釋和一些邊緣邏輯代碼):工具

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 的邏輯以下:ui

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 :this

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
複製代碼
相關文章
相關標籤/搜索