Python

Python基础-20装饰器

2020-05-17  本文已影响0人  Surpassme

20.装饰器

20.1 函数基础知识

    在Python中函数为一等公民,我们可以:

20.1.1 把函数赋值给变量

    在Python里,函数是对象,因此可以把它赋值给变量,如下所示:

def hello(name="Surpass"):
    return "Hello,"+name.capitalize()

    上述代码定义了一个函数hello(),具体功能是把对输入的姓名打招呼,并将姓名首字母转换为大写。下面将函数赋值给变量,如下所示:

func=hello
print(func)

其输出结果如下所示:

<function hello at 0x0000029D25213048>

    在上述代码中,将hello赋值给func并输出打印结果。从结果上来看,当不带括号使用hello时,仅输出函数对象,而非调用函数,当带上括号时,则表示调用函数,如下所示:

func=hello
print(func())

其输出结果如下所示:

Hello,Surpass

    既然函数是对象,那是不是可以尝试删除对象,来看看以下代码:

def hello(name="Surpass"):
    return "Hello,"+name.capitalize()

func=hello
del hello
try:
    print(hello())
except Exception as ex:
    print(ex)

print(func())

其输出结果如下所示:

name 'hello' is not defined
Hello,Surpass

    在上面代码中,虽然hello函数已经删除,但在删除之前,已经赋值给变量func,所以不影响func的功能。因此可以得出以下总结:

函数是对象,可以将基赋值给其他变量variable,在调用时,需要添加括号variable()

20.1.2 在函数中定义函数

    在Python中,可以在函数中定义函数,示例代码如下所示:

def hello(name="Surpass"):
    def welcome(country="China"):
        return "Hello,"+name.capitalize()+",Welcome to "+country
    print(welcome())

hello()

其输出结果如下所示:

Hello,Surpass,Welcome to China

    上述示例中,内部函数welcome表示欢迎来到某个国家,而且这个函数位于hello函数内部,只有通过调用hello函数时才能生效,测试代码如下所示:

def hello(name="Surpass"):
    def welcome(country="China"):
        return "Hello,"+name.capitalize()+",Welcome to "+country
    print(welcome())

try:
    print(welcome())
except Exception as ex:
    print(ex)

其输出结果如下所示:

name 'welcome' is not defined

    通过上面的示例代码可以得到以下总结:

在函数funcA中可以定义函数funcB,但funcB无法在函数funcA之外进行调用

20.1.3 在函数中返回函数

    通过前面两个示例,函数即可以返回其值,也可以返回其函数对象,示例代码如下所示:

def hello(type=1):

    def name(name="Surpass"):
        return "My name is "+name

    def welcome(country="China"):
        return " Welcome to "+country

    def occupation(occupation="engineer"):
        return " I am "+occupation

    if type==1:
        return name
    elif type==2:
        return welcome
    else:
        return occupation

    在函数hello中定义了3个函数,然后根据type参数来返回不同的信息,在hello函数中,返回都为函数对象,可视为变量,运行以下代码:

print(hello(1))
print(hello(1)())

func=hello(1)
print(func())

其输出结果如下所示:

<function hello.<locals>.name at 0x000001D65783D5E8>
My name is Surpass
My name is Surpass

    注意上面在写代码上的区别,如果不加括号,则代表仅返回函数对象,添加括号则代表调用函数,总结如下所示:

在函数funcA中可以定义多个函数funcB...,并且可以把funcB当成对象进行返回

20.1.4 把函数传递给函数

    在Python中,既然一切都为对象,那函数也可以做参数传递给另一个函数,示例代码如下所示:

import datetime
import time

def hello(name="Surpass"):
    print(f"Hello,{name}")

def testFunc(func):
    print(f"current time is {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    hello()
    time.sleep(2)
    print(f"current time is {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

testFunc(hello)

其输出结果如下所示:

current time is 2020-05-17 14:58:56
Hello,Surpass
current time is 2020-05-17 14:58:58

    在函数testFunc中接收参数为函数类型的函数hello,而且并未改变函数hello的任何内容,且实现在函数hello前后增加一些内容。因此我们可以总结如下所示:

函数funcA传递到函数funcB,函数funcB仅在函数funcA运行前后有操作,但不改变funcA,这就是装饰器的锥形。

20.2 装饰器

20.2.1 闭包

    维基的解释如下所示:

在计算机科学中,闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

    Python里面的闭包是一种高阶函数,返回值为函数对象,简单来讲就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外执行,这种函数称之为闭包。详细如下图所示:

01闭包定义.png

    在函数make_averager中,series为make_averager函数的局部变量,在averager函数中series称之为自由变量(未在本地作用域中绑定的变量)

示例如下所示:

def outter(name):
    def inner():
        print(f"para is {name}")
    return inner

    内部函数inner()可以使用外部函数outter()的参数name,最后返回内部函数对象inner。可以按照在函数中返回函数进行调用。现在我们将outter换成decorator,inner换成wrapper,如下所示:

def decorator(name):
    def wrapper():
        print(f"para is {name}")
    return wrapper

    以上这种形式就是装饰器,返回值为wrapper对象并等候调用,一旦被调用就运行 print(f"para is {name}")输出结果。对于装饰器而言,严格定义来讲,参数是函数而不是变量,常用形式如下所示:

def decorator(func):
    def wrapper():
        return func()
    return wrapper

20.2.2 装饰器初体验

    我们先来看看一个简单的示例,代码如下所示:

def hello():
    print("call hello function")

def decorator(func):
    def wrapper():
        return func()
    return wrapper

tmp=decorator(hello)
tmp()

其输出结果如下所示:

call hello function

    装饰器的特性就是给原函数做装饰,但不改变原函数的内容,在以下代码中,希望在运行原函数func()之前,输出原函数的名字:

def hello():
    print("call hello function")

def decorator(func):
    def wrapper():
        print(f"before call func name is {func.__name__}")
        return func()
    return wrapper

tmp=decorator(hello)
tmp()

其输出结果如下所示:

before call func name is hello
call hello function

    经过前面的不断学习,相信已经初步掌握了装饰器的原理,如果每次都这样调用是不是太麻烦了,于是Python提供了一种语法糖@装饰函数写在被装饰函数上面即可。如下所示:

def decorator(func):
    def wrapper():
        print(f"before call func name is {func.__name__}")
        return func()
    return wrapper

@decorator
def hello():
    print("call hello function")

hello()

其输出结果如下所示:

before call func name is hello
call hello function

    上面这种写法等价于

tmp=decorator(hello)
tmp()

语法糖 (syntactic sugar):指计算机语言中添加的某种语法,对语言的功能没有影响,但是让程序员更方便地使用。

20.2.3 装饰器知识点

20.2.3.1 多个装饰器

    即装饰器的特性是给函数进行装饰,那是不是一个函数可以被多个函数进行装饰,定义一个函数如下所示:

def hello(name="Surpass"):
    return f"Hello,{name}"

    针对以上这个函数,我们希望能完成以下功能:

    为完成以上功能,我们定义两个装饰器如下所示:

# 装饰器一:
def toUpper(func):
    def wrapper():
        return func().upper()
    return wrapper

# 装饰器二:
def splitStr(func):
    def wrapper():
        return func().split(",")
    return wrapper
@splitStr
@toUpper
def hello(name="Surpass"):
    return f"Hello,{name}"

print(hello())

其输出结果如下所示:

['HELLO', 'SURPASS']

    以下代码等价于:

print(splitStr(toUpper(hello))())

总结一下:一个函数存在多个装饰器,顺序是按照就近原则,即哪个函数靠近被装饰函数,则优先进行装饰,上面示例中@toUpper离被装饰函数hello最近,优先运行,依次类推。

20.2.3.2 传递参数给装饰函数

    装饰函数就是wrapper(),因为要调用原函数func,一旦它有参数,则需要将这些参数传递给wrapper(),示例如下所示:

1.没有参数的wrapper()

def decorator(func):
    def wrapper():
        funcName=func.__name__
        print(f"Befor call {funcName}")
        func()
        print(f"After call {funcName}")
    return wrapper

@decorator
def testFunc():
    print(f"call function is testFunc")

testFunc()

其输出结果如下所示:

Befor call testFunc
call function is testFunc
After call testFunc

2.传固定参数个数的wrapper()

    如果被装饰函数有传入参数,则在装饰函数添加参数即可。

def decorator(func):
    def wrapper(args1,args2):
        funcName=func.__name__
        print(f"Befor call {funcName}")
        func(args1,args2)
        print(f"After call {funcName}")
    return wrapper

@decorator
def testFunc(name,age):
    print(f"Name is {name},age is {str(age)}")

testFunc("Surpass",28)

其输出结果如下所示:

Befor call testFunc
Name is Surpass,age is 28
After call testFunc

3.带返回值的wrapper()

    如果被装饰函数有返回,则在装饰函数中将被装饰函数的结果赋给变量返回即可。

def decorator(func):
    def wrapper(args1,args2):
        funcName=func.__name__
        print(f"Befor call {funcName}")
        result=func(args1,args2)
        print(f"After call {funcName}")
        return result
    return wrapper

@decorator
def testFunc(name,age):
    return f"Name is {name},age is {str(age)}"

print(testFunc("Surpass",28))

其输出结果如下所示:

Befor call testFunc
After call testFunc
Name is Surpass,age is 28

4.无固定参数个数的wrapper()
    在学习Python函数时,如果参数较多或无法确定参数个数,可以使用位置参数(*agrs)也可以使用关键字参数(**kwargs),示例代码如下所示:

def decorator(func):
    def wrapper(*args,**kwargs):
        funcName=func.__name__
        print(f"Befor call {funcName}")
        result=func(*args,**kwargs)
        print(f"After call {funcName}")
        return result
    return wrapper

@decorator
def testFunc(*args,**kwargs):
    sum,dicSum=0,0
    for item in args:
        sum+=item
    for k,v in kwargs.items():
        dicSum+=v
    return sum,dicSum

print(testFunc(1,2,3,4,5,key1=1,key2=2,key3=5,key4=1000))

其输出结果如下所示:

Befor call testFunc
After call testFunc
(15, 1008)
20.2.3.3 functools.wrap

    在装饰器中,装饰之后的函数名称会变乱,如一个函数为hello(),其名称为hello

def hello():
    pass
hello.__name__

其最终的函数名为:

'hello'

    而在装饰器中,在使用@装饰函数名称之后,其名称已经变成wrapper,原因也非常简单,因为我们最终的调用形式都是decorator(被装饰函数),而这个函数返回的是wrapper()函数,因此名称为wrapper。示例代码如下所示:

def decorator(func):
    def wrapper():
        return func()
    return wrapper

@decorator
def testFunc():
    pass

print(testFunc.__name__)

其输出结果如下所示:

wrapper

    这种情况下,多种情况下应该不需要关注,但如果需要根据函数名来进行一些操作(如反射)则会出错。如果仍然希望保持函数的原名,可以使用functools.wraps,示例代码如下所示:

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper():
        return func()
    return wrapper

@decorator
def testFunc():
    pass

print(testFunc.__name__)

其输出结果如下所示:

testFunc

仔细看上面代码,是不是装饰函数又被装饰了一次?

20.2.3.4 传递参数给装饰器

    除了可以传递参数给装饰函数(wapper),也可以传递参数给装饰函数(decorator)。来看看以下示例,在示例中,只有装饰函数有参数,装饰器将数值保留2位小数,如下所示:

def digitalFormat(func):
    def wrapper(*args,**kwargs):
        result=func(*args,**kwargs)
        formatResult=round(result,2)
        return formatResult
    return wrapper


@digitalFormat
def add(a:float,b:float)->float:
    return a+b

print(add(12.09,19.12345))

其输出结果如下所示:

31.21

    通过装饰器,我们很快就达到要求。但有些情况,单纯的数值可能并没有太大意义,需要结合单位。假设上面示例返回为重量,则单位可能为g、kg等。那这种需求有没有解决办法了?示例代码如下所示:

def unit(unit:str)->str:
    def digitalFormat(func):
        def wrapper(*args,**kwargs):
            result=func(*args,**kwargs)
            formatResult=f"{round(result,2)} {unit}"
            return formatResult
        return wrapper
    return digitalFormat


@unit("kg")
def add(a:float,b:float)->float:
    return a+b

print(add(12.09,19.12345))

其输出结果如下所示:

31.21 kg

如果装饰饰器本身需要传递参数,那么再定义一层函数,将装饰的参数传入即可,是不是很简单。

20.2.3.5 类装饰器

1.使用类去装饰函数

    前面实现的装饰器都是针对函数而言,在实际应用中,类也可以作为装饰器。在类装饰器中主要依赖函数call(),每调用一个类的示例时,函数call()就会被执行一次,示例代码如下所示:

class Count:

    def __init__(self,func):
        self.func=func
        self.callCount=0

    def __call__(self, *args, **kwargs):
        self.callCount+=1
        print(f"call count is {self.callCount}")
        return self.func(*args,**kwargs)

@Count
def testSample():
    print("Hello, Surpass")

for i in range(3):
    print(f"first call {testSample()}")

其输出结果如下所示:

call count is 1
Hello, Surpass
first call None
call count is 2
Hello, Surpass
first call None
call count is 3
Hello, Surpass
first call None

2.使用函数去装饰类

def decorator(num):
    def wrapper(cls):
        cls.callCount=num
        return cls
    return wrapper

@decorator(10)
class Count:
    callCount = 0
    def __init__(self):
       pass

    def __call__(self, *args, **kwargs):
        self.callCount+=1
        print(f"call count is {self.callCount}")
        return self.func(*args,**kwargs)


if __name__ == '__main__':
    count=Count()
    print(count.callCount)

其输出结果如下所示:

10

20.4.4 何时使用装饰器

    这个需要据实际的需求而定。比如需要测试每个函数运行时间,此时可以使用装饰器,很轻松就能达到。另外装饰器也可应用到身价认证、日志记录和输入合理性检查等等。

20.4.5 装饰器实际案例

    在实际工作中,装饰器经常会用来记录日志和时间,示例如下所示:

from functools import wraps
import logging
import time
def logType(logType):
    def myLogger(func):
        logging.basicConfig(filename=f"{func.__name__}.log",level=logging.INFO)
        @wraps(func)
        def wrapper(*args,**kwargs):
            logging.info(f" {logType} Run with args:{args} and kwargs is {kwargs}")
            return func(*args,**kwargs)
        return wrapper
    return myLogger

def myTimer(func):
    @wraps(func)
    def wrapper(*args,**kwargs):
        startTime=time.time()
        result=func(*args,**kwargs)
        endTime=time.time()
        print(f"{func.__name__} run time is {endTime-startTime} s ")
        return result
    return wrapper

@logType("Release - ")
@myTimer
def testWrapperA(name:str,color:str)->None:
    time.sleep(5)
    print(f"{testWrapperA.__name__} input paras is {(name,color)}")

@logType("Test - ")
@myTimer
def testWrapperB(name:str,color:str)->None:
    time.sleep(5)
    print(f"{testWrapperB.__name__} input paras is {(name,color)}")


if __name__ == '__main__':
    testWrapperA("apple","red")
    testWrapperB("apple", "red")

其输出结果如下所示:

testWrapperA input paras is ('apple', 'red')
testWrapperA run time is 5.000441789627075 s
testWrapperB input paras is ('apple', 'red')
testWrapperB run time is 5.000349521636963 s

日志记录信息如下所示:

02日志记录信息.png

20.4.6 小结

    装饰器就是接受参数类型为函数或类并返回函数或类的函数,通过装饰器函数,在不改变原函数的基础上添加新的功能。示意图如下所示:

03装饰器示意图.png
上一篇下一篇

猜你喜欢

热点阅读