在python編程中砰识,我們經(jīng)常看到下面的函數(shù)用法:
with open("test.txt", "w") as f:
f.write("hello world!")
習慣了java開發(fā)的python初學者佣渴,心里不免犯嘀咕:
文件open操作之后辫狼,為什么沒有close,不怕文件描述符資源耗盡嗎辛润?
文件write操作沒有異常捕獲膨处,不怕中斷程序主流程嗎?如果您也有同樣的憂慮,那太正常不過了真椿,起碼說明您是一位有“開發(fā)原則”的人鹃答,同時也說明您對其背后的原理了解存在盲區(qū)。如果是這種情況突硝,本文強烈建議您耐心閱讀完以下章節(jié)测摔。為了系統(tǒng)的闡述其背后的奧秘,本文從最基本的函數(shù)講起狞换。
關(guān)于函數(shù)
在Python中避咆,一切皆為對象舟肉,包括函數(shù)修噪。
def foo(num):
return num + 1
value = foo(3)
print(value)
def bar():
print("bar")
foo = bar
foo()
上面簡單的函數(shù)例子中,可以總結(jié)幾點信息:
函數(shù)名字foo可以作為變量名字路媚,指向函數(shù)對象
函數(shù)名字foo作為對象黄琼,可以賦值給變量value
函數(shù)名字foo可以作為變量名字,指向其他函數(shù)bar
函數(shù)名字(函數(shù)對象)通過括號調(diào)用函數(shù) 不僅如此整慎,作為對象的函數(shù)也具有一般對象的特性脏款,比如:
函數(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 #注意這里沒有括號
print(bar()) # <function foo at 0x10a2f4140>
print(bar()()) # 1
等價于
print(foo()) # 1
函數(shù)嵌套
def outer():
x = 1
def inner():
print(x)
inner() # 注意這里有括號,直接被調(diào)用
outer() #
閉包
def outer(x):
def inner():
print(x)
return inner #沒括號裤园,不被直接調(diào)用
closure = outer(1) # closure就是一個閉包
closure()
同樣是嵌套函數(shù)撤师,只是稍改動一下,把局部變量 x 作為參數(shù)了傳遞進來拧揽,嵌套函數(shù)不再直接在函數(shù)里被調(diào)用剃盾,而是作為返回值返回,這里的 closure就是一個閉包淤袜,本質(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ù)作為參數(shù)并返回一個新函數(shù)的閉包.本質(zhì)上裝飾器也是函數(shù),outer 函數(shù)的返回值是 inner 函數(shù)铡羡。
注:上面示例中的裝飾器函數(shù)調(diào)用积蔚,可以用語法糖@簡寫為:
@outer
def foo():
print("foo")
foo()
我們進一步抽象裝飾器:
def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper
@decorator
def function():
print("hello, decorator")
可見,通過裝飾器烦周,可以讓代碼更加簡練尽爆、優(yōu)雅、可讀性更強读慎。
裝飾器進階
類裝飾器 基于類裝飾器的實現(xiàn)漱贱,必須實現(xiàn) call 和init 兩個內(nèi)置函數(shù)。 init :接收被裝飾函數(shù) call:實現(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ù)饱亿,還可以裝飾類,比如如果想改寫類的方法的部分實現(xiàn),除了通過類繼承重載彪笼,還可以通過裝飾器钻注,實現(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的指向來達到目的:通過指向新的方法new_getattribute配猫,在新的方法中在調(diào)用原來方法之前幅恋,添加額外邏輯。
偏函數(shù) 使用裝飾器的前提是裝飾器必須是可被調(diào)用的對象泵肄,比如函數(shù)捆交、實現(xiàn)了call 函數(shù)的類等,即將介紹的偏函數(shù)其實也是 callable 對象腐巢。在了解偏函數(shù)之前品追,先舉個例子:計算 100 加任意個數(shù)字的和。我們用parital函數(shù)解決這個問題:
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ù)作用和裝飾器一樣肉瓦,它可以擴展函數(shù)的功能,但又不完全等價于裝飾器胃惜。通常應(yīng)用的場景是當我們要頻繁調(diào)用某個函數(shù)時泞莉,其中某些參數(shù)是已知的固定值,可以將這些固定值“固定”船殉,然后用其他的參數(shù)參與調(diào)用鲫趁。類似偏導數(shù)計算那樣,固定幾個變量利虫,對剩下的變量求導挨厚。我們看下partial的函數(shù)參數(shù)定義:
func = functools.partial(func, *args, **keywords)
func: 需要被擴展的函數(shù),返回的函數(shù)其實是一個類 func 的函數(shù)
*args: 需要被固定的位置參數(shù)
**kwargs: 需要被固定的關(guān)鍵字參數(shù)
如果在原來的函數(shù) func 中關(guān)鍵字不存在列吼,將會擴展幽崩,如果存在,則會覆蓋
同樣是剛剛求和的代碼寞钥,不同的是加入的關(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ù)與裝飾器 我們再看看如何使用類和偏函數(shù)結(jié)合實現(xiàn)裝飾器理郑,如下所示蹄溉,DelayFunc 是一個實現(xiàn)了call 的類,delay 返回一個偏函數(shù)您炉,在這里 delay 就可以做為一個裝飾器:
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):
"""
裝飾器:推遲某個函數(shù)的執(zhí)行柒爵。
"""
# 此處為了避免定義額外函數(shù),
# 直接使用 functools.partial 幫助構(gòu)造 DelayFunc 實例
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棉胀,其實這也好理解法瑟,因為decorator返回的就是wrapper。但有時我們需要返回function的本來名字唁奢,那怎么做呢霎挟?python 的functools模塊提供了一系列的高階函數(shù)以及對可調(diào)用對象的操作,比如reduce麻掸,partial酥夭,wraps等。其中partial作為偏函數(shù)脊奋,在前面已經(jīng)介紹過熬北,warps旨在消除裝飾器對原函數(shù)造成的影響,即對原函數(shù)的相關(guān)屬性(比如name)進行拷貝诚隙,以達到裝飾器不修改原函數(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()讶隐,括號表示調(diào)用執(zhí)行函數(shù)。作為對比最楷,請看下面的調(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")
因為裝飾返回func整份,不會發(fā)生調(diào)用,因此需要兩對括號籽孙,其中function()返回的是函數(shù)定義。
print(function())
function()()
print(function.name)
裝飾器應(yīng)用之contextmanager
contextmanager是python中一個使用廣泛的上下文管理器火俄,(實際上也是裝飾器)經(jīng)常跟with語句一起使用犯建,用于精確地控制資源的分配和釋放」峡停回憶以下常規(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)異常谱仪,導致一些必須要執(zhí)行的操作無法執(zhí)行玻熙,通常使用try...finally語句,保證必要操作一定被執(zhí)行疯攒。但是如果代碼中大量使用這種語句嗦随,又導致程序邏輯冗余,可讀性變差敬尺。但是結(jié)合with枚尼,并將以上語句稍作改動:將try...finally的邏輯拆分成兩個函數(shù),分別執(zhí)行比如資源的初始化和釋放砂吞,封裝在一個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],用來簡化 try / finally 語句署恍。當執(zhí)行with語句、進入代碼塊前蜻直,調(diào)用enter方法盯质,代碼塊執(zhí)行結(jié)束之后執(zhí)行exit方法袁串。需要注意的是可以根據(jù)exit方法的返回值來決定是否拋出異常,如果沒有返回值或者返回值為 False 呼巷,則異常由上下文管理器處理般婆,如果為 True 則由用戶自己處理。上述代碼可以通過contextmanager進一步簡化:
@contextmanager
def controlled_execution():
#set things up
yield thing
#tear things down
with controlled_execution() as t:
print(t)
引入yield將函數(shù)變成生成器朵逝,yield將函數(shù)體分為兩部分:yield之前的語句在執(zhí)行with代碼塊之前執(zhí)行蔚袍,yield之后的代碼塊在with代碼塊之后執(zhí)行。到此為止配名,相信大家能夠理解文章開篇提到的代碼塊了啤咽,然后基于此,我們也可以自定義一個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!')