Python闭包与装饰器

1. 内嵌函数

首先,明确 Python 的内嵌函数,也就是函数内定义新的函数:

def outside():
    print("正在调用outside")
    def inside():
        print("正在调用inside")
    inside()

outside()
inside()  #error

2. 闭包(closure)

大白话说,闭包就是“有权访问另一个函数作用域变量的函数都是闭包”。

def line_creater(a, b):
    def line(x):
        return a*x + b
    return line

model = line_creater(5, 10)
x = 23
y = model(x)  # 求解函数y值

2.1. 内嵌函数同名变量(局部变量)

如果你尝试直接修改外部作用域的局部变量

def foo():
    m = 0
    def foo1():
        m = 1  # 直接定义,则创建一个局部变量
        print(m)  # 输出:1

    print(m)  # 输出:0
    foo1()
    print(m)  # 输出:0

foo()  # 输出:0, 1, 0

是不行的……在foo1中,m是新创建的一个局部变量。

2.2. nonlocal 关键字

如果有你尝试在闭包内改写外部变量,会得到异常:

def outer():
    name = "abcd"
    print("outer name id = ", id(name))

    def inner():
        print(">> 正常运行")
        print(name)  # Error【运行时时语法错误】
        print("inner name id = ", id(name))

        name = "ABCD"  # 因为这里定义了局部变量,所以上面的print()报错
        print(name)
        print("inner name id change to ", id(name))
    return inner

func_inner = outer()
func_inner()

UnboundLocalError: local variable 'name' referenced before assignment

说明Python并不允许改写(如果 x 存在)——追踪显示,正常的闭包,返回 inter() 包含有外层函数的属性项name。而如果上述代码中的inner执行进入inner时,并没有name的local变量。【不过我没想明白,Python动态记录function-symbol时,怎么会了解并区分函数内部情况的呢?!】——说明 outer() 执行时,python会导入全部的闭包函数体inner()来分析其的局部变量,而不仅仅是记录symbol。

解决方案:向闭包函数传递外部的局部变量,使用 nonlocal name 在inner() 中声明name对象。

2.3. list.append(func)for 循环传递的 param_i

在list中append函数,并给函数传递(往往由for循环得到的)“不同的”参数:

list_func = []
for i in range(3):
    def inner(x):
        print(x*i)  # 注意这里的i,不是局部变量!故函数定义里没有i的实际值!
    list_func.append(inner)

for func in list_func:
    func(10)

这个问题经常跟闭包联系在一起,但实际上与闭包无关~

问题出在:函数在append时,只是append了函数的symbol,而非函数定义;函数直到调用时才获取i值,此时的i已经循环到了range的末位!

解决方案:在定义inner时:def inner(x, i=i) 将外部的i值赋给局部变量 i(此 i 非彼 i )。这样就让 i 以局部变量的形式(会执行复制)并存储于函数体内。

或者使用 functools.partial 而不是lambda或def定义函数。

或者使用生成器代替list:

def gen_func():
    for i in range(3):
        yield lambda x: print(i * x)

for func in gen_func():
    func(10)

2.4. 闭包的作用

闭包避免了使用全局变量,此外,闭包允许将函数与其所操作的某些数据(环境)关连起来。这一点与面向对象编程是非常类似的,在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

一般来说,当对象中只有一个方法时,这时使用闭包是更好的选择。来看一个例子:

def adder(x):
    def wrapper(y):
        return x + y
    return wrapper

adder5 = adder(5)
adder5(10)  # 输出 15
adder5(6)   # 输出 11

这比用类来实现更优雅!

1、实现封装(以JavaScripts为例 ,Python一般没必要用闭包实现封装):

var person = function(){
    //变量作用域为函数内部,外部无法访问,不会与外部变量发生重名冲突
    var name = "default";

    return {
      //管理私有变量
       getName : function(){
           return name;
       },
       setName : function(newName){
           name = newName;
       }
    }
}();

2、实现装饰器

2.5. 闭包的缺点

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包。

——不过,个人认为这也不是什么缺点(如果你利用闭包实现封装,跟class对比,class也同样需要空间存储变量)。

3. 装饰器

详解Python的装饰器

Python装饰器的另类用法

装饰器里的那些坑

装饰器,虽然不同于GOF23设计模式的实现方式,但目的与结果相同——动态添加功能:

  • GOF23的装饰模式,对class添加功能;
  • Python @装饰器:对function添加功能,并保持原函数名不变

3.1. 装饰器的原型

  • 本质上,是通过一个 wrapper() 函数,对原函数进行一层的包装。
  • 闭包,只是针对function提供的一种非常合适的封装方法。
def decorator(func):
    def wrapper():
        print "[DEBUG]: enter {}()".format(func.__name__)
        return func()
    return wrapper

def say_hello():
    print "hello!"

say_hello = decorator(say_hello)  # 添加功能并保持原函数名不变

@语法糖

def decorator(func):
    def wrapper():
        print "[DEBUG]: enter {}()".format(func.__name__)
        return func()
    return wrapper

@decorator
def say_hello():
    print "hello!"

3.1.1. 思考

关于添加的功能,是不是也可以放在 decorator 的函数体中?

def decorator(func):
    print( "[DEBUG]: enter {}()".format(func.__name__))
    def wrapper():
        return func()
    return func

@decorator
def say_hello():
    print("hello!...")

# 其实这个模式是错误的,try this:
say_hello()
say_hello()  # 第二次调用时,“忽略”了[DEBUG]打印

那么,这个跟标准的方式有何区别?

继续思考,为什么需要 wrapper() 闭包?去掉内嵌函数可以吗?

def decorator(func):
    print( "[DEBUG]: enter {}()".format(func.__name__))
    return func  # 其实是装饰模式的变形: return lambda func: func

    # 或者,原始函数被完全忽略...
    return lambda: print("你甚至可以完全替换掉原始函数")

@decorator
def say_hello():
    print("hello!...")

答案:不能去掉 wrapper()(lambda的本质也是func_wrapper)。因为装饰器 @decorator 的设计模式必须要返回一个function。

总结,装饰器的设计要点:

  • 必须在 def decorator() 中定义一个闭包 wrapper() ,并 return wrapper
  • 新增加的功能应该放在 wrapper() 内部,而不是在 decorator()

3.2. 多层的装饰器

def w1(func):
    def inner():
        print("---正在装饰1----")
        func()
    return inner

def w2(func):
    def inner():
        print("---正在装饰2----")
        func()
    return inner

@w1
@w2
def f1():
    print("---f1---")

>>>---正在装饰2----
   ---正在装饰1----

3.3. 有参函数应用装饰器

def decorator(func):
    def wrapper(*args, **kwargs):  # 传入任意参数
        print "[DEBUG]: enter {}()".format(func.__name__)
        print 'Prepare and say...',
        return func(*args, **kwargs)
    return wrapper  # 返回

@decorator
def say(something):
    print "hello {}!".format(something)

3.4. 含参的装饰器

def logging(level):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            print "[{level}]: enter function {func}()".format(
                level=level,
                func=func.__name__)
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper

@logging(level='INFO')
def say(something):
    print "say {}!".format(something)

# 如果没有使用@语法,等同于
# say = logging(level='INFO')(say)

@logging(level='DEBUG')
def do(something):
    print "do {}...".format(something)

if __name__ == '__main__':
    say('hello')
    do("my work")

3.5. 适用于obj.method()的装饰器

如下示例代码,定义为方法的装饰器函数无需定义self变量,而是直接传入装饰的对象(method)。而在调用 obj.method() 时,实际上会先进入装饰器函数。示例中的装饰器无需传入参数,而实际执行函数的args通过wrapper()的参数传入。

class GitRepo:
    def __init__(self, repo_dir):
        self.repo_dir = repo_dir

    def switch_dir(func):
        """ 装饰器 """
        def wrapper(self, *args, **kwargs):
            def inner_wrapper():
                cwd = os.path.abspath(os.path.curdir)
                os.chdir(self.repo_dir)
                ret = func(self, *args, **kwargs)
                os.chdir(cwd)
                return ret
            return inner_wrapper()
        return wrapper

    @switch_dir
    def status(self, type_=None):
        list_lines = run_cmd("git status -s")
        if type_:
            list_files = self._filter_status(list_lines, type_)
            return list_files
        else:
            return list_lines

3.6. 基于类实现的装饰器

装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重载了__call__()方法,那么这个对象就是callable的。

回到装饰器上的概念上来,装饰器要求接受一个callable对象,并返回一个callable对象(不太严谨,详见后文)。那么用类来实现也是也可以的。我们可以让类的构造函数 init() 接受一个函数,然后重载 call() 并返回一个函数,也可以达到装饰器函数的效果。

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

    def __call__(self, *args, **kwargs):
        print("[DEBUG]: Function {func}()".format(func=self.func.__name__))
        return self.func(*args, **kwargs)

@logging
def say(something):
    print("say {}!".format(something))

say("abc")

3.6.1. 带参数的类装饰器

class logging:
    def __init__(self, level='DEBUG'):
        self.level = level

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print("[{level}]: enter function {func}()".format(
                    level=self.level,
                    func=func.__name__))
            func(*args, **kwargs)
        return wrapper

@logging(level='INFO')  # 如缺省,必须加括号:@logging()
def say(something):
    print("say {}!".format(something))

say("abc")

4. 内置的装饰器

4.1. @property

@property 广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

4.1.1. 常规使用

属性有三个装饰器:setter, getter, deleter ,都是在property()的基础上做了一些封装,因为setter和deleter是property()的第二和第三个参数,不能直接套用@语法。getter装饰器和不带getter的属性装饰器效果是一样的,但如果定义了setter,则该属性则定义为只读。经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。

举例:

s = Student()
s.score = 9999

这显然不合逻辑。为了限制score的范围,可以通过一个set_score()方法来设置成绩,再通过一个get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数:

class Student(object):

    def get_score(self):
        return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

但是,上面的调用方法又略显复杂,没有直接用属性这么直接简单。

有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?对于追求完美的Python程序员来说,这是必须要做到的!

还记得装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的 @property 装饰器就是负责把一个方法变成属性调用的:

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

@property 的实现比较复杂,我们先考察如何使用。把一个 getter 方法变成属性,只需要加上 @property 就可以了,此时,@property 本身又创建了另一个装饰器 @score.setter ,负责把一个 setter 方法变成属性赋值,于是,我们就拥有一个可控的属性操作:

>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!

注意到这个神奇的@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法来实现的。

4.1.2. 设置只读

还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2014 - self._birth

上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。

4.1.3. property() 函数

class Handler(Filterer):
    def __init__(self, level=NOTSET):
        Filterer.__init__(self)
        self._name = None
        pass

    def get_name(self):
        return self._name

    def set_name(self, name):
        _acquireLock()
        try:
            if self._name in _handlers:
                del _handlers[self._name]
            self._name = name
            if name:
                _handlers[name] = self
        finally:
            _releaseLock()

    name = property(get_name, set_name)  # 而不是:@name.setter

可以直接使用property()设定属性的输入、输出,而不是使用哪个蹩脚的 @name.setter

4.2. @staticmethod , @classmethod

@staticmethod 返回的是一个 staticmethod 类对象,而 @classmethod 返回的是一个 classmethod 类对象。他们都是调用的是各自的__init__()构造函数。

class classmethod(object):
    """
    classmethod(function) -> method
    """
    def __init__(self, function): # for @classmethod decorator
        pass
        # ...

class staticmethod(object):
    """
    staticmethod(function) -> method
    """
    def __init__(self, function): # for @staticmethod decorator
        pass
        # ...

装饰器的@语法就等同调用了这两个类的构造函数。

class Foo(object):

    @staticmethod
    def bar():
        pass
    # 等同于 bar = staticmethod(bar)

至此,我们上文提到的装饰器接口定义可以更加明确一些,装饰器必须接受一个callable对象,其实它并不关心你返回什么,可以是另外一个callable对象(大部分情况),也可以是其他类对象,比如property。

原文地址:https://www.cnblogs.com/brt2/p/13152202.html