函式作為回傳值
高階函式除了可以接受函式作為引數外,還可以把函式作為結果值回傳,
我們來實作一個可變引數的求和,通常情況下,求和的函式是這樣定義的:
def calc_sum(*args):
i = 0
for n in args:
i = i + n
return i
但是,如果不需要立刻求和,而是在后面的代碼中,根據需要再計算怎么辦?可以不回傳求和的結果,而是回傳求和的函式:
def lazy_sum(*args):
def sum():
i = 0
for n in args:
i = i + n
return i
return sum
當我們呼叫lazy_sum()時,回傳的并不是求和結果,而是求和函式:
f = lazy_sum(1, 3, 5, 7, 9)
print(f)
# <function lazy_sum.<locals>.sum at 0x000002C5C32328C8>
呼叫函式f時,才真正計算求和的結果:
print(f())
# 25
在這個例子中,我們在函式lazy_sum中又定義了函式sum,并且,內部函式sum可以參考外部函式lazy_sum的引數和區域變數,當lazy_sum回傳函式sum時,相關引數和變數都保存在回傳的函式中,這種稱為“閉包(Closure)”的程式結構擁有極大的威力,
請再注意一點,當我們呼叫lazy_sum()時,每次呼叫都會回傳一個新的函式,即使傳入相同的引數:
f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
print(f1 == f2)
# False
f1()和f2()的呼叫結果是互不影響的,
閉包
回傳的函式在其定義內部參考了區域變數args,所以,當一個函式回傳了一個函式后,其內部的區域變數還被新函式參考,所以,閉包用起來簡單,實作起來可不容易,
另一個需要注意的問題是,回傳的函式并沒有立刻執行,而是直到呼叫了f()才執行,我們來看一個例子:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
在上面的例子中,每次回圈,都創建了一個新的函式,然后,把創建的3個函式都回傳了,
你可能認為呼叫f1(),f2()和f3()結果應該是1,4,9,但實際結果是:
print(f1())
# 9
print(f2())
# 9
print(f3())
# 9
全部都是9!原因就在于回傳的函式參考了變數i,但它并非立刻執行,等到3個函式都回傳時,它們所參考的變數i已經變成了3,因此最終結果為9,
回傳閉包時牢記一點:回傳函式不要參考任何回圈變數,或者后續會發生變化的變數,
如果一定要參考回圈變數怎么辦?方法是再創建一個函式,用該函式的引數系結回圈變數當前的值,無論該回圈變數后續如何更改,已系結到函式引數的值不變:
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被執行,因此i的當前值被傳入f()
return fs
f1, f2, f3 = count()
再看看結果:
print(f1())
# 1
print(f2())
# 4
print(f3())
# 9
缺點是代碼較長,可利用lambda函式縮短代碼,
由于函式也是一個物件,而且函式物件可以被賦值給變數,所以,通過變數也能呼叫該函式,
def now():
print('2021-04-17')
f = now
f()
__name__屬性
函式物件有一個__name__屬性,可以拿到函式的名字:
print(now.__name__) # now
print(f.__name__) # now
裝飾器
現在,假設我們要增強now()函式的功能,比如,在函式呼叫前后自動列印日志,但又不希望修改now()函式的定義,這種在代碼運行期間動態增加功能的方式,稱之為“裝飾器”(Decorator),
decorator的本質就是閉包,所以,我們要定義一個能列印日志的decorator,可以定義如下:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
觀察上面的log,因為它是一個decorator,所以接受一個函式作為引數,并回傳一個函式,我們要借助Python的@語法,把decorator置于函式的定義處:
@log
def now():
print('2021-04-17')
呼叫now()函式,不僅會運行now()函式本身,還會在運行now()函式前列印一行日志:
now()
# call now():
# 2021-04-17
把@log放到now()函式的定義處,相當于執行了陳述句:
now = log(now)
由于log()是一個decorator,回傳一個函式,所以,原來的now()函式仍然存在,只是現在同名的now變數指向了新的函式,于是呼叫now()將執行新函式,即在log()函式中回傳的wrapper()函式,
wrapper()函式的引數定義是(*args, **kw),因此,wrapper()函式可以接受任意引數的呼叫,在wrapper()函式內,首先列印日志,再緊接著呼叫原始函式,
如果decorator本身需要傳入引數,那就需要撰寫一個回傳decorator的高階函式,寫出來會更復雜,比如,要自定義log的文本:
def log(text):
def decorator(func):
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('2021-04-17')
執行結果如下:
now()
# execute now():
# 2021-04-17
和兩層嵌套的decorator相比,3層嵌套的效果是這樣的:
now = log('execute')(now)
我們來剖析上面的陳述句,首先執行log('execute'),回傳的是decorator函式,再呼叫回傳的函式,引數是now函式,回傳值最終是wrapper函式,
以上兩種decorator的定義都沒有問題,但還差最后一步,因為函式也是物件,它有__name__等屬性,但你去看經過decorator裝飾之后的函式,它們的__name__已經從原來的'now'變成了'wrapper':
print(now.__name__)
# wrapper
因為回傳的那個wrapper()函式名字就是'wrapper',所以,需要把原始函式的__name__等屬性復制到wrapper()函式中,否則,有些依賴函式簽名的代碼執行就會出錯,
不需要撰寫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
或者針對帶引數的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
import functools是匯入functools模塊,模塊的概念稍候講解,現在,只需記住在定義wrapper()的前面加上@functools.wraps(func)即可,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/277282.html
標籤:Python
下一篇:時間戳轉換小工具
