python-對裝飾器/語法糖/函數(shù)封裝 的思考

裝飾器

裝飾器本質上是一個Python函數(shù),它可以讓其他函數(shù)在不需要做任何代碼變動的前提下增加額外功能杂腰,裝飾器的返回值也是一個函數(shù)對象森渐。
它經常用于有切面需求的場景,比如:插入日志臊泰、性能測試蛉加、事務處理、緩存缸逃、權限校驗等場景针饥。裝飾器是解決這類問題的絕佳設計,有了裝飾器需频,我們就可以抽離出大量與函數(shù)功能本身無關的雷同代碼并繼續(xù)重用丁眼。概括的講,裝飾器的作用就是為已經存在的對象添加額外的功能昭殉。

def foo():
    print('i am foo')

現(xiàn)在有一個新的需求苞七,希望可以記錄下函數(shù)的執(zhí)行日志,于是在代碼中添加日志代碼:

def foo():
    print('i am foo')
    logging.info("foo is running")

bar()挪丢、bar2()也有類似的需求蹂风,怎么做?再寫一個logging在bar函數(shù)里乾蓬?這樣就造成大量雷同的代碼惠啄,為了減少重復寫代碼,我們可以這樣做任内,重新定義一個函數(shù):專門處理日志 撵渡,日志處理完之后再執(zhí)行真正的業(yè)務代碼

def use_logging(func):
    logging.warn("%s is running" % func.__name__)
    func()

def bar():
    print('i am bar')

use_logging(bar)

邏輯上不難理解, 但是這樣的話死嗦,我們每次都要將一個函數(shù)作為參數(shù)傳遞給use_logging函數(shù)姥闭。而且這種方式已經破壞了原有的代碼邏輯結構,之前執(zhí)行業(yè)務邏輯時越走,執(zhí)行運行bar()棚品,但是現(xiàn)在不得不改成use_logging(bar)靠欢。那么有沒有更好的方式的呢?當然有铜跑,答案就是裝飾器门怪。

def use_logging(func):      # 不帶參數(shù)的裝飾器 第一個def 參數(shù)就是func,帶參數(shù)的裝飾器锅纺,第一個def參數(shù)是裝飾元素參數(shù)掷空,第二個def參數(shù)才是func,注意區(qū)分(都是套路)
#  裝飾器就是某種閉包,總是return一些函數(shù)
    def wrapper(*args, **kwargs):
        logging.warn("%s is running" % func.__name__)
        return func(*args, **kwargs)
    return wrapper

def bar():
    print('i am bar')

bar = use_logging(bar)
bar()

函數(shù)use_logging就是裝飾器囤锉,它把執(zhí)行真正業(yè)務方法的func包裹在函數(shù)里面坦弟,看起來像bar被use_logging裝飾了。在這個例子中官地,函數(shù)進入和退出時 酿傍,被稱為一個橫切面(Aspect),這種編程方式被稱為面向切面的編程(Aspect-Oriented Programming)驱入。

@符號是裝飾器的語法糖赤炒,在定義函數(shù)的時候使用,避免再一次賦值操作

def use_logging(func):
# 不帶參數(shù)的裝飾器 第一個def 參數(shù)就是func亏较,帶參數(shù)的裝飾器莺褒,第一個def參數(shù)是裝飾元素參數(shù),第二個def參數(shù)才是func,注意區(qū)分(都是套路)
    def wrapper(*args, **kwargs):
        logging.warn("%s is running" % func.__name__)
        return func(*args)
    return wrapper    # return幾回雪情,最終回去的還是func

@use_logging
def foo():
    print("i am foo")

@use_logging
def bar():
    print("i am bar")

bar()          #  相當于省去`bar = use_logging(bar)`

如上所示遵岩,這樣我們就可以省去bar = use_logging(bar)這一句了,直接調用bar()即可得到想要的結果巡通。如果我們有其他的類似函數(shù)旷余,我們可以繼續(xù)調用裝飾器來修飾函數(shù),而不用重復修改函數(shù)或者增加新的封裝扁达。這樣正卧,我們就提高了程序的可重復利用性,并增加了程序的可讀性跪解。

帶參數(shù)的裝飾器
裝飾器還有更大的靈活性炉旷,例如帶參數(shù)的裝飾器:在上面的裝飾器調用中,比如@use_logging叉讥,該裝飾器唯一的參數(shù)就是執(zhí)行業(yè)務的函數(shù)窘行。裝飾器的語法允許我們在調用時,提供其它參數(shù)图仓,比如@decorator(a)罐盔。這樣,就為裝飾器的編寫和使用提供了更大的靈活性救崔。

In [10]: import logging

In [11]: def use_logging(level):
    ...:     def decorator(func):
    ...:         def wrapper(*args, **kwargs):
    ...:             if level == 'warn':
    ...:                 logging.warn('%s is running'% func.__name__)
    ...:             return func(*args)
    ...:         return wrapper
    ...:     return decorator
    ...:

In [12]: @use_logging(level="warn")
    ...: def foo(name="foo"):
    ...:     print('i am %s'% name)
    ...:

In [13]: foo()
/usr/local/bin/ipython3:5: DeprecationWarning: The 'warn' function is deprecated, use 'warning' instead
  import sys
WARNING:root:foo is running
i am foo

In [14]: @use_logging(level)
    ...: def foo(name="foo"):
    ...:     print('i am %s'% name)
    ...:
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-14-203ce5546292> in <module>()
----> 1 @use_logging(level)
      2 def foo(name="foo"):
      3     print('i am %s'% name)
      4

NameError: name 'level' is not defined

上面的use_logging是允許帶參數(shù)的裝飾器惶看。它實際上是對原有裝飾器的一個函數(shù)封裝捏顺,并返回一個裝飾器。我們可以將它理解為一個含有參數(shù)的閉包纬黎。當我 們使用@use_logging(level="warn")調用的時候幅骄,Python能夠發(fā)現(xiàn)這一層的封裝,并把參數(shù)傳遞到裝飾器的環(huán)境中本今。

類裝飾器
再來看看類裝飾器拆座,相比函數(shù)裝飾器,類裝飾器具有靈活度大冠息、高內聚挪凑、封裝性等優(yōu)點。使用類裝飾器還可以依靠類內部的__call__方法逛艰,當使用 @ 形式將裝飾器附加到函數(shù)上時躏碳,就會調用此方法

In [19]: class Foo(object):
    ...:     def __init__(self,func):
    ...:         self._func = func
    ...:     def __call__(self):      # 內置的特殊函數(shù)
    ...:         print('class decorator running')
    ...:         self._func()
    ...:         print('class decorator ending')
    ...:

In [20]: @Foo
    ...: def bar():
    ...:     print('bar')
    ...:

In [21]: bar()
class decorator running
bar
class decorator ending

functools.wraps
使用裝飾器極大地復用了代碼瓮孙,但是他有一個缺點就是原函數(shù)的元信息不見了,比如函數(shù)的docstring选脊、__name__杭抠、參數(shù)列表,先看例子:
裝飾器

def logged(func):
    def with_logging(*args, **kwargs):
        print func.__name__ + " was called"
        return func(*args, **kwargs)
    return with_logging

函數(shù)

@logged
def f(x):
   """does some math"""
   return x + x * x

該函數(shù)完成等價于:

def f(x):
    """does some math"""
    return x + x * x
f = logged(f)

不難發(fā)現(xiàn)恳啥,函數(shù)fwith_logging取代了偏灿,當然它的docstring__name__就是變成了with_logging函數(shù)的信息了钝的。

print f.__name__    # prints 'with_logging'
print f.__doc__     # prints None

這個問題就比較嚴重的翁垂,好在我們有functools.wrapswraps本身也是一個裝飾器硝桩,它能把原函數(shù)的元信息拷貝到裝飾器函數(shù)中沿猜,這使得裝飾器函數(shù)也有和原函數(shù)一樣的元信息了。

from functools import wraps
def logged(func):
    @wraps(func)      #  引入wraps,其他代碼不動碗脊,即解決問題
    def with_logging(*args, **kwargs):
        print func.__name__ + " was called"
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
    """does some math"""
    return x + x * x

print f.__name__  # prints 'f'
print f.__doc__   # prints 'does some math'

內置裝飾器
@staticmathod啼肩、@classmethod、@property

裝飾器的順序

@a
@b
@c
def f ():

等效于
f = a(b(c(f)))



函數(shù)與方法
類的函數(shù)稱為方法(method)衙伶,模塊里的函數(shù)稱為函數(shù)(function)祈坠。凡是def foo()這種,都是函數(shù)矢劲,在類中定義的函數(shù)赦拘,就是方法。
每一個包芬沉,模塊躺同,類阁猜,函數(shù),方法都應該包含文檔笋籽,包括類的__init__方法


特殊函數(shù)__call__

所有的函數(shù)都是可調用對象蹦漠。
一個類實例也可以變成一個可調用對象,只需要實現(xiàn)一個特殊方法__call__()车海。

我們把 Person 類變成一個可調用對象:

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

    def __call__(self, friend):
        print 'My name is %s...' % self.name
        print 'My friend is %s...' % friend

現(xiàn)在可以對 Person 實例直接調用:

>>> p = Person('Bob', 'male')
>>> p('Tim')
My name is Bob...
My friend is Tim...

單看p('Tim')你無法確定p是一個函數(shù)還是一個類實例笛园,所以,在Python中侍芝,函數(shù)也是對象研铆,對象和函數(shù)的區(qū)別并不顯著。



函數(shù)封裝
函數(shù)封裝是一種函數(shù)的功能州叠,它把一個程序員寫的一個或者多個功能通過函數(shù)棵红、類的方式封裝起來,對外只提供一個簡單的函數(shù)接口咧栗。當程序員在寫程序的過程中需要執(zhí)行同樣的操作時逆甜,程序員(調用者)不需要寫同樣的函數(shù)來調用,直接可以從函數(shù)庫里面調用致板。程序員也可以從網絡上下載的功能函數(shù)交煞,然后封裝到[編譯器]的[庫函數(shù)]中,當需要執(zhí)行這一功能的函數(shù)時斟或,直接調用即可素征。而程序員不必知道函數(shù)內部如何實現(xiàn)的,只需要知道這個函數(shù)或者類提供什么功能萝挤。


語法糖
語法糖(Syntactic sugar),是由Peter J. Landin(和圖靈一樣的天才人物御毅,是他最先發(fā)現(xiàn)了Lambda演算,由此而創(chuàng)立了函數(shù)式編程)創(chuàng)造的一個詞語端蛆,它意指那些沒有給計算機語言添加新功能,而只是對人類來說更“甜蜜”的語法酥泛。語法糖往往給程序員提供了更實用的編碼方式欺税,有益于更好的編碼風格,更易讀揭璃。不過其并沒有給語言添加什么新東西晚凿。

對于列表形如list_1 = [[1, 2], [3, 4, 5], [6, 7], [8], [9]]轉化成列表list_2 = [1, 2, 3, 4, 5, 6, 7, 8, 9]的問題。

一般方法

list_1 = [[1, 2], [3, 4, 5], [6, 7], [8], [9]]
list_2 = []
for _ in list_1:
    list_2 += _
print(list_2)

更Pythonic的方法二瘦馍,列表推導

list_1 = [[1, 2], [3, 4, 5], [6, 7], [8], [9]]
[i for k in list_1 for i in k]       #  ??注意歼秽,這里沒有 , 號

抽象用法(知道就好但不推薦哦)

list_1 = [[1, 2], [3, 4, 5], [6, 7], [8], [9]]
sum(list_1, [])

sum的第一個參數(shù)為可迭代對象即可,第二個參數(shù)默認為0



lambda表達式
函數(shù)式那一套黑魔法-語法糖

+1函數(shù)

f=lambda x:x+1

max函數(shù)(條件語句的寫法如下)

f_max=lambda x,y:x if x>y else y

filter, map, reduce
filter函數(shù)接受兩個參數(shù)情组,第一個是過濾函數(shù)燥筷,第二個是可遍歷的對象箩祥,用于選擇出所有滿足過濾條件的元素

去除小寫字母

s=filter(lambda x:not str(x).islower(),"asdasfAsfBsdfC")
for ch in s:
    print(ch)

map函數(shù)接受的參數(shù)類型與filter類似,它用于把函數(shù)作用于可遍歷對象的每一個元素肆氓。類似于數(shù)學中映射的概念袍祖。
例:求y=2x+1(偷偷用了一下range函數(shù)生成定義域)

s=map(lambda x:2*x+1,range(6))
for x in s:
    print(x)

range(6) = range(0, 6) = [ 0 ,1, 2, 3, 4, 5 ]
函數(shù)原型:range(start, end谢揪, scan):
range()函數(shù)可創(chuàng)建一個整數(shù)列表蕉陋,一般用在 for 循環(huán)中計數(shù)從start開始到end結束,但不包括end,scan:每次跳躍的間距拨扶,默認為1

reduce函數(shù)對每個元素作累計操作凳鬓,它接受的第一個參數(shù)必須是有兩個參數(shù)的函數(shù)。

In [54]: from functools import reduce
In [55]: s = reduce( lambda x,y: x+y, range(1,6) )
In [56]: print(s)
15

求乘積(第三個可選參數(shù)表示累計變量的初值)

from functools import reduce
s=reduce(lambda x,y:x*y,range(1,6),1)
print(s)
# 120

柯里化(curry)函數(shù)
如果一個函數(shù)需要2個參數(shù)患民,而你只傳入一個參數(shù)缩举,那么你就可以得到一個柯里化的函數(shù),這是函數(shù)式編程語言的重要特性之一
*3函數(shù)

f_mul=lambda x,y:x*y
from functools import partial
mul3=partial(f_mul,3)
print(mul3(1))
print(mul3(6))

注:匿名函數(shù)過多會影響代碼效率匹颤,勁酒雖好仅孩,不能貪杯



廖神關于裝飾器的解釋

如果decorator本身需要傳入?yún)?shù),那就需要編寫一個返回decorator的高階函數(shù)印蓖,寫出來會更復雜辽慕。比如,要自定義log的文本:

def log(text):       # text是想打印出的裝飾提示另伍,func指代的就是需要被裝飾的函數(shù)鼻百,都是這樣的套路
    def decorator(func):          # 記住套路=事谩0诔ⅰ!
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

這個3層嵌套的decorator用法如下:

@log('execute')
def now():
    print('2015-3-25')

執(zhí)行結果如下:

>>> now()
execute now():
2015-3-25

和兩層嵌套的decorator相比因悲,3層嵌套的效果是這樣的:

>>> now = log('execute')(now)

我們來剖析上面的語句堕汞,首先執(zhí)行log('execute'),返回的是decorator函數(shù)晃琳,再調用返回的函數(shù)讯检,參數(shù)是now函數(shù),返回值最終是wrapper函數(shù)卫旱。
以上兩種decorator的定義都沒有問題人灼,但還差最后一步。因為我們講了函數(shù)也是對象顾翼,它有__name__等屬性投放,但你去看經過decorator裝飾之后的函數(shù),它們的__name__已經從原來的'now'變成了'wrapper':

>>> now.__name__
'wrapper'

因為返回的那個wrapper()函數(shù)名字就是'wrapper'适贸,所以灸芳,需要把原始函數(shù)的__name__等屬性復制到wrapper()函數(shù)中涝桅,否則,有些依賴函數(shù)簽名的代碼執(zhí)行就會出錯烙样。

不需要編寫wrapper.__name__ = func.__name__這樣的代碼冯遂,Python內置的functools.wraps就是干這個事的,所以谒获,一個完整的decorator的寫法如下:

import functools

def log(func):
    @functools.wraps(func)   # 加這一行就可以了蛤肌,其他代碼不變
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

或者針對帶參數(shù)的decorator:

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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末究反,一起剝皮案震驚了整個濱河市寻定,隨后出現(xiàn)的幾起案子精耐,更是在濱河造成了極大的恐慌狼速,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卦停,死亡現(xiàn)場離奇詭異向胡,居然都是意外死亡,警方通過查閱死者的電腦和手機惊完,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門僵芹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人小槐,你說我怎么就攤上這事拇派。” “怎么了凿跳?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵件豌,是天一觀的道長。 經常有香客問我控嗜,道長茧彤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任疆栏,我火速辦了婚禮曾掂,結果婚禮上,老公的妹妹穿的比我還像新娘壁顶。我一直安慰自己珠洗,他們只是感情好,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布若专。 她就那樣靜靜地躺著许蓖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蛔糯,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天拯腮,我揣著相機與錄音,去河邊找鬼蚁飒。 笑死动壤,一個胖子當著我的面吹牛,可吹牛的內容都是我干的淮逻。 我是一名探鬼主播琼懊,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼爬早!你這毒婦竟也來了哼丈?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤筛严,失蹤者是張志新(化名)和其女友劉穎醉旦,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桨啃,經...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡车胡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了照瘾。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匈棘。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖析命,靈堂內的尸體忽然破棺而出主卫,到底是詐尸還是另有隱情,我是刑警寧澤鹃愤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布簇搅,位于F島的核電站,受9級特大地震影響昼浦,放射性物質發(fā)生泄漏馍资。R本人自食惡果不足惜筒主,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一关噪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乌妙,春花似錦使兔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春欲险,著一層夾襖步出監(jiān)牢的瞬間镐依,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工天试, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留槐壳,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓喜每,卻偏偏與公主長得像务唐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子带兜,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內容

  • 每個人都有的內褲主要功能是用來遮羞枫笛,但是到了冬天它沒法為我們防風御寒,咋辦刚照?我們想到的一個辦法就是把內褲改造一下刑巧,...
    chen_000閱讀 1,365評論 0 3
  • 本文為《爬著學Python》系列第四篇文章。從本篇開始无畔,本專欄在順序更新的基礎上海诲,會有不規(guī)則的更新。 在Pytho...
    SyPy閱讀 2,505評論 4 11
  • **裝飾器是一個很著名的設計模式檩互,經常被用于有切面需求的場景特幔,較為經典的有插入日志、性能測試闸昨、事務處理等蚯斯。裝飾器是...
    牛崽兒酷閱讀 321評論 0 0
  • Python進階框架 希望大家喜歡,點贊哦首先感謝廖雪峰老師對于該課程的講解 一饵较、函數(shù)式編程 1.1 函數(shù)式編程簡...
    Gaolex閱讀 5,502評論 6 53
  • 今天還是和往常一樣5.50分起床,聽課勇劣,下樓慢跑靖避。因為有時外出潭枣,環(huán)境影響沒有堅持運動,這周又接上去幻捏,感覺很累盆犁,心里...
    滴滴雨閱讀 259評論 0 0