Python中的魔法方法深入理解

來源:互聯網
上載者:User
接觸Python也有一段時間了,Python相關的架構和模組也接觸了不少,希望把自己接觸到的自己 覺得比較好的設計和實現分享給大家,於是取了一個“Charming Python”的小標,算是給自己開了一個頭吧, 希望大家多多批評指正。 :)

from flask import request

Flask 是一個人氣非常高的Python Web架構,筆者也拿它寫過一些大大小小的項目,Flask 有一個特性我非常的喜歡,就是無論在什麼地方,如果你想要擷取當前的request對象,只要 簡單的:
複製代碼 代碼如下:


from flask import request

# 從當前request擷取內容
request.args
request.forms
request.cookies
... ...


非常簡單好記,用起來也非常的友好。不過,簡單的背後藏的實現可就稍微有一些複雜了。 跟隨我的文章來看看其中的奧秘吧!

兩個疑問?

在我們往下看之前,我們先提出兩個疑問:

疑問一 : request ,看上去只像是一個靜態類執行個體,我們為什麼可以直接使用request.args 這樣的運算式來擷取當前request的args屬性,而不用使用比如:
複製代碼 代碼如下:


from flask import get_request

# 擷取當前request
request = get_request()
get_request().args


這樣的方式呢?flask是怎麼把request對應到當前的請求對象的呢?

疑問二 : 在真正的生產環境中,同一個背景工作處理序下面可能有很多個線程(又或者是協程), 就像我剛剛所說的,request這個類執行個體是怎麼在這樣的環境下正常工作的呢?

要知道其中的秘密,我們只能從flask的源碼開始看了。

源碼,源碼,還是源碼

首先我們開啟flask的源碼,從最開始的__init__.py來看看request是怎麼出來的:
複製代碼 代碼如下:


# File: flask/__init__.py
from .globals import current_app, g, request, session, _request_ctx_stack


# File: flask/globals.py
from functools import partial
from werkzeug.local import LocalStack, LocalProxy


def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError('working outside of request context')
return getattr(top, name)

# context locals
_request_ctx_stack = LocalStack()
request = LocalProxy(partial(_lookup_req_object, 'request'))

我們可以看到flask的request是從globals.py引入的,而這裡的定義request的代碼為 request = LocalProxy(partial(_lookup_req_object, 'request')) , 如果有不瞭解 partial是什麼東西的同學需要先補下課,首先需要瞭解一下 partial 。

不過我們可以簡單的理解為 partial(func, 'request') 就是使用 'request' 作為func的第一個預設參數來產生另外一個function。

所以, partial(_lookup_req_object, 'request') 我們可以理解為:

產生一個callable的function,這個function主要是從 _request_ctx_stack 這個LocalStack對象擷取堆棧頂部的第一個RequestContext對象,然後返回這個對象的request屬性。

這個werkzeug下的LocalProxy引起了我們的注意,讓我們來看看它是什麼吧:

複製代碼 代碼如下:


@implements_bool
class LocalProxy(object):
"""Acts as a proxy for a werkzeug local. Forwards all operations to
a proxied object. The only operations not supported for forwarding
are right handed operands and any kind of assignment.
... ...

看前幾句介紹就能知道它主要是做什麼的了,顧名思義,LocalProxy主要是就一個Proxy, 一個為werkzeug的Local物件服務的代理。他把所以作用到自己的操作全部“轉寄”到 它所代理的對象上去。

那麼,這個Proxy通過Python是怎麼實現的呢?答案就在源碼裡:
複製代碼 代碼如下:


# 為了方便說明,我對代碼進行了一些刪減和改動

@implements_bool
class LocalProxy(object):
__slots__ = ('__local', '__dict__', '__name__')

def __init__(self, local, name=None):
# 這裡有一個點需要注意一下,通過了__setattr__方法,self的
# "_LocalProxy__local" 屬性被設定成了local,你可能會好奇
# 這個屬性名稱為什麼這麼奇怪,其實這是因為Python不支援真正的
# Private member,具體可以參見官方文檔:
# http://docs.python.org/2/tutorial/classes.html#private-variables-and-class-local-references
# 在這裡你只要把它當做 self.__local = local 就可以了 :)
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)

def _get_current_object(self):
"""
擷取當前被代理的真正對象,一般情況下不會主動調用這個方法,除非你因為
某些效能原因需要擷取做這個被代理的真正對象,或者你需要把它用來另外的
地方。
"""
# 這裡主要是判斷代理的對象是不是一個werkzeug的Local對象,在我們分析request
# 的過程中,不會用到這塊邏輯。
if not hasattr(self.__local, '__release_local__'):
# 從LocalProxy(partial(_lookup_req_object, 'request'))看來
# 通過調用self.__local()方法,我們得到了 partial(_lookup_req_object, 'request')()
# 也就是 ``_request_ctx_stack.top.request``
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError('no object bound to %s' % self.__name__)

# 接下來就是一大段一段的Python的魔法方法了,Local Proxy重載了(幾乎)?所有Python
# 內建魔法方法,讓所有的關於他自己的operations都指向到了_get_current_object()
# 所返回的對象,也就是真正的被代理對象。

... ...
__setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
__delattr__ = lambda x, n: delattr(x._get_current_object(), n)
__str__ = lambda x: str(x._get_current_object())
__lt__ = lambda x, o: x._get_current_object() < o
__le__ = lambda x, o: x._get_current_object() <= o
__eq__ = lambda x, o: x._get_current_object() == o
__ne__ = lambda x, o: x._get_current_object() != o
__gt__ = lambda x, o: x._get_current_object() > o
__ge__ = lambda x, o: x._get_current_object() >= o
... ...

事情到了這裡,我們在文章開頭的第二個疑問就能夠得到解答了,我們之所以不需要使用get_request() 這樣的方法調用來擷取當前的request對象,都是LocalProxy的功勞。

LocalProxy作為一個代理,通過自訂魔法方法。代理了我們對於request的所有操作, 使之指向到真正的request對象。

怎麼樣,現在知道了 request.args 不是它看上去那麼簡簡單單的吧。

現在,讓我們來看看第二個問題,在多線程的環境下,request是怎麼正常工作的呢? 還是讓我們回到globals.py吧:
複製代碼 代碼如下:


from functools import partial
from werkzeug.local import LocalStack, LocalProxy


def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError('working outside of request context')
return getattr(top, name)

# context locals
_request_ctx_stack = LocalStack()
request = LocalProxy(partial(_lookup_req_object, 'request'))

問題的關鍵就在於這個 _request_ctx_stack 對象了,讓我們找到LocalStack的源碼:

複製代碼 代碼如下:


class LocalStack(object):

def __init__(self):
# 其實LocalStack主要還是用到了另外一個Local類
# 它的一些關鍵的方法也被代理到了這個Local類上
# 相對於Local類來說,它多實現了一些和堆棧“Stack”相關方法,比如push、pop之類
# 所以,我們只要直接看Local代碼就可以
self._local = Local()

... ...

@property
def top(self):
"""
返回堆棧頂部的對象
"""
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None


# 所以,當我們調用_request_ctx_stack.top時,其實是調用了 _request_ctx_stack._local.stack[-1]
# 讓我們來看看Local類是怎麼實現的吧,不過在這之前我們得先看一下下面出現的get_ident方法

# 首先嘗試著從greenlet匯入getcurrent方法,這是因為如果flask跑在了像gevent這種容器下的時候
# 所以的請求都是以greenlet作為最小單位,而不是thread線程。
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident

# 總之,這個get_ident方法將會返回當前的協程/線程ID,這對於每一個請求都是唯一的


class Local(object):
__slots__ = ('__storage__', '__ident_func__')

def __init__(self):
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)

... ...

# 問題的關鍵就在於Local類重載了__getattr__和__setattr__這兩個魔法方法

def __getattr__(self, name):
try:
# 在這裡我們返回調用了self.__ident_func__(),也就是當前的唯一ID
# 來作為__storage__的key
return self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)

def __setattr__(self, name, value):
ident = self.__ident_func__()
storage = self.__storage__
try:
storage[ident][name] = value
except KeyError:
storage[ident] = {name: value}

... ...

# 重載了這兩個魔法方法之後

# Local().some_value 不再是它看上去那麼簡單了:
# 首先我們先調用get_ident方法來擷取當前啟動並執行線程/協程ID
# 然後擷取這個ID空間下的some_value屬性,就像這樣:
#
# Local().some_value -> Local()[current_thread_id()].some_value
#
# 設定屬性的時候也是這個道理

通過這些分析,相信疑問二也得到瞭解決,通過使用了當前的線程/協程ID,加上重載一些魔法 方法,Flask實現了讓不同背景工作執行緒都使用了自己的那一份stack對象。這樣保證了request的正常 工作。

說到這裡,這篇文章也差不多了。我們可以看到,為了使用者的方便,作為架構和工具的開發人員 需要付出很多額外的工作,有時候,使用一些語言上的魔法是無法避免的,Python在這方面也有著 相當不錯的支援。

我們所需要做到的就是,學習掌握好Python中那些魔法的部分,使用魔法來讓自己的代碼更簡潔, 使用更方便。

但是要記住,魔法雖然炫,千萬不要濫用哦。

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.