Python 函數(shù)式編程、裝飾器以及一些相關(guān)概念簡介

Python 中的 Decorator(裝飾器) 是對一個函數(shù)或者方法的封裝飒炎,從而使其可以完成一些與自身功能無關(guān)的工作。

預(yù)備知識

一切皆對象

在 Python 中笆豁,所有的一切都被視為對象郎汪,任何的變量、函數(shù)闯狱、類等都是 object 的子類煞赢。因此除了變量之外,函數(shù)和類等也可以被指向和傳遞哄孤。

>>> def foo():
...     pass
...
>>> def Foo():
...     pass
...
>>> v = foo
>>> v
<function foo at 0x7f457ecb2b18>
>>> v = Foo
>>> v
<function Foo at 0x7f457ef96848>
歡迎加入我的QQ群`923414804`與我一起學(xué)習(xí)照筑,群里有我學(xué)習(xí)過程中整理的大量學(xué)習(xí)資料。加群即可免費獲取

命名空間

Python 通過提供 namespace 來實現(xiàn)重名函數(shù)/方法瘦陈、變量等信息的識別凝危。其大致可以分為三種 namespace,分別為:

  • local namespace: 局部空間双饥,作用范圍為當前函數(shù)或者類方法
  • global namespace: 全局空間媒抠,作用范圍為當前模塊
  • build-in namespace: 內(nèi)建空間,在 Python 解釋器啟動時就已經(jīng)具有的命名空間咏花,作用范圍為所有模塊趴生。例如:abs,all昏翰,chr苍匆,cmp,int棚菊,str 等內(nèi)建函數(shù)浸踩,它們在解釋器啟動時就被自動載入。

當函數(shù)/方法统求、變量等信息發(fā)生重名時检碗,Python 會按照 local namespace -> global namespace -> build-in namespace 的順序搜索用戶所需元素,并且以第一個找到此元素的 namespace 為準码邻。

>>> # 重寫內(nèi)建名字空間中的 str 函數(shù)
>>> def str(obj):
...     print "This is str"
...
>>> str(1)
This is str

閉包

閉包(Closure)是詞法閉包(Lexical Closure)的簡稱折剃。簡單地說,閉包就是根據(jù)不同的配置信息得到不同的結(jié)果像屋。對閉包的具體定義有很多種說法怕犁,大致可以分為兩類:

  • 一種說法認為閉包是符合一定條件的函數(shù),比如一些參考資源中這樣定義閉包:閉包是在其詞法上下文中引用了自由變量的函數(shù)。
  • 另一種說法認為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實體奏甫。比如一些參考資源中這樣來定義:在實現(xiàn)深約束時戈轿,需要創(chuàng)建一個能顯式表示引用環(huán)境的東西,并將它與相關(guān)的子程序捆綁在一起阵子,這樣捆綁起來的整體被稱為閉包思杯。

以上兩種定義從某種意義上來說是對立的,一個認為閉包是函數(shù)挠进,另一個認為閉包是函數(shù)和引用環(huán)境組成的整體智蝠。閉包確實可以認為就是函數(shù),但第二種說法更確切些奈梳。閉包只是在形式和表現(xiàn)上像函數(shù),但實際上不是函數(shù)解虱。函數(shù)是一些可執(zhí)行的代碼攘须,這些代碼在函數(shù)被定義后就確定了,不會在執(zhí)行時發(fā)生變化殴泰,所以一個函數(shù)只有一個實例于宙。

閉包在運行時可以有多個實例,不同的引用環(huán)境和相同的函數(shù)組合可以產(chǎn)生不同的實例悍汛。所謂引用環(huán)境是指在程序執(zhí)行中的某個點所有處于活躍狀態(tài)的約束所組成的集合捞魁。其中的約束是指一個變量的名字和其所代表的對象之間的聯(lián)系。那么為什么要把引用環(huán)境與函數(shù)組合起來呢离咐?這主要是因為在支持嵌套作用域的語言中谱俭,有時不能簡單直接地確定函數(shù)的引用環(huán)境。

在 Python 語言中宵蛀,可以這樣簡單的理解閉包:一個閉包就是調(diào)用了一個函數(shù) A昆著,這個函數(shù) A 返回了一個函數(shù) B。這個返回的函數(shù) B 就叫做閉包术陶。在調(diào)用函數(shù) A 的時候傳遞的參數(shù)就是對不同引用環(huán)境所做的配置凑懂。如下示例所示:

>>> def make_adder(addend):
...     def adder(augend):
...         return augend + addend
...     return adder
...
>>> add1 = make_adder(11)
>>> add2 = make_adder(22)
>>> add1(100)
111
>>> add2(100)
122

函數(shù)式編程

函數(shù)式編程指使用一系列的函數(shù)解決問題。函數(shù)僅接受輸入并產(chǎn)生輸出梧宫,不包含任何能影響產(chǎn)生輸出的內(nèi)部狀態(tài)接谨。任何情況下,使用相同的參數(shù)調(diào)用函數(shù)始終能產(chǎn)生同樣的結(jié)果塘匣。

函數(shù)式編程就是一種抽象程度很高的編程范式脓豪,純粹的函數(shù)式編程語言編寫的函數(shù)沒有變量,因此馆铁,任意一個函數(shù)跑揉,只要輸入是確定的,輸出就是確定的,這種純函數(shù)我們稱之為沒有副作用历谍。而允許使用變量的程序設(shè)計語言现拒,由于函數(shù)內(nèi)部的變量狀態(tài)不確定,同樣的輸入望侈,可能得到不同的輸出印蔬,因此,這種函數(shù)是有副作用的脱衙。函數(shù)式編程的一個特點就是侥猬,允許把函數(shù)本身作為參數(shù)傳入另一個函數(shù),還允許返回一個函數(shù)捐韩!

可以認為函數(shù)式編程剛好站在了面向?qū)ο缶幊痰膶α⒚嫱诉搿ο笸ǔ0瑑?nèi)部狀態(tài)(字段),和許多能修改這些狀態(tài)的函數(shù)荤胁,程序則由不斷修改狀態(tài)構(gòu)成瞧预;函數(shù)式編程則極力避免狀態(tài)改動,并通過在函數(shù)間傳遞數(shù)據(jù)流進行工作仅政。但這并不是說無法同時使用函數(shù)式編程和面向?qū)ο缶幊坦赣停聦嵣希瑥?fù)雜的系統(tǒng)一般會采用面向?qū)ο蠹夹g(shù)建模圆丹,但混合使用函數(shù)式風(fēng)格也能體現(xiàn)函數(shù)式風(fēng)格的優(yōu)點滩愁。

高階函數(shù)

高階函數(shù)即能接受函數(shù)作為參數(shù)的函數(shù)。因為在 Python 中一切皆對象辫封,變量可以指向函數(shù)硝枉,函數(shù)名其實也是指向函數(shù)的變量。也就是說倦微,我們可以將函數(shù)賦給其他變量檀咙,也就可以將函數(shù)作為參數(shù)傳遞給其他函數(shù)。

>>> def add(x, y, f):
...     return f(x) + f(y)
...
>>> add(6, -9, abs)
15

裝飾器(decorator)

Python 裝飾器的作用就是為已經(jīng)存在的對象添加額外的功能璃诀。例如裝飾器可以用來 引入日志弧可、添加計時邏輯來檢測性能給函數(shù)加入事務(wù)處理 等等劣欢。其實總體說起來棕诵,裝飾器也就是一個函數(shù),一個用來包裝函數(shù)的函數(shù)凿将。裝飾器在函數(shù)申明完成的時候被調(diào)用校套,調(diào)用之后申明的函數(shù)被換成一個被裝飾器裝飾過后的函數(shù)。簡單說牧抵,本質(zhì)上笛匙,裝飾器就是一個返回函數(shù)的高階函數(shù)侨把,也是一個閉包。

裝飾器的語法以 @ 開頭妹孙,接著是裝飾器要裝飾的函數(shù)的申明秋柄。

無參裝飾器

先來看一下裝飾器本身沒有參數(shù)的情況。例如蠢正,我們想要知道一個函數(shù)被調(diào)用時所花的時間骇笔,可以采用如下的方式實現(xiàn):

# Author: Huoty
# Time: 2015-08-12 10:37:10

import time

def foo():
    print 'in foo()'

# 定義一個計時器,傳入一個函數(shù)嚣崭,并返回另一個附加了計時功能的方法
def timeit(func):

    # 定義一個內(nèi)嵌的包裝函數(shù)笨触,給傳入的函數(shù)加上計時功能的包裝
    def wrapper():
        start = time.clock()
        func()
        end = time.clock()
        print 'used:', end - start

    # 將包裝后的函數(shù)返回
    return wrapper

# Script starts from here

foo = timeit(foo)
foo()

Python 實現(xiàn)裝飾器的目的就是為了讓程序更加簡潔,上邊的代碼可以繼續(xù)用裝飾器來簡化:

# Author:  Huoty
# Time: 2015-08-12 10:37:10

import time

def timeit(func):
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
    return wrapper

@timeit
def foo():
    print 'in foo()'

# Script starts from here

foo()

由上例可以看出雹舀,裝飾器 @timeit 的作用等價與 foo = timeit(foo)芦劣。被裝飾器裝飾后的執(zhí)行結(jié)果取決于裝飾函數(shù)的是想,如果裝飾函數(shù)返回被裝飾函數(shù)本身说榆,就等于沒有裝飾持寄,如果裝飾函數(shù)對被裝飾函數(shù)進行了包裝,并返回包裝后的函數(shù)娱俺,那調(diào)用函數(shù)時執(zhí)行的就是包裝過的函數(shù)。

如果被裝飾的函數(shù)帶有參數(shù)废麻,則在裝飾器中也應(yīng)該為包裝函數(shù)提供對應(yīng)的參數(shù)荠卷。如果被裝飾的函數(shù)參數(shù)不確定,則可以用如下方式實現(xiàn):

# Author:  Huoty
# Time: 2015-08-12 10:59:11

import time

def log(func):
    def wrapper(*args, **kw):
        print "call %s." % func.__name__
        return func(*args, **kw)
    return wrapper

@log
def now():
    print time.asctime()

# Script starts from here

now()

帶參數(shù)裝飾器

裝飾器本身也可以帶參數(shù)烛愧,但是通常對參數(shù)會有一定的要求油宜。由于有參數(shù)的裝飾器函數(shù)在調(diào)用時只會使用應(yīng)用時的參數(shù),而不接收被裝飾的函數(shù)做為參數(shù)怜姿,所以必須在其內(nèi)部再創(chuàng)建一個函數(shù)慎冤。如下示例所示:

# Author:  Huoty
# Time: 2015-08-12 11:13:30

def deco(arg):
    def _deco(func):
        def __deco():
            print("before %s called [%s]." % (func.__name__, arg))
            func()
            print("  after %s called [%s]." % (func.__name__, arg))
        return __deco
    return _deco

@deco("module")
def foo():
    print(" foo() called.")

@deco("module2")
def hoo():
    print(" hoo() called.")

# Script starts from here

foo()
hoo()

上例中的第一個函數(shù) deco 是裝飾器函數(shù),它的參數(shù)是用來加強 加強裝飾 的沧卢。由于此函數(shù)并非被裝飾的函數(shù)對象蚁堤,所以在內(nèi)部必須至少創(chuàng)建一個接受被裝飾函數(shù)的函數(shù),然后返回這個對象(實際上此時等效于 foo=decomaker(arg)(foo))但狭。

如果裝飾器和被裝飾函數(shù)都帶參數(shù)披诗,則用如下實現(xiàn)是形式:

def deco(pm):
    def _deco(func):
        def __deco(*args, **kw):
            ret =func(*args, **kw)
            print "func result: ", ret
            return ret ** pm
        return __deco
    return _deco

@deco(2)
def foo(x, y, z=1):
    return x + y + z

print "deco_func result: %s" % foo(10, 20)

輸出:

func result:  31
deco_func result: 961

類裝飾器

裝飾器不經(jīng)可以用來裝飾函數(shù),還可以用來裝飾類立磁。例如給類添加一個類方法:

>>> def bar(obj):
...     print type(obj)
...
>>> def inject(cls):
...     cls.bar = bar
...     return cls
...
>>> @inject
... class Foo(object):
...     pass
...
>>> foo = Foo()
>>> foo.bar()
<class '__console__.Foo'>

內(nèi)置裝飾器

內(nèi)置的裝飾器有三個呈队,分別是 staticmethod、classmethod 和 property唱歧,作用分別是把類中定義的實例方法變成靜態(tài)方法宪摧、類方法和類屬性粒竖。由于模塊里可以定義函數(shù),所以靜態(tài)方法和類方法的用處并不是太多几于,除非你想要完全的面向?qū)ο缶幊倘锩纭_@三個裝飾器的實現(xiàn)都涉及到 描述符 的概念。

Functools 模塊

Python的functools模塊主要功能是對函數(shù)進行包裝孩革,增加原有函數(shù)的功能岁歉,起主要內(nèi)容包括:cmp_to_key, partial, reduce, total_ordering, update_wrapper, wraps

函數(shù)也是一個對象膝蜈,它有__name__等屬性锅移。以上我們有一個 callin.py 的例子,我們用裝飾器裝飾之后的 now 函數(shù)饱搏,當我們用 now.__name__ 查看時非剃,發(fā)現(xiàn)它的 __name__ 已經(jīng)從原來的'now'變成了'wrapper'。因為返回的那個wrapper()函數(shù)名字就是'wrapper'推沸,所以备绽,需要把原始函數(shù)的name等屬性復(fù)制到wrapper()函數(shù)中,否則鬓催,有些依賴函數(shù)簽名的代碼執(zhí)行就會出錯肺素。當然,我們可以用wrapper.__name__ = func.__name__來實現(xiàn)宇驾,但是我們不必這么麻煩倍靡,用 Python 內(nèi)置的 functools.wraps 便可實現(xiàn)這樣的功能:

import functools

def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

用類實現(xiàn)裝飾器

除了可以用函數(shù)來實現(xiàn)裝飾器外,類也可以實現(xiàn)课舍。Python 類有一個 __call__ 方法塌西,它能夠讓對象可調(diào)用。因此可以用該特性來實現(xiàn)裝飾器筝尾。例如實現(xiàn)一個磁盤緩存:

import os
import uuid
import glob
import pickle


class DiskCache(object):

    _NAMESPACE = uuid.UUID("c875fb30-a8a8-402d-a796-225a6b065cad")

    def __init__(self, func):
        self._func = func
        self.__name__ = func.__name__
        self.__module__ = func.__module__
        self.__doc__ = func.__doc__

        self.cache_path = "/tmp/.diskcache"

    def __call__(self, *args, **kw):
        params_uuid = uuid.uuid5(self._NAMESPACE, "-".join(map(str, (args, kw))))
        key = '{}-{}.cache'.format(self._func.__name__, str(params_uuid))
        cache_file = os.path.join(self.cache_path, key)

        if not os.path.exists(self.cache_path):
            os.makedirs(self.cache_path)

        try:
            with open(cache_file, 'rb') as f:
                val = pickle.load(f)
        except:
            val = self._func(*args, **kw)
            try:
                with open(cache_file, 'wb') as f:
                    pickle.dump(val, f)
            except:
                pass
        return val

    def clear_cache(self):
        for cache_file in glob.iglob("{}/{}-*".format(self.cache_path, self.__name__)):
            os.remove(cache_file)

@DiskCache
def add(x, y):
    print "add: %s + %s" % (x, y)
    return x, y

輸出:

add.clear_cache()
print add(1, 2)
print add(2, 3)
print add(1, 2)

print "cached files:", os.listdir(add.cache_path)
add.clear_cache()
print "cached files:", os.listdir(add.cache_path)

本質(zhì)上捡需,內(nèi)置的 property 裝飾器也是一個類,只不過它是一個描述符筹淫≌净裕可以用類似的形式實現(xiàn)一個可緩存的 property 裝飾器:

class CachedProperty(object):
    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.__name__)
        if value is None:
            value = self.func(obj)
            obj.__dict__[self.__name__] = value
        return value


class Foo(object):

    @CachedProperty
    def foo(self):
        print 'first calculate'
        result = 'this is result'
        return result


f = Foo()

print f.foo
print f.foo

輸出:

first calculate
this is result
this is result

多重裝飾

一個函數(shù)可以同時被多個裝飾器裝飾。裝飾的初始化在函數(shù)定義時完成损姜,初始化順序為離函數(shù)定義最近的裝飾器首先被初始化庵寞,最遠的則最后被初始化,初始化只進行一次薛匪。而裝飾器的執(zhí)行順序則跟初始化順序相反捐川。

def decorator_a(func):
    print "decorator_a"
    def wrapper(*args, **kw):
        print "call %s in decorator_a" % func.__name__
        return func()
    return wrapper

def decorator_b(func):
    print "decorator_b"
    def wrapper(*args, **kw):
        print "call %s in decorator_b" % func.__name__
        return func()
    return wrapper

@decorator_a
@decorator_b
def foo():
    print "foo"

print "-"*10
foo()
foo()

輸出:

decorator_b
decorator_a
----------
call wrapper in decorator_a
call foo in decorator_b
foo
call wrapper in decorator_a
call foo in decorator_b
foo

有以上示例可以看出,離函數(shù)定義最近的 decorator_b 裝飾器首先被初始化逸尖,在執(zhí)行時則是里函數(shù)定義最遠的 decorator_a 首先被執(zhí)行古沥。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瘸右,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子岩齿,更是在濱河造成了極大的恐慌太颤,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盹沈,死亡現(xiàn)場離奇詭異龄章,居然都是意外死亡,警方通過查閱死者的電腦和手機乞封,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門做裙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肃晚,你說我怎么就攤上這事锚贱。” “怎么了关串?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵拧廊,是天一觀的道長。 經(jīng)常有香客問我晋修,道長吧碾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任墓卦,我火速辦了婚禮倦春,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘趴拧。我一直安慰自己,他們只是感情好山叮,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布著榴。 她就那樣靜靜地躺著,像睡著了一般屁倔。 火紅的嫁衣襯著肌膚如雪脑又。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天锐借,我揣著相機與錄音问麸,去河邊找鬼。 笑死钞翔,一個胖子當著我的面吹牛严卖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播布轿,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼哮笆,長吁一口氣:“原來是場噩夢啊……” “哼来颤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起稠肘,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤福铅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后项阴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滑黔,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年环揽,在試婚紗的時候發(fā)現(xiàn)自己被綠了略荡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡薯演,死狀恐怖撞芍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情跨扮,我是刑警寧澤序无,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站衡创,受9級特大地震影響帝嗡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜璃氢,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一哟玷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧一也,春花似錦巢寡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至舆蝴,卻和暖如春谦絮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背洁仗。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工层皱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赠潦。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓叫胖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親她奥。 傳聞我的和親對象是個殘疾皇子臭家,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容