一.回調(diào)函數(shù)
1.回調(diào)函數(shù)的概念: 是在某一函數(shù)中調(diào)用另一個(gè)函數(shù)變量方式,來執(zhí)行函數(shù).回調(diào)函數(shù)不是有實(shí)現(xiàn)方調(diào)用,應(yīng)該在特定的的時(shí)間或事件下,由另一個(gè)函數(shù)調(diào)用的,用于對(duì)某一事件或條件的響應(yīng).
2.什么事回調(diào)
軟件模塊之間總是存在著一定的接口,從調(diào)用方式上,可以把他們分為三類: 同步調(diào)用春贸、回調(diào)和異步調(diào)用迈嘹。
- 同步調(diào)用是一種阻塞式調(diào)用钧舌,調(diào)用方要等待對(duì)方執(zhí)行完畢才返回,它是一種單向調(diào)用歉糜;
- 回調(diào)是一種雙向調(diào)用模式,也就是說苫纤,被調(diào)用方在接口被調(diào)用時(shí)也會(huì)調(diào)用對(duì)方的接口叠赦;
- 異步調(diào)用是一種類似消息或事件的機(jī)制,不過它的調(diào)用方向剛好相反牲蜀,接口的服務(wù)在收到某種訊息或發(fā)生某種事件時(shí)笆制,會(huì)主動(dòng)通知客戶方(即調(diào)用客戶方的接口.
回調(diào)和異步調(diào)用的關(guān)系非常緊密,通常我們使用回調(diào)來實(shí)現(xiàn)異步消息的注冊(cè)涣达,通過異步調(diào)用來實(shí)現(xiàn)消息的通知在辆。同步調(diào)用是三者當(dāng)中最簡(jiǎn)單的,而回調(diào)又常常是異步調(diào)用的基礎(chǔ)度苔,因此开缎,下面我們著重討論回調(diào)機(jī)制在不同軟件架構(gòu)中的實(shí)現(xiàn)回調(diào)和異步調(diào)用的關(guān)系非常緊密,通常我們使用回調(diào)來實(shí)現(xiàn)異步消息的注冊(cè)林螃,通過異步調(diào)用來實(shí)現(xiàn)消息的通知。同步調(diào)用是三者當(dāng)中最簡(jiǎn)單的俺泣,而回調(diào)又常常是異步調(diào)用的基礎(chǔ)疗认,因此,下面我們著重討論回調(diào)機(jī)制在不同軟件架構(gòu)中的實(shí)現(xiàn).
3.簡(jiǎn)單的案例
案例一
import random as rd
# -----------被調(diào)用方----------------------------
def newRN(fn): # 生成10個(gè)[0,1)之間小數(shù)
ns = []
for i in range(10):
n = round(rd.random(), 2)
ns.append(n)
# 不用直接 return, 因?yàn)檎{(diào)用方 通知不接返回結(jié)果
# 改成回調(diào)函數(shù)方式
fn(ns) # 調(diào)用是調(diào)用方函數(shù)伏钠,這一操作稱之為回調(diào)
# ----------------調(diào)用方------------------------
# 定義回調(diào)函數(shù)
def abc(*args):
# 進(jìn)入到本函數(shù)內(nèi)横漏,意味著被調(diào)用方函數(shù)已執(zhí)行完
print('生成數(shù)據(jù)成功')
print(args)
newRN(abc)
案例二
import random as rd
import time
def test(fn, n):
"""
測(cè)試生成 n 個(gè)隨機(jī)數(shù)需要的時(shí)間(秒)
:param fn: 回調(diào)函數(shù)
:param n: 生成的數(shù)量
:return:
"""
start_time = time.time() # 開始時(shí)間
for i in range(n):
rd.random()
time.sleep(0.1) # 讓CPU休眠0.1秒
end_time = time.time() # 循環(huán)結(jié)束的時(shí)間
seconds = round(end_time - start_time) # 執(zhí)行的時(shí)間
fn(seconds,n) # 回傳用時(shí)(秒),n 生成的數(shù)量
def test_callback(tm_s,n):
print('生成 {0}個(gè)數(shù)熟掂,用時(shí) {1} 秒'.format(n, tm_s))
test(test_callback, 20)
案例三
def findNumbers(path,num,fn):
l =[]
rowNo = 1
with open(path,'r+') as f:
for line in f.readlines():
index = line.find(str(num))
if index !=-1:
l.append((rowNo,index+1))
rowNo +=1
fn(l)
def callback_(sl):
print(sl)
findNumbers('222.txt',5,callback_)
二.裝飾函數(shù)
一).裝飾函數(shù)的用途
裝飾器用來實(shí)現(xiàn)一種AOP切面功能缎浇,即一些函數(shù)在調(diào)用前都必須實(shí)現(xiàn)的功能,比如用戶是否登錄赴肚,用戶是否有權(quán)限素跺, 數(shù)據(jù)讀寫操作前打開事務(wù)等之類需求,由裝飾器來實(shí)現(xiàn)比較容易誉券。
二).裝飾器的前世今生
1.史前故事
先看一個(gè)簡(jiǎn)單例子指厌,實(shí)際可能會(huì)復(fù)雜很多:
def today():
print('2018-05-25')
現(xiàn)在有一個(gè)新的需求,希望可以記錄下函數(shù)的執(zhí)行日志踊跟,于是在代碼中添加日志代碼:
def today():
print('2018-05-25')
logging.info('today is running...')
如果函數(shù) yesterday()踩验、tomorrow() 也有類似的需求,怎么做?再寫一個(gè) logging 在yesterday函數(shù)里箕憾?這樣就造成大量雷同的代碼牡借,為了減少重復(fù)寫代碼,我們可以這樣做袭异,重新定義一個(gè)新的函數(shù):專門處理日志 钠龙,日志處理完之后再執(zhí)行真正的業(yè)務(wù)代碼
def logging_tool(func):
logging.info('%s is running...' % func.__name__)
func()
def today():
print('2018-05-25')
logging_tool(today)
這樣做邏輯上是沒問題的,功能是實(shí)現(xiàn)了扁远,但是我們調(diào)用的時(shí)候不再是調(diào)用真正的業(yè)務(wù)邏輯today函數(shù)俊鱼,而是換成了logging_tool函數(shù),這就破壞了原有的代碼結(jié)構(gòu)畅买,為了支持日志功能并闲,原有代碼需要大幅修改,那么有沒有更好的方式的呢谷羞?當(dāng)然有帝火,答案就是裝飾器。
2.開天辟地
一個(gè)簡(jiǎn)單的裝飾器
def logging_tool(func):
def wrapper(*arg, **kwargs):
logging.info('%s is running...' % func.__name__)
func() # 把today當(dāng)作參數(shù)傳遞進(jìn)來湃缎,執(zhí)行func()就相當(dāng)于執(zhí)行today()
return wrapper
def today():
print('2018-05-25')
today = logging_tool(today) # 因?yàn)檠b飾器logging_tool(today)返回函數(shù)對(duì)象wrapper犀填,故這條語句相當(dāng)于today=wrapper
today() # 執(zhí)行today() 就相當(dāng)于執(zhí)行wrapper()
以上也是裝飾器的原理!Iのァ九巡!
3.Pythonic世界的初探
@語法糖
接觸 Python 有一段時(shí)間的話,對(duì) @ 符號(hào)一定不陌生了蹂季,沒錯(cuò) @ 符號(hào)就是裝飾器的語法糖冕广,它放在函數(shù)開始定義的地方,這樣就可以省略最后一步再次賦值的操作
def logging_tool(func):
def wrapper(*arg, **kwargs):
logging.info('%s is running...' % func.__name__)
func() # 把today當(dāng)作參數(shù)傳遞進(jìn)來偿洁,執(zhí)行func()就相當(dāng)于執(zhí)行today()
return wrapper
@logging_tool
def today():
print('2018-05-25')
today()
有了 @ 撒汉,我們就可以省去today = logging_tool(today)這一句了,直接調(diào)用 today() 即可得到想要的結(jié)果涕滋。
不需要對(duì)today() 函數(shù)做任何修改睬辐,只需在定義的地方加上裝飾器,調(diào)用的時(shí)候還是和以前一樣宾肺。
如果我們有其他的類似函數(shù)溯饵,可以繼續(xù)調(diào)用裝飾器來修飾函數(shù),而不用重復(fù)修改函數(shù)或者增加新的封裝锨用。這樣瓣喊,提高程序可重復(fù)利用性,并增加程序的可讀性黔酥。
裝飾器在 Python 使用之所以如此方便藻三,歸因于Python函數(shù)能像普通的對(duì)象一樣能作為參數(shù)傳遞給其他函數(shù)洪橘,可以被賦值給其他變量,可以作為返回值棵帽,可以被定義在另外一個(gè)函數(shù)內(nèi)熄求。
裝飾器本質(zhì)上是一個(gè)Python函數(shù)或類,它可以讓其他函數(shù)或類在不需要做任何代碼修改的前提下增加額外的功能逗概,裝飾器的返回值也是一個(gè)函數(shù)/類對(duì)象弟晚。
它經(jīng)常用于有切面需求的場(chǎng)景,比如:插入日志逾苫、性能測(cè)試卿城、事務(wù)處理、緩存铅搓、權(quán)限校驗(yàn)等場(chǎng)景就轧,裝飾器是解決這類問題的絕佳設(shè)計(jì)今穿。
有了裝飾器窿冯,我們就可以抽離出大量與函數(shù)功能本身無關(guān)的代碼到裝飾器中并繼續(xù)重用凤跑。
簡(jiǎn)單來說:裝飾器的作用就是讓已經(jīng)存在的對(duì)象添加額外的功能。
4.多元化百家爭(zhēng)鳴
1).帶參數(shù)的裝飾器
裝飾器的語法允許我們?cè)谡{(diào)用時(shí)氢烘,提供其它參數(shù)怀偷,比如@decorator(condition)。為裝飾器的編寫和使用提供了更大的靈活性播玖。比如椎工,我們可以在裝飾器中指定日志的等級(jí),因?yàn)椴煌瑯I(yè)務(wù)函數(shù)可能需要不同的日志級(jí)別蜀踏。
def logging_tool(level):
def decorator(func):
def wrapper(*arg, **kwargs):
if level == 'error':
logging.error('%s is running...' % func.__name__)
elif level == 'warn':
logging.warn('%s is running...' % func.__name__)
else:
logging.info('%s is running...' % func.__name__)
func()
return wrapper
return decorator
@logging_tool(level='warn')
def today(name='devin'):
print('Hello, %s! Today is 208-05-25' % name)
today()
2).讓裝飾器同時(shí)支持帶參數(shù)或不帶參數(shù)
def new_logging_tool(obj):
if isinstanc(obj, str): # 帶參數(shù)的情況晋渺,參數(shù)類型為str
def decorator(func):
@functools.wraps(func)
def wrapper(*arg, **kwargs):
if obj == 'error':
logging.error('%s is running...' % func.__name__)
elif obj == 'warn':
logging.warn('%s is running...' % func.__name__)
else:
logging.info('%s is running...' % func.__name__)
func()
return wrapper
return decorator
else: # 不帶參數(shù)的情況,參數(shù)類型為函數(shù)類型脓斩,即被裝飾的函數(shù)
@functools.wraps(obj)
def wrapper(*args, **kwargs):
logging.info('%s is running...' % obj.__name__)
obj()
return wrapper
@new_logging_tool
def yesterday():
print('2018-05-24')
yesterday()
@new_logging_tool('warn')
def today(name='devin'):
print('Hello, %s! Today is 208-05-25' % name)
today()
如上所示,參數(shù)有兩種類型畴栖,一種是字符串随静,另一種是可調(diào)用的函數(shù)類型。因此吗讶,通過對(duì)參數(shù)類型的判斷即可實(shí)現(xiàn)支持帶參數(shù)和不帶參數(shù)的兩種情況燎猛。
3).類裝飾器
裝飾器不僅可以是函數(shù),還可以是類照皆,相比函數(shù)裝飾器重绷,類裝飾器具有靈活度大、高內(nèi)聚膜毁、封裝性等優(yōu)點(diǎn)昭卓。使用類裝飾器主要依靠類的call方法愤钾,當(dāng)使用 @ 形式將裝飾器附加到函數(shù)上時(shí),就會(huì)調(diào)用此方法候醒。
- 示例一能颁、被裝飾函數(shù)不帶參數(shù)
class Foo(object):
def __init__(self, func):
self._func = func # 初始化裝飾的函數(shù)
def __call__(self):
print ('class decorator runing')
self._func() # 調(diào)用裝飾的函數(shù)
print ('class decorator ending')
@Foo
def bar(): # 被裝飾函數(shù)不帶參數(shù)的情況
print ('bar')
bar()
- 示例二、被裝飾函數(shù)帶參數(shù)
class Counter:
def __init__(self, func):
self.func = func
self.count = 0 # 記錄函數(shù)被調(diào)用的次數(shù)
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
@Counter
def today(name='devin'):
print('Hello, %s! Today is 208-05-25' % name) # 被裝飾的函數(shù)帶參數(shù)的情況
for i in range(10):
today()
print(today.count) # 10
- 示例三倒淫、不依賴初始化函數(shù)伙菊,單獨(dú)使用call函數(shù)實(shí)現(xiàn)(體現(xiàn)類裝飾器靈活性大、高內(nèi)聚敌土、封裝性高的特點(diǎn))
實(shí)現(xiàn)當(dāng)一些重要函數(shù)執(zhí)行時(shí)镜硕,打印日志到一個(gè)文件中,同時(shí)發(fā)送一個(gè)通知給用戶
class LogTool(object):
def __init__(self, logfile='out.log'):
self.logfile = logfile # 指定日志記錄文件
def __call__(self, func): # __call__作為裝飾器函數(shù)
@functools.wraps(func)
def wrapper(*args, **kwargs):
log_string = func.__name__ + " was called"
print(log_string) # 輸出日志
with open(self.logfile, 'a') as fw:
fw.write(log_string + '\n') # 保存日志
self.notify() # 發(fā)送通知
return func(*args, **kwargs)
return wrapper
# 在類中實(shí)現(xiàn)通知功能的封裝
def notify(self):
pass
@LogTool() # 單獨(dú)使用__call__函數(shù)實(shí)現(xiàn)時(shí)返干,別忘了添加括號(hào)兴枯,進(jìn)行類的初始化
def bill_func():
pass
進(jìn)一步擴(kuò)展,給LogTool創(chuàng)建子類犬金,來添加email的功能:
class EmailTool(LogTool):
"""
LogTool的子類念恍,實(shí)現(xiàn)email通知功能,在函數(shù)調(diào)用時(shí)發(fā)送email給用戶
"""
def __init__(self, email='admin@myproject.com', *args, **kwargs):
self.email = email
super(EmailTool, self).__init__(*args, **kwargs)
# 覆蓋父類的通知功能晚顷,實(shí)現(xiàn)發(fā)送一封email到self.email
def notify(self):
pass
@EmailTool()
def bill_func():
pass
4).裝飾函數(shù) -> 裝飾類
- 函數(shù)層面的裝飾器很常見峰伙,以一個(gè)函數(shù)作為輸入,返回一個(gè)新的函數(shù)该默;
- 類層面的裝飾其實(shí)也類似瞳氓,已一個(gè)類作為輸入,返回一個(gè)新的類栓袖;
例如:給一個(gè)已有的類添加長(zhǎng)度屬性和getter匣摘、setter方法
def Length(cls):
class NewClass(cls):
@property
def length(self):
if hasattr(self, '__len__'):
self._length = len(self)
return self._length
@length.setter
def length(self, value):
self._length = value
return NewClass
@Length
class Tool(object):
pass
t = Tool()
t.length = 10
print(t.length) # 10
5.上古神器
1) @property -> getter/setter方法
示例:給一個(gè)Student添加score屬性的getter、setter方法
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer')
if value < 0 or value > 100:
raise ValueError('score must between 0~100!')
self._score = value
s = Student()
s.core = 60
print('s.score = ', s.score)
s.score = 999 # ValueError: score must between 0~100!
2). @classmethod裹刮、@staticmethod
- @classmethod 類方法:定義備選構(gòu)造器音榜,第一個(gè)參數(shù)是類本身(參數(shù)名不限制,一般用cls)
- @staticmethod 靜態(tài)方法:跟類關(guān)系緊密的函數(shù)
簡(jiǎn)單原理示例:
class A(object):
@classmethod
def method(cls):
pass
等價(jià)于
class A(object):
def method(cls):
pass
method = classmethod(method)
3).@functools.wraps
裝飾器極大地復(fù)用了代碼捧弃,但它有一個(gè)缺點(diǎn):因?yàn)榉祷氐氖乔短椎暮瘮?shù)對(duì)象wrapper赠叼,不是原函數(shù),導(dǎo)致原函數(shù)的元信息丟失违霞,比如函數(shù)的docstring嘴办、name、參數(shù)列表等信息买鸽。不過呢涧郊,辦法總比困難多,我們可以通過@functools.wraps將原函數(shù)的元信息拷貝到裝飾器里面的func函數(shù)中眼五,使得裝飾器里面的func和原函數(shù)有一樣的元信息妆艘。
def timethis(func):
"""
Decorator that reports the execution time.
"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(func.__name__, time.time() - start)
return result
return wrapper
@timethis
def countdown(n: int):
"""Counts down"""
while n > 0:
n -= 1
countdown(10000000) # 1.3556335
print(countdown.__name__, ' doc: ', countdown.__doc__, ' annotations: ', countdown.__annotations__)
@functools.wraps讓我們可以通過屬性wrapped直接訪問被裝飾的函數(shù)彤灶,同時(shí)讓被裝飾函數(shù)正確暴露底層的參數(shù)簽名信息
countdown.__wrapped__(1000) # 訪問被裝飾的函數(shù)
print(inspect.signature(countdown)) # 輸出被裝飾函數(shù)的簽名信息
4).Easter egg
- 定義一個(gè)接受參數(shù)的包裝器
@decorator(x, y, z)
def func(a, b):
pass
等價(jià)于
func = decorator(x, y, z)(func)
即:decorator(x, y, z)的返回結(jié)果必須是一個(gè)可調(diào)用的對(duì)象,它接受一個(gè)函數(shù)作為參數(shù)并包裝它双仍。
- 一個(gè)函數(shù)可以同時(shí)定義多個(gè)裝飾器枢希,比如:
@a
@b
@c
def f():
pass
等價(jià)于
f = a(b(c(f)))
即:它的執(zhí)行順序是從里到外,最先調(diào)用最里層朱沃,最后調(diào)用最外層的裝飾器苞轿。