在python編程中,我們經(jīng)??吹较旅娴暮瘮?shù)用法:
with open("test.txt", "w") as f:
f.write("hello world!")
習(xí)慣了java開發(fā)的python初學(xué)者,心里不免犯嘀咕:
文件open操作之后,為什么沒有close,不怕文件描述符資源耗盡嗎?
文件write操作沒有異常捕獲,不怕中斷程序主流程嗎?如果您也有同樣的憂慮,那太正常不過了,起碼說明您是一位有“開發(fā)原則”的人,同時(shí)也說明您對(duì)其背后的原理了解存在盲區(qū)。如果是這種情況,本文強(qiáng)烈建議您耐心閱讀完以下章節(jié)。為了系統(tǒng)的闡述其背后的奧秘,本文從最基本的函數(shù)講起。
關(guān)于函數(shù)
在Python中,一切皆為對(duì)象,包括函數(shù)。
def foo(num):
return num + 1
value = foo(3)
print(value)
def bar():
print("bar")
foo = bar
foo()
上面簡(jiǎn)單的函數(shù)例子中,可以總結(jié)幾點(diǎn)信息:
函數(shù)名字foo可以作為變量名字,指向函數(shù)對(duì)象
函數(shù)名字foo作為對(duì)象,可以賦值給變量value
函數(shù)名字foo可以作為變量名字,指向其他函數(shù)bar
函數(shù)名字(函數(shù)對(duì)象)通過括號(hào)調(diào)用函數(shù) 不僅如此,作為對(duì)象的函數(shù)也具有一般對(duì)象的特性,比如:
函數(shù)作為參數(shù)
def foo(num):
return num + 1
def bar(fun):
return fun(3)
value = bar(foo)
print(value)
函數(shù)作為返回值
def foo():
return 1
def bar():
return foo #注意這里沒有括號(hào)
print(bar()) # <function foo at 0x10a2f4140>
print(bar()()) # 1
等價(jià)于
print(foo()) # 1
函數(shù)嵌套
def outer():
x = 1
def inner():
print(x)
inner() # 注意這里有括號(hào),直接被調(diào)用
outer() #
閉包
def outer(x):
def inner():
print(x)
return inner #沒括號(hào),不被直接調(diào)用
closure = outer(1) # closure就是一個(gè)閉包
closure()
同樣是嵌套函數(shù),只是稍改動(dòng)一下,把局部變量 x 作為參數(shù)了傳遞進(jìn)來,嵌套函數(shù)不再直接在函數(shù)里被調(diào)用,而是作為返回值返回,這里的 closure就是一個(gè)閉包,本質(zhì)上它還是函數(shù),閉包是引用了自由變量(x)的函數(shù)(inner)。
裝飾器
def outer(func):
def inner():
print("before call fun")
func()
print("after call fun")
return inner
def foo():
print("foo")
new_foo = outer(foo)
new_foo()
outer 函數(shù)其實(shí)就是一個(gè)裝飾器:一個(gè)帶有函數(shù)作為參數(shù)并返回一個(gè)新函數(shù)的閉包.本質(zhì)上裝飾器也是函數(shù),outer 函數(shù)的返回值是 inner 函數(shù)。
注:上面示例中的裝飾器函數(shù)調(diào)用,可以用語(yǔ)法糖@簡(jiǎn)寫為:
@outer
def foo():
print("foo")
foo()
我們進(jìn)一步抽象裝飾器:
def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper
@decorator
def function():
print("hello, decorator")
可見,通過裝飾器,可以讓代碼更加簡(jiǎn)練、優(yōu)雅、可讀性更強(qiáng)。
裝飾器進(jìn)階
類裝飾器 基于類裝飾器的實(shí)現(xiàn),必須實(shí)現(xiàn) call 和init 兩個(gè)內(nèi)置函數(shù)。 init :接收被裝飾函數(shù) call:實(shí)現(xiàn)裝飾邏輯。以日志打印為例:
class logger(object):
def init(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("[INFO]: the function {func}() is running..."\
.format(func=self.func.__name__))
return self.func(*args, **kwargs)
@logger
def say(something):
print("say {}!".format(something))
say("hello")
裝飾類的裝飾器 裝飾器不僅可以裝飾函數(shù),還可以裝飾類,比如如果想改寫類的方法的部分實(shí)現(xiàn),除了通過類繼承重載,還可以通過裝飾器,實(shí)現(xiàn)如下:
def log_getattribute(cls):
# Get the original implementation
orig_getattribute = cls.getattribute
# Make a new definition
def new_getattribute(self, name):
print('getting:', name)
return orig_getattribute(self, name)
# Attach to the class and return
cls.__getattribute__ = new_getattribute
return cls
Example use
@log_getattribute
class A:
def init(self,x):
self.x = x
def spam(self):
pass
a = A(42)
print(a.x)
示例中,通過裝飾器函數(shù)log_getattribute修改原有類的屬性方法getattribute的指向來達(dá)到目的:通過指向新的方法new_getattribute,在新的方法中在調(diào)用原來方法之前,添加額外邏輯。
偏函數(shù) 使用裝飾器的前提是裝飾器必須是可被調(diào)用的對(duì)象,比如函數(shù)、實(shí)現(xiàn)了call 函數(shù)的類等,即將介紹的偏函數(shù)其實(shí)也是 callable 對(duì)象。在了解偏函數(shù)之前,先舉個(gè)例子:計(jì)算 100 加任意個(gè)數(shù)字的和。我們用parital函數(shù)解決這個(gè)問題:
from functools import partial
def add(*args):
return sum(args)
add_100 = partial(add, 100)
print(add_100(1, 2)) # 103
print(add_100(1, 2, 3)) # 106
跟上面的例子那樣,偏函數(shù)作用和裝飾器一樣,它可以擴(kuò)展函數(shù)的功能,但又不完全等價(jià)于裝飾器。通常應(yīng)用的場(chǎng)景是當(dāng)我們要頻繁調(diào)用某個(gè)函數(shù)時(shí),其中某些參數(shù)是已知的固定值,可以將這些固定值“固定”,然后用其他的參數(shù)參與調(diào)用。類似偏導(dǎo)數(shù)計(jì)算那樣,固定幾個(gè)變量,對(duì)剩下的變量求導(dǎo)。我們看下partial的函數(shù)參數(shù)定義:
func = functools.partial(func, *args, **keywords)
func: 需要被擴(kuò)展的函數(shù),返回的函數(shù)其實(shí)是一個(gè)類 func 的函數(shù)
*args: 需要被固定的位置參數(shù)
**kwargs: 需要被固定的關(guān)鍵字參數(shù)
如果在原來的函數(shù) func 中關(guān)鍵字不存在,將會(huì)擴(kuò)展,如果存在,則會(huì)覆蓋
同樣是剛剛求和的代碼,不同的是加入的關(guān)鍵字參數(shù)
def add(*args, *kwargs):
# 打印位置參數(shù)
for n in args:
print(n)
print("-"20)
# 打印關(guān)鍵字參數(shù)
for k, v in kwargs.items():
print('%s:%s' % (k, v))
# 暫不做返回,只看下參數(shù)效果,理解 partial 用法
普通調(diào)用
add(1, 2, 3, v1=10, v2=20)
add_partial = partial(add, 10, k1=10, k2=20)
add_partial(1, 2, 3, k3=20)
偏函數(shù)與裝飾器 我們?cè)倏纯慈绾问褂妙惡推瘮?shù)結(jié)合實(shí)現(xiàn)裝飾器,如下所示,DelayFunc 是一個(gè)實(shí)現(xiàn)了call 的類,delay 返回一個(gè)偏函數(shù),在這里 delay 就可以做為一個(gè)裝飾器:
import time
import functools
class DelayFunc:
def init(self, duration, func):
self.duration = duration
self.func = func
def __call__(self, *args, **kwargs):
print(f'Wait for {self.duration} seconds...')
time.sleep(self.duration)
return self.func(*args, **kwargs)
def delay(duration):
"""
裝飾器:推遲某個(gè)函數(shù)的執(zhí)行。
"""
# 此處為了避免定義額外函數(shù),
# 直接使用 functools.partial 幫助構(gòu)造 DelayFunc 實(shí)例
return functools.partial(DelayFunc, duration)
@delay(duration=2)
def add(a, b):
return a+b
wraps
繼續(xù)深入函數(shù)裝飾器,首先打印被裝飾的函數(shù)function的名字:
def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper
@decorator
def function():
print("hello, decorator")
print(function.name) #wrapper
輸出發(fā)現(xiàn)是wrapper,其實(shí)這也好理解,因?yàn)閐ecorator返回的就是wrapper。但有時(shí)我們需要返回function的本來名字,那怎么做呢?python 的functools模塊提供了一系列的高階函數(shù)以及對(duì)可調(diào)用對(duì)象的操作,比如reduce,partial,wraps等。其中partial作為偏函數(shù),在前面已經(jīng)介紹過,warps旨在消除裝飾器對(duì)原函數(shù)造成的影響,即對(duì)原函數(shù)的相關(guān)屬性(比如name)進(jìn)行拷貝,以達(dá)到裝飾器不修改原函數(shù)(屬性)的目的:
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kw):
print(func.name)
return func()
return wrapper
@decorator
def function():
print("hello, decorator")
function()
print(function.name)
注意代碼中return func(),括號(hào)表示調(diào)用執(zhí)行函數(shù)。作為對(duì)比,請(qǐng)看下面的調(diào)用:
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kw):
print(func.name)
return func
return wrapper
@decorator
def function():
print("hello, decorator")
因?yàn)檠b飾返回func,不會(huì)發(fā)生調(diào)用,因此需要兩對(duì)括號(hào),其中function()返回的是函數(shù)定義。
print(function())
function()()
print(function.name)
裝飾器應(yīng)用之contextmanager
contextmanager是python中一個(gè)使用廣泛的上下文管理器,(實(shí)際上也是裝飾器)經(jīng)常跟with語(yǔ)句一起使用,用于精確地控制資源的分配和釋放?;貞浺韵鲁R?guī)代碼結(jié)構(gòu):
def controlled_execution(callback):
try:
#比如環(huán)境初始化、資源分配等
set things up
callback(thing)
finally:
#比如資源回收、事物提交等
tear things down
def my_function(thing):
#執(zhí)行具體的業(yè)務(wù)邏輯
do something
controlled_execution(my_function)
以上為了防止業(yè)務(wù)邏輯出現(xiàn)異常,導(dǎo)致一些必須要執(zhí)行的操作無法執(zhí)行,通常使用try...finally語(yǔ)句,保證必要操作一定被執(zhí)行。但是如果代碼中大量使用這種語(yǔ)句,又導(dǎo)致程序邏輯冗余,可讀性變差。但是結(jié)合with,并將以上語(yǔ)句稍作改動(dòng):將try...finally的邏輯拆分成兩個(gè)函數(shù),分別執(zhí)行比如資源的初始化和釋放,封裝在一個(gè)class中:
class controlled_execution:
def enter(self):
set things up
return thing
def exit(self, type, value, traceback):
tear things down
with controlled_execution() as thing:
# code body
do something
其中with expression [as variable],用來簡(jiǎn)化 try / finally 語(yǔ)句。當(dāng)執(zhí)行with語(yǔ)句、進(jìn)入代碼塊前,調(diào)用enter方法,代碼塊執(zhí)行結(jié)束之后執(zhí)行exit方法。需要注意的是可以根據(jù)exit方法的返回值來決定是否拋出異常,如果沒有返回值或者返回值為 False ,則異常由上下文管理器處理,如果為 True 則由用戶自己處理。上述代碼可以通過contextmanager進(jìn)一步簡(jiǎn)化:
@contextmanager
def controlled_execution():
#set things up
yield thing
#tear things down
with controlled_execution() as t:
print(t)
引入yield將函數(shù)變成生成器,yield將函數(shù)體分為兩部分:yield之前的語(yǔ)句在執(zhí)行with代碼塊之前執(zhí)行,yield之后的代碼塊在with代碼塊之后執(zhí)行。到此為止,相信大家能夠理解文章開篇提到的代碼塊了,然后基于此,我們也可以自定義一個(gè)open函數(shù):
from contextlib import contextmanager
@contextmanager
def my_open(name):
f = open(name, 'w')
yield f
f.close()
with my_open('some_file') as f:
f.write('hola!')