在python編程中,我們經常看到下面的函數用法:
with open("test.txt", "w") as f:
f.write("hello world!")
習慣了java開發的python初學者,心里不免犯嘀咕:
文件open操作之后,為什么沒有close,不怕文件描述符資源耗盡嗎?
文件write操作沒有異常捕獲,不怕中斷程序主流程嗎?如果您也有同樣的憂慮,那太正常不過了,起碼說明您是一位有“開發原則”的人,同時也說明您對其背后的原理了解存在盲區。如果是這種情況,本文強烈建議您耐心閱讀完以下章節。為了系統的闡述其背后的奧秘,本文從最基本的函數講起。
關于函數
在Python中,一切皆為對象,包括函數。
def foo(num):
return num + 1
value = foo(3)
print(value)
def bar():
print("bar")
foo = bar
foo()
上面簡單的函數例子中,可以總結幾點信息:
函數名字foo可以作為變量名字,指向函數對象
函數名字foo作為對象,可以賦值給變量value
函數名字foo可以作為變量名字,指向其他函數bar
函數名字(函數對象)通過括號調用函數 不僅如此,作為對象的函數也具有一般對象的特性,比如:
函數作為參數
def foo(num):
return num + 1
def bar(fun):
return fun(3)
value = bar(foo)
print(value)
函數作為返回值
def foo():
return 1
def bar():
return foo #注意這里沒有括號
print(bar()) # <function foo at 0x10a2f4140>
print(bar()()) # 1
等價于
print(foo()) # 1
函數嵌套
def outer():
x = 1
def inner():
print(x)
inner() # 注意這里有括號,直接被調用
outer() #
閉包
def outer(x):
def inner():
print(x)
return inner #沒括號,不被直接調用
closure = outer(1) # closure就是一個閉包
closure()
同樣是嵌套函數,只是稍改動一下,把局部變量 x 作為參數了傳遞進來,嵌套函數不再直接在函數里被調用,而是作為返回值返回,這里的 closure就是一個閉包,本質上它還是函數,閉包是引用了自由變量(x)的函數(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 函數其實就是一個裝飾器:一個帶有函數作為參數并返回一個新函數的閉包.本質上裝飾器也是函數,outer 函數的返回值是 inner 函數。
注:上面示例中的裝飾器函數調用,可以用語法糖@簡寫為:
@outer
def foo():
print("foo")
foo()
我們進一步抽象裝飾器:
def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper
@decorator
def function():
print("hello, decorator")
可見,通過裝飾器,可以讓代碼更加簡練、優雅、可讀性更強。
裝飾器進階
類裝飾器 基于類裝飾器的實現,必須實現 call 和init 兩個內置函數。 init :接收被裝飾函數 call:實現裝飾邏輯。以日志打印為例:
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")
裝飾類的裝飾器 裝飾器不僅可以裝飾函數,還可以裝飾類,比如如果想改寫類的方法的部分實現,除了通過類繼承重載,還可以通過裝飾器,實現如下:
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)
示例中,通過裝飾器函數log_getattribute修改原有類的屬性方法getattribute的指向來達到目的:通過指向新的方法new_getattribute,在新的方法中在調用原來方法之前,添加額外邏輯。
偏函數 使用裝飾器的前提是裝飾器必須是可被調用的對象,比如函數、實現了call 函數的類等,即將介紹的偏函數其實也是 callable 對象。在了解偏函數之前,先舉個例子:計算 100 加任意個數字的和。我們用parital函數解決這個問題:
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
跟上面的例子那樣,偏函數作用和裝飾器一樣,它可以擴展函數的功能,但又不完全等價于裝飾器。通常應用的場景是當我們要頻繁調用某個函數時,其中某些參數是已知的固定值,可以將這些固定值“固定”,然后用其他的參數參與調用。類似偏導數計算那樣,固定幾個變量,對剩下的變量求導。我們看下partial的函數參數定義:
func = functools.partial(func, *args, **keywords)
func: 需要被擴展的函數,返回的函數其實是一個類 func 的函數
*args: 需要被固定的位置參數
**kwargs: 需要被固定的關鍵字參數
如果在原來的函數 func 中關鍵字不存在,將會擴展,如果存在,則會覆蓋
同樣是剛剛求和的代碼,不同的是加入的關鍵字參數
def add(*args, *kwargs):
# 打印位置參數
for n in args:
print(n)
print("-"20)
# 打印關鍵字參數
for k, v in kwargs.items():
print('%s:%s' % (k, v))
# 暫不做返回,只看下參數效果,理解 partial 用法
普通調用
add(1, 2, 3, v1=10, v2=20)
add_partial = partial(add, 10, k1=10, k2=20)
add_partial(1, 2, 3, k3=20)
偏函數與裝飾器 我們再看看如何使用類和偏函數結合實現裝飾器,如下所示,DelayFunc 是一個實現了call 的類,delay 返回一個偏函數,在這里 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):
"""
裝飾器:推遲某個函數的執行。
"""
# 此處為了避免定義額外函數,
# 直接使用 functools.partial 幫助構造 DelayFunc 實例
return functools.partial(DelayFunc, duration)
@delay(duration=2)
def add(a, b):
return a+b
wraps
繼續深入函數裝飾器,首先打印被裝飾的函數function的名字:
def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper
@decorator
def function():
print("hello, decorator")
print(function.name) #wrapper
輸出發現是wrapper,其實這也好理解,因為decorator返回的就是wrapper。但有時我們需要返回function的本來名字,那怎么做呢?python 的functools模塊提供了一系列的高階函數以及對可調用對象的操作,比如reduce,partial,wraps等。其中partial作為偏函數,在前面已經介紹過,warps旨在消除裝飾器對原函數造成的影響,即對原函數的相關屬性(比如name)進行拷貝,以達到裝飾器不修改原函數(屬性)的目的:
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(),括號表示調用執行函數。作為對比,請看下面的調用:
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,不會發生調用,因此需要兩對括號,其中function()返回的是函數定義。
print(function())
function()()
print(function.name)
裝飾器應用之contextmanager
contextmanager是python中一個使用廣泛的上下文管理器,(實際上也是裝飾器)經常跟with語句一起使用,用于精確地控制資源的分配和釋放。回憶以下常規代碼結構:
def controlled_execution(callback):
try:
#比如環境初始化、資源分配等
set things up
callback(thing)
finally:
#比如資源回收、事物提交等
tear things down
def my_function(thing):
#執行具體的業務邏輯
do something
controlled_execution(my_function)
以上為了防止業務邏輯出現異常,導致一些必須要執行的操作無法執行,通常使用try...finally語句,保證必要操作一定被執行。但是如果代碼中大量使用這種語句,又導致程序邏輯冗余,可讀性變差。但是結合with,并將以上語句稍作改動:將try...finally的邏輯拆分成兩個函數,分別執行比如資源的初始化和釋放,封裝在一個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 語句。當執行with語句、進入代碼塊前,調用enter方法,代碼塊執行結束之后執行exit方法。需要注意的是可以根據exit方法的返回值來決定是否拋出異常,如果沒有返回值或者返回值為 False ,則異常由上下文管理器處理,如果為 True 則由用戶自己處理。上述代碼可以通過contextmanager進一步簡化:
@contextmanager
def controlled_execution():
#set things up
yield thing
#tear things down
with controlled_execution() as t:
print(t)
引入yield將函數變成生成器,yield將函數體分為兩部分:yield之前的語句在執行with代碼塊之前執行,yield之后的代碼塊在with代碼塊之后執行。到此為止,相信大家能夠理解文章開篇提到的代碼塊了,然后基于此,我們也可以自定義一個open函數:
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!')