首頁 後端開發 Python教學 Python中的魔法方法深入理解

Python中的魔法方法深入理解

Jun 16, 2016 am 08:43 AM
python 魔法方法

接触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()     __le__ = lambda x, o: x._get_current_object()     __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中那些魔法的部分,使用魔法来让自己的代码更简洁, 使用更方便。

但是要记住,魔法虽然炫,千万不要滥用哦。

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1423
52
Laravel 教程
1321
25
PHP教程
1269
29
C# 教程
1249
24
PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

在PHP和Python之間進行選擇:指南 在PHP和Python之間進行選擇:指南 Apr 18, 2025 am 12:24 AM

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

sublime怎麼運行代碼python sublime怎麼運行代碼python Apr 16, 2025 am 08:48 AM

在 Sublime Text 中運行 Python 代碼,需先安裝 Python 插件,再創建 .py 文件並編寫代碼,最後按 Ctrl B 運行代碼,輸出會在控制台中顯示。

PHP和Python:深入了解他們的歷史 PHP和Python:深入了解他們的歷史 Apr 18, 2025 am 12:25 AM

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

Python vs. JavaScript:學習曲線和易用性 Python vs. JavaScript:學習曲線和易用性 Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

Golang vs. Python:性能和可伸縮性 Golang vs. Python:性能和可伸縮性 Apr 19, 2025 am 12:18 AM

Golang在性能和可擴展性方面優於Python。 1)Golang的編譯型特性和高效並發模型使其在高並發場景下表現出色。 2)Python作為解釋型語言,執行速度較慢,但通過工具如Cython可優化性能。

vscode在哪寫代碼 vscode在哪寫代碼 Apr 15, 2025 pm 09:54 PM

在 Visual Studio Code(VSCode)中編寫代碼簡單易行,只需安裝 VSCode、創建項目、選擇語言、創建文件、編寫代碼、保存並運行即可。 VSCode 的優點包括跨平台、免費開源、強大功能、擴展豐富,以及輕量快速。

notepad 怎麼運行python notepad 怎麼運行python Apr 16, 2025 pm 07:33 PM

在 Notepad 中運行 Python 代碼需要安裝 Python 可執行文件和 NppExec 插件。安裝 Python 並為其添加 PATH 後,在 NppExec 插件中配置命令為“python”、參數為“{CURRENT_DIRECTORY}{FILE_NAME}”,即可在 Notepad 中通過快捷鍵“F6”運行 Python 代碼。

See all articles