python高级_day4

python闭包和装饰器

1 魔法方法之__call__

   在python中,函数也是一个对象

f = abs
print(f.__name__)
print(f(-123))

abs
123

   由于 f 可以被调用,所以f 被称为可调用对象。所有的函数都是可调用对象。一个类实例也可以变成一个可调用对象,只需要实现一个特殊方法__call__()。

class Person(object):
    def __init__(self,name,gender):
        self.name = name
        self.gender = gender
        
    def __call__(self,friend):
        print('My name is %s ...'%self.name)
        print('My friend is %s ...'%friend)
        
p = Person('Bob','male')
p('Tim')

My name is Bob ...
My friend is Tim ...

   单看 p('Tim') 你无法确定p是一个函数还是一个类实例,所以,在python中,函数也是对象,对象和函数的区别并不显著。可以把实例对象用类似函数的形式表示,进一步模糊了函数和对象之间的概念

class Fib(object):
    def __init__(self):
        pass
    def __call__(self,num):
        a,b = 0,1
        self.l = []
        
        for i in range(num):
            self.l.append(a)
            a,b = b,a + b
        return self.l
    
    def __str__(self):
        return str(self.l)
    
f = Fib()
print(f(10))

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

   在函数内部再定义一个函数,并且内部函数用到了外部函数作用域里的变量(enclosing),那么将这个内部函数以及用到的外部函数内的变量一起称为闭包(Closure)。装饰器(decorator)接受一个callable对象(可以是函数或者实现了call方法的类)作为参数,并返回一个callable对象,它经常用于有切面需求[1] 的场景,比如:插入日志、性能测试(函数执行时间统计)、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用

2 闭包

   python中函数也是对象,允许把函数本身作为参数传入另一个函数,还可以把函数作为结果值返回。
   在函数内部再定义一个函数,并且内部函数用到了外部函数作用域里的变量(enclosing),那么将这个内部函数以及用到的外部函数内的变量一起称为闭包(Closure):

def line(a,b):
    def get_y_axis(x):
        return a * x + b # 内部函数使用了外部函数的变量a和b
    return get_y_axis # 返回值是闭包函数名,注意不是函数调用,没有小括号

L1 = line(1,1)# 创建一条直线:y = x + 1
print(L1)
print(L1(3))

L2 = line(2,3)# 创建一条直线:y = 2x+3
print(L2)
print(L2(3))

L3 = line(2,3)
print(L3)

print(L2 ==L3) # 每次调用line()返回的都是不同的函数,即使传入相同的参数
print(L2 is L3)

<function line.<locals>.get_y_axis at 0x0000000004DCFBF8>
4
<function line.<locals>.get_y_axis at 0x0000000004DF5158>
9
<function line.<locals>.get_y_axis at 0x0000000004DF5598>
False
False

注意:闭包中不要引用外部变量中任何循环变量或后续会发生变化的值

# 错误的做法
def count():
    fs = []
    for i in range(1,4):
        def f():
            return i*i
        fs.append(f)# 注意:这里不是函数调用,而是将函数传递到列表中
    return fs

f1,f2,f3 = count()
print(f1(),f2(),f3())# f1()、f2()和f3()的输出结果都是9,原因是调用这三个函数时,闭包中引用的外部函数中变量i的值已经变成3

9 9 9

# 正确的做法
def count():
    def f(j):
        return lambda:j*j
    fs = []
    for i in range(1,4):
        fs.append(f(i))
    return fs
f1,f2,f3 = count()
print(f1(),f2(),f3())

1 4 9

3 装饰器

   装饰器(decorator)接受一个callable对象(可以是函数或者实现了call方法的类)作为参数,并返回一个callable对象
   它经常用于有切面需求的场景,比如:插入日志、性能测试(函数执行时间统计)、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
   举个实例,假设你写好了100个Flask中的路由函数,现在要在访问这些路由函数前,先判断用户是否有权限,你不可能去这100个路由函数中都添加一遍权限判断的代码(如果权限判断代码为5行,你得加500行)。那么,你可能想把权限认证的代码抽离为一个函数,然后去那100个路由函数中调用这个权限认证函数,这样只要加100行。但是,根据开放封闭原则,对于已经实现的功能代码建议不能修改,但可以扩展,因为可能你在这些路由函数中直接添加代码会导致原函数出现问题,那么最佳实践是使用装饰器。

3.1 被装饰的函数无参数

   如果原来是调用f1()、f2() ... ,我们只要让用户还是调用f1()、f2() ... ,即他们调用的函数名还是保持不变,但实际执行的函数体代码已经变了(python中函数也是对象,函数名只是变量,可能改变它引用的函数体对象)

def f1():
    print('function f1...')

def f2():
    print('function f2...')
    
f1()
f2()

def login_required(func):
    def inner():  # inner是一个闭包,它使用了外部函数的变量func,即传入的原函数引用f1、f2...
        if func.__name__ == 'f1':  # 这里是权限验证的逻辑判断,此处简化为只能调用f1
            print(func.__name__, ' 权限验证成功')
            func()  # 执行原函数,相当于f1()或f2()...
        else:
            print(func.__name__, ' 权限验证失败')
    return inner
 
new_f1 = login_required(f1)  # 将f1传入装饰器,返回inner引用,并赋值给新的变量new_f1
new_f1()  # 执行函数,即执行inner(),这个闭包中使用的func变量指向原f1函数体

new_f2 = login_required(f2)  # 将f2传入装饰器,返回inner引用,并赋值给新的变量new_f2
new_f2()  # 执行函数,即执行inner(),func变量指向原f2,所以它不会通过权限验证,即不会执行func()

function f1...
function f2...
f1 权限验证成功
function f1...
f2 权限验证失败

   上面使用装饰器有个问题,就是用户原来是调用f1()、f2()... ,现在你让他们调用new_f1()、new_f2()... , 这样肯定不行,所以需要修改如下:

f1 = login_required(f1)  # 将f1引用传入装饰器,此时func指向了原f1函数体。返回inner引用,并赋值给f1,即现在是func指向原函数体,而f1重新指向了返回的inner闭包
f1()  # 执行函数,即执行inner(),这个闭包中使用的func变量指向原f1函数体

   上述两步可以用@Python语法糖简写为:


def login_required(func):
    def inner():  # inner是一个闭包,它使用了外部函数的变量func,即传入的原函数引用f1、f2...
        if func.__name__ == 'f1':  # 这里是权限验证的逻辑判断,此处简化为只能调用f1
            print(func.__name__, ' 权限验证成功')
            func()  # 执行原函数,相当于f1()或f2()...
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

# 1. 定义时
@login_required
def f1():
    print('function f1...')

# 2. 调用时
f1()

f1 权限验证成功
function f1...

3.2 被装饰的函数有参数

def login_required(func):
    def inner():
        if func.__name__ == 'f1':
            print(func.__name__, ' 权限验证成功')
            func()
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

@login_required
def f1(a):
    print('function f1, args: a=', a)

f1(10)

   此时调用f1(10)实际上调用的是inner(10),而装饰器中的闭包inner没有定义参数,所以会报错

# 正确的做法
def login_required(func):
    def inner(a):
        if func.__name__ == 'f1':
            print(func.__name__, ' 权限验证成功')
            func(a)
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

@login_required
def f1(a):
    print('function f1, args: a=', a)

f1(10)

   正确的做法在两个地方都加入了参数,一个是inner()函数的定义中,一个是func()函数调用时。
   现在这个装饰器能正确修饰一个参数的情况,但是遇到多个参数的时候还是需要重新定义,所以我们现在使用python中的*args和**kwargs来匹配任意长度的位置参数或关键字参数

def login_required(func):
    def inner(*args, **kwargs):
        if func.__name__ == 'f1':
            print(func.__name__, ' 权限验证成功')
            func(*args, **kwargs)
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

3.3 被装饰的函数有返回值

   如果使用3.2的装饰器,修改f1()函数定义,它里面有return返回值,将会有问题

def login_required(func):
    def inner(*args, **kwargs):
        if func.__name__ == 'f1':
            print(func.__name__, ' 权限验证成功')
            func(*args, **kwargs)
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

@login_required
def f1(a, b, c):
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

res = f1(10, 20, 30)
print()
print(res)

f1 权限验证成功
function f1, args: a=10, b=20, c=30

None

   原因是调用f1(10, 20, 30),实际是调用inner(10, 20, 30),然后执行inner闭包的函数体,在执行到func(*args, **kwargs)后,没有接收原f1函数体的返回值。当inner闭包执行完毕,Python解释器也没有发现有return语句,就默认返回None
   在inner中接收func函数的返回值,然后return返回它,本示例中装饰器的inner执行完func(*args, **kwargs)后没有其它代码了,所以可以直接修改为return func(*args, **kwargs),如果还有其它逻辑,则用变量保存func的返回值res = func(*args, **kwargs),inner最后一行返回return res

def login_required(func):
    def inner(*args, **kwargs):
        if func.__name__ == 'f1':
            print(func.__name__, ' 权限验证成功')
            return func(*args, **kwargs)
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

@login_required
def f1(a, b, c):
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

res = f1(10, 20, 30)
print(res)

f1 权限验证成功
function f1, args: a=10, b=20, c=30
hello, world

3.4 装饰器带参数

   像Flask的@route('/index')就是带参数的,其实route只是一个函数,它返回真正的装饰器,即在原来的装饰器外面再加一层函数:

def logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print('[日志级别 {}]: 被装饰的函数名是 {}'.format(level, func.__name__))
            return func(*args, **kwargs)
        return wrapper
    return decorator


@logging('DEBUG')  # 等价于 f1 = logging('DEBUG')(f1) ,即先执行loggin('DEBUG'),返回decorator引用(真正的装饰器),再用decorator装饰f1,返回wrapper
def f1(a, b, c):
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'


@logging('INFO')
def f2():
    print('function f2...')


res = f1(10, 20, 30)
print(res)
f2()

[日志级别 DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world
[日志级别 INFO]: 被装饰的函数名是 f2
function f2...

3.5 使用@wraps

   即使上面的做法已经很完美了,但是还是有瑕疵。实际上现在f1已经被装饰器重新赋值了,所以已经不是原来的f1了,当在外部查看函数的名字或者是文档时,就会发现错误

def logging(level='INFO'):
    def decorator(func):
        def wrapper(*args, **kwargs):
            """print log before a function."""
            print('[日志级别 {}]: 被装饰的函数名是 {}'.format(level, func.__name__))
            return func(*args, **kwargs)
        return wrapper
    return decorator

@logging('DEBUG')
def f1(a, b, c):
    """This is f1 function"""
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

res = f1(10, 20, 30)
print(res)
print('错误的函数签名:', f1.__name__)
print('错误的函数文档:', f1.__doc__)

[日志级别 DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world
错误的函数签名: wrapper
错误的函数文档: print log before a function.

   原因是调用f1(10, 20, 30),实际是调用装饰器中的wrapper(),所以打印出来的函数签名和文档都是wrapper的,可以使用functools模块的wraps装饰器解决这个问题:

from functools import wraps

def logging(level='INFO'):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            """print log before a function."""
            print('[日志级别 {}]: 被装饰的函数名是 {}'.format(level, func.__name__))
            return func(*args, **kwargs)
        return wrapper
    return decorator

@logging('DEBUG')
def f1(a, b, c):
    """This is f1 function"""
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

res = f1(10, 20, 30)
print(res)

print('正确的函数签名:', f1.__name__)
print('正确的函数文档:', f1.__doc__

[日志级别 DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world
正确的函数签名: f1
正确的函数文档: This is f1 function

3.6 多个装饰器装饰同一个函数

# 装饰器1
def makeBold(func):
    print('这是加粗装饰器')

    def blod_wrapped():
        print('---1---')
        return '<b>' + func() + '</b>'

    return blod_wrapped


# 装饰器2
def makeItalic(func):
    print('这是斜体装饰器')

    def italic_wrapped():
        print('---2---')
        return '<i>' + func() + '</i>'

    return italic_wrapped


@makeBold
@makeItalic
def test():
    print('---3---')
    return 'Hello, world'

print()
res = test()
print(res)

这是斜体装饰器
这是加粗装饰器

---1---
---2---
---3---
<b><i>Hello, world</i></b>

   此时两个装饰器的情况就相当于下面的简化写法:

test = makeBold(makeItalic(test))

3.7 基于类实现的装饰器

   只要类实现了__call__方法,那么类实例化后的对象就是callable,即拥有了被直接调用的能力

class Test():
    def __call__(self):
        print('call me!')


t = Test()
t()  # 类实例化后的对象可以直接调用,输出:call me!

call me!

   装饰器接受一个callable对象作为参数,并返回一个callable对象,那么我们可以让类的构造函数__init__ ()接受一个函数,然后重载__call__ ()并返回一个函数,也可以达到装饰器函数的效果

class logging(object):
    def __init__(self, func):
        self._func = func

    def __call__(self, *args, **kwargs):
        print('[DEBUG]: 被装饰的函数名是 {}'.format(self._func.__name__))
        return self._func(*args, **kwargs)

@logging
def f1(a, b, c):
    """This is f1 function"""
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

res = f1(10, 20, 30)
print(res)

[DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world

# 带参数的类装饰器
class logging(object):
    def __init__(self, level='INFO'):
        self._level = level

    def __call__(self, func):  # 接受函数
        def wrapper(*args, **kwargs):
            print('[日志级别 {}]: 被装饰的函数名是 {}'.format(self._level, func.__name__))
            return func(*args, **kwargs)
        return wrapper  # 返回闭包

@logging('DEBUG')
def f1(a, b, c):
    """This is f1 function"""
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

res = f1(10, 20, 30)
print(res)

[日志级别 DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world

4 装饰器实例

Python Cookbook

4.1 函数执行时间

import time
from functools import wraps

def timethis(func):
    '''
    Decorator that reports the execution time.
    '''
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end-start)
        return result
    return wrapper

@timethis
def countdown(n):
    '''Counts down'''
    while n > 0:
        n -= 1
    return 'done'

res = countdown(100000)
print(res)

countdown 0.008999824523925781

done

   内置的装饰器比如@staticmethod、@classmethod、@property原理也是一样的

4.2 插入日志

from functools import wraps
import logging

def logged(level, name=None, message=None):
    """
    Add logging to a function. level is the logging
    level, name is the logger name, and message is the
    log message. If name and message aren't specified,
    they default to the function's module and name.
    """
    def decorate(func):
        logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__

        @wraps(func)
        def wrapper(*args, **kwargs):
            log.log(level, logmsg)# logging.log(level, *args, **kwargs)     创建一条严重级别为level的日志记录
            return func(*args, **kwargs)
        return wrapper
    return decorate

# Example use
@logged(logging.DEBUG)
def add(x, y):
    return x + y

@logged(logging.CRITICAL, 'example')
def spam():
    print('Spam!')

print(add(3, 5))
spam()

2020-03-22 22:23:49,259 - <ipython-input-1-690b98a53a19>[line:19] - DEBUG: add
2020-03-22 22:23:49,334 - <ipython-input-1-690b98a53a19>[line:19] - CRITICAL: spam
8
Spam!


  1. AOP (面向切面编程)

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

推荐阅读更多精彩内容

  • 装饰器本质上是一个函数,该函数用来处理其他函数,它可以让其他函数在不需要修改代码的前提下增加额外的功能,装饰器的返...
    胡一巴阅读 416评论 0 0
  • 本节课纲: 魔法方法之_call_ 闭包 装饰器 装饰器实例 一、魔法方法之_call_ 在Python中,函数其...
    郭_扬阅读 420评论 0 0
  • 21世纪了,社会中也没有什么太多的教条,条条框框去束缚男女之间的相爱了。媒体的熏陶让女孩子们都觉得,爱情是需要勇气...
    金三岁呀阅读 508评论 0 3
  • 在女人最好的岁月里,在人生观、价值观形成阶段,我们读的书,见的人,行的路,会影响我们一辈子,岁月的烙印会影...
    璁明的熊孩子阅读 119评论 0 1
  • 你为我花开时, 忘了自己, 我沉浸在你的美丽里, 也忘了自己。 我为你绽放时, 不想我会不会熄灭何时熄灭, 你仰望...
    香芋予香阅读 530评论 0 4