python装饰器原理及应用

在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!')

参考

https://mp.weixin.qq.com/s/cMqJulHjfo5oYfnwKDP7zw

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容

  • 装饰器本质上是一个函数,该函数用来处理其他函数,它可以让其他函数在不需要修改代码的前提下增加额外的功能,装饰器的返...
    胡一巴阅读 379评论 0 0
  • 每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个办法就是把内裤改造一下,...
    chen_000阅读 1,352评论 0 3
  • 一、装饰器的基本使用 在不改变函数源代码的前提下,给函数添加新的功能,这时就需要用到“装饰器”。 0.开放封闭原则...
    NJingZYuan阅读 484评论 0 0
  • 一. 有时候我们会有这样需求: 在原有的逻辑前后添加一段逻辑 如: 在增/删/改操作之前检查用户是否登录、某个操...
    元亨利贞o阅读 691评论 1 4
  • 夸奖宝宝,就像使用抗生素一样,需要有一定的标准,如果夸奖不到位,对孩子也会产生负面影响。那么夸奖宝宝要遵循怎样的原...
    光谷鱼儿也想飞阅读 108评论 0 0