第7章 函数装饰器和闭包
[toc]
装饰器
基本概念
装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理, Web权限校验, Cache等。
很有名的例子,就是咖啡,加糖的咖啡,加牛奶的咖啡。 本质上,还是咖啡,只是在原有的东西上,做了“装饰”,使之附加一些功能或特性。
例如记录日志,需要对某些函数进行记录
笨的办法,每个函数加入代码,如果代码变了,就悲催了。
装饰器的办法,定义一个专门日志记录的装饰器,对需要的函数进行装饰
优点
抽离出大量函数中与函数功能本身无关的雷同代码并继续重用
即,可以将函数“修饰”为完全不同的行为,可以有效的将业务逻辑正交分解,如用于将权限和身份验证从业务中独立出来
概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能
使用
在Python中,装饰器实现是十分方便的
原因是:函数可以被扔来扔去。
函数作为一个对象:
A.可以被赋值给其他变量,可以作为返回值
B.可以被定义在另外一个函数内
def:
装饰器是一个函数,一个用来包装函数的函数,装饰器在函数申明完成的时候被调用,调用之后返回一个修改之后的函数对象,将其重新赋值原来的标识符,并永久丧失对原始函数对象的访问(申明的函数被换成一个被装饰器装饰过后的函数)
当我们对某个方法应用了装饰方法后, 其实就改变了被装饰函数名称所引用的函数代码块入口点,使其重新指向了由装饰方法所返回的函数入口点。
由此我们可以用decorator改变某个原有函数的功能,添加各种操作,或者完全改变原有实现。
装饰器分类
装饰器分为无参数decorator,有参数decorator
- 无参数decorator 生成一个新的装饰器函数
- 有参decorator 有参装饰,装饰函数先处理参数,再生成一个新的装饰器函数,然后对函数进行装饰
装饰器有参/无参,函数有参/无参,组合共4种
无参数装饰器 – 包装无参数函数
def decorator(func):
print("hello")
return func
@decorator
def foo():
print("this is foo")
return "111"
print(foo())
# 等价于
# foo = decorator(foo)
# foo()
无参数装饰器 – 包装带参数函数
def decorator_func_args(func):
def inner(*args, **kwargs): # 处理传入函数的参数
print("begin")
ret = func(*args, **kwargs) # 函数调用
print("end")
return ret
return inner
@decorator_func_args
def foo2(a, b=2):
print(a, b)
return "111"
print(foo2(1))
带参数装饰器 – 包装无参数函数
def decorator_with_params(arg_of_decorator): # 这里是装饰器的参数
print(arg_of_decorator)
# 最终被返回的函数
def newDecorator(func):
print(func)
return func
return newDecorator
@decorator_with_params("deco_args")
def foo3():
print("Hello")
return "111"
print(foo3())
带参数装饰器 – 包装带参数函数
def decorator_whith_params_and_func_args(arg_of_decorator):
def handle_func(func):
def handle_args(*args, **kwargs):
print("begin")
func(*args, **kwargs)
print("end")
print(arg_of_decorator, func, args, kwargs)
return func
return handle_args
return handle_func
@decorator_whith_params_and_func_args("123")
def foo4(a, b=2):
print("Content")
return "111"
print(foo4(1, b=3))
装饰器总结
1.装饰器是用来装饰函数
2.返回的是一个函数对象
3.被修饰函数标识符 指向返回的函数对象
4.语法糖 @deco
几个内置的装饰器
@staticmethod
staticmethod,静态方法在调用时,对类及实例一无所知
仅仅是获取传递过来的参数,没有隐含的第一个参数,在Python里基本上用处不大,你完全可以用一个模块函数替换它
class Test(object):
# 定义类Test的属性
name = 'python'
content = '人生苦短,我用python!'
def instance_method(self):
print("是类{}的实例方法,只能被实例对象调用".format(Test))
@staticmethod # 静态方法,无法访问Test类的属性
def static_method(): # 默认没有参数?能不能给个参数?
print("Call static method ()\n")
# print(self.name, self.content) # 错误调用
# 静态方法调用方法, 可以通过类调用,类的实例调用
Test.static_method()
Test().static_method()
# 类的实例方法只能实例调用
Test().instance_method()
@staticmethod
装饰(静态方法):经常有一些跟类有关系的功能但在运行时又不需要实例和类参与的情况下需要用到静态方法。
比如更改环境变量或者修改其他类的属性等能用到静态方法
例子:
class Test:
def __init__(self, num1, num2):
self.num1 = num1
self.num2 = num2
def plus(self):
result = self.num1 + self.num2
return result
def multiply(self):
result = self.num1 * self.num2
return result
@staticmethod
def sum(num1, num2):
s = num1 * num1 + num2 * num2
print(s)
Test.sum(3, 4)
IND = 'ON'
class Kls(object):
def __init__(self, data):
self.data = data
@staticmethod
def checkind():
return IND == 'ON'
def do_reset(self):
if self.checkind():
print('Reset done for: %s' % self.data)
def set_db(self):
if self.checkind():
print('DB connection made for: %s' % self.data)
ik1 = Kls(24)
ik1.do_reset()
ik1.set_db()
class Foo(object):
X = 1
Y = 2
@staticmethod
def averag(*mixes):
return sum(mixes) / len(mixes)
@staticmethod
def static_method():
return Foo.averag(Foo.X, Foo.Y)
foo = Foo()
# 子类的实例继承了父类的static_method静态方法,
# 调用该方法,还是调用的父类的方法和类属性。
print(foo.static_method())
总结:
静态方法:staticmethod修饰,无需参数,类与实例共享。
无法访问类属性、实例属性,相当于一个相对独立的方法,
跟类其实没什么关系,换个角度来讲,其实就是放在一个类的作用域里的函数而已。
静态方法中引用类属性的话,必须通过类对象来引用。
@classmethod
@classmethod
是一个函数修饰符,它表示接下来的是一个类方法,类方法的第一个参数cls
class Test(object):
# 定义类Test的属性
name = 'python'
content = '人生苦短,我用python!'
@classmethod # 类方法访问Test类的属性
def class_method(cls):
# 参数cls必须要有
print(cls.name, cls.content)
class Son(Test):
content = "你好"
name = "世界"
# 调用方法
Test.class_method()
Test().class_method()
# 类方法有类变量cls传入,从而可以用cls做一些相关的处理。
# 并且有子类继承时,调用该类方法时,传入的类变量cls是子类,而非父类。
# 调用的是子类的方法和子类的类属性
print("*" * 10)
Son.class_method()
Son().class_method()
普通方法、类方法和静态方法的比较
普通方法:由对象调用。至少一个self参数,执行普通方法时,
自动将调用该方法的对象赋值给self类方法:由类调用; 至少一个cls参数;执行类方法时,自动将调用该方法的类复制给cls
静态方法:由类调用;无默认参数
class Foo(object):
# def __init__(self, name):
# self.name = name
def ord_func(self):
""" 定义普通方法,至少有一个self参数 """
# print self.name
print('普通方法')
@classmethod
def class_func(cls):
""" 定义类方法,至少有一个cls参数 """
print('类方法')
@staticmethod
def static_func():
""" 定义静态方法 ,无默认参数"""
print('静态方法')
# 调用普通方法
f = Foo()
f.ord_func()
# 调用类方法
Foo.class_func()
f.class_func()
# 调用静态方法
Foo.static_func()
f.static_func()
相同点:对于所有的方法而言,均属于类(非对象)中,所以,在内存中也只保存一份。
不同点:方法调用者不同、调用方法时自动传入的参数不同。
使用普通方法、类方法和静态方法都可以通过对象(t)进行调用,但是静态方法和类方法无法访问对象的属性,所以更改对象(t)的属性仅仅只是对普通方法起作用
class Test(object):
# 定义类Test的属性
name = 'python'
content = '人生苦短,我用python!'
def normal_method(self):
print(self.content)
@classmethod
def class_method(cls):
print(cls.content)
@staticmethod
def static_method():
print('content')
t = Test()
t.content = '人生苦短,及时行乐' # 设置对象t的属性
t.normal_method() # 人生苦短,及时行乐
t.class_method() # 人生苦短,我用python!
t.static_method() # content
class P(object):
tip = 'Persion tips' # 类属性,类及类成员共享
@classmethod
def clsFunc(cls):
"""类方法共享"""
return 'Welcome:' + cls.tip
def insFunc(self):
"""实例方法"""
return self.__class__
@staticmethod
def staFunc():
"""静态方法"""
return P.tip
p = P()
print(P.tip)
print(p.tip)
print(p.insFunc())
p.tip = "Hello"
print(p.clsFunc())
print(p.staFunc())
print(P.clsFunc())
print(P.staFunc())
总结:
三种方法都可以通过对象进行调用,但类方法和静态方法无法访问对象属性,类方法通过对象调用获取的仍是类属性(并非对象属性);
普通方法无法通过类名调用,类方法和静态方法可以,但静态方法不能进行访问,仅仅只是通过传值得方式(与函数调用相同)
@property
访问类或实例的属性时是直接通过obj.XXX的形式访问,但property是一个特性的属性
访问添加了装饰器@property的方法时无须额外添加()来调用,而是以属性的形式来访问。
所以,利用这个特性可以虚拟实现类属性的只读设置。
class Student(object):
# 上面的birth是可读写属性,而age就是一个只读属性
# 因为age可以根据birth和当前时间计算出来。
@property # 相当于property.getter(birth) 或者property(birth)
def birth(self):
return self._birth
@birth.setter # 相当于birth = property.setter(birth)
def birth(self, value):
self._birth = value
@property
def age(self):
return 2019 - self._birth
student = Student()
student.birth = 1987
print(student.birth)
print(student.age)
这里有个问题,为什么property属性只能类的实例调用?直接使用类调用可以不可以?
class Test(object):
@property
def p(self):
return "Hello World"
# 类调用
print(Test.p) # <property object at 0x0000026EBE999AE8>
# 实例调用
test = Test()
print(test.p) # Hello World
print(Test.p.__get__(test)) # Hello World
print(test.p()) # TypeError: 'str' object is not callable
这里要看下什么是python描述符
Test.p 返回的是一个property,它的属性是描述符.
它是一个类,用很多魔法方法,比如__get__,__set__...
当property被类的实例调用时候,就会触发其中的
__get__
方法.
所以可以通过类调用property,也可以使用类的实例调用property
最后在重复一下@property的含义:
它是一个装饰器,作用是把类中的方法变成属性来进行调用。
变量作用域
主要有下面几种作用域
L: Local 函数内部作用域 -局部名字空间中的名字
E: Enclosing 封闭作用域-函数内部与内嵌函数之间
G: Global全局作用域 -内置和全局命名空间中的名字都属于全局作用域
B: build-in 内置作用域
顺序: L>E>G>B
下面分别介绍这几个作用域
Local作用域
# 这里所有的变量都在 local 作用域
def foo(a, b):
c = 1
d = a + b + c
return d
Enclosing作用域
i = 0
def a():
i = 1
# 对函数b来说就是Enclosing 封闭作用域
def b():
i = 2
b()
print(i)
a() # 1
关键字nolocal
修改Enclosing变量的问题,就需要使用nonlocal关键字。
还是刚才的例子,增加一个nonlocal:
i = 0
def a():
i = 1
# 对函数b来说就是Enclosing 封闭作用域
def b():
nonlocal i
i = 2
b()
print(i)
a() # 2
注意两点:
nonlocal不能代替global。 如果在上述代码的函数a中,就只能使用global。
因为,外层就是Global作用域了。在多重嵌套中,nonlocal只会上溯一层; 而如果上一层没有,则会继续上溯。
举个例子来说明:
def a():
i = 1
def b():
# i = 2
def c():
nonlocal i
i = 3
c()
print('b:', i)
b()
print('a:', i)
a()
返回结果:
b: 3
a: 3
把上面的注释打开,对比这个:
def a():
i = 1
def b():
i = 2
def c():
nonlocal i
i = 3
c()
print('b:', i)
b()
print('a:', i)
a()
返回结果:
b: 3
a: 1
Global作用域
看一个例子:
i = 0
def a():
i = 1
print('local:', i)
a()
print('global:', i)
返回结果:
local: 1
global: 0
如果在一个局部(函数)内声明一个global变量,
那么这个变量在局部的所有操作将对全局的变量有效
i = 0
def a():
global i
i = 1
print('local:', i)
a()
print('global:', i)
返回结果:
local: 1
global: 1
i = 0
def a():
i = 1
print('local:', i)
# 可以查看局部作用域
print(locals())
# 查看全局作用域
print(globals())
a()
print('global:', i)
print("*" * 10)
print(locals())
print(globals())
记住两点:
global永远打印全局的名字
locals 输出什么 根据locals所在位置
build-in作用域
Python内置模块的名字空间
def list_length(a):
return len(a)
a = [1, 2, 3]
print(list_length(a)) # 3
# python3 写法
import builtins
print(builtins.len) # <built-in function len>
python命名空间规则
python使用命名空间记录变量
python中的命名空间就像是一个dict,key是变量的名字,value是变量的值。
python中,每个函数都有一个自己的命名空间,叫做local namespace,它记录了函数的变量。
python中,每个module有一个自己的命名空间,叫做global namespace,它记录了module的变量,包括 functions, classes 和其它imported modules,还有 module级别的 变量和常量。
还有一个build-in 命名空间,可以被任意模块访问,这个build-in命名空间中包含了build-in function 和 exceptions。
当python中的某段代码要访问一个变量x时,python会在所有的命名空间中寻找这个变量,查找的顺序为:
local namespace - 指的是当前函数或者当前类方法。
如果在当前函数中找到了变量,停止搜索global namespace - 指的是当前的模块。如果在当前模块中找到了变量,停止搜索
build-in namespace - 如果在之前两个namespace中都找不到变量x,python会假设x是build-in的函数或者变量。
如果x不是内置函数或者变量,python会报错NameError。
对于闭包来说,这里有一点区别,如果在local namespace中找不到变量的话,还会去父函数的local namespace中找变量
直接看例子:
len = len([])
print("befoe global len=", len)
def a():
len = 1
def b():
len = 2
def c():
len = 3
print('locals len:', len)
c()
print('enclosing function locals len:', len)
b()
print("after global len=", len)
a()
运行结果:
befoe global len= 0
locals len: 3
enclosing function locals len: 2
after global len= 1
- from module import 和 import module
使用import module时,module本身被引入,但是保存它原有的命名空间,所以我们需要使用module.name这种方式访问它的 函数和变量。
from module import这种方式,是将其它模块的函数或者变量引到当前的命名空间中
所以就不需要使用module.name这种方式访问其它的模块的方法了。
- if
__name__
trick
python中的module也是对象,所有的modules都有一个内置的属性__name__
,
模块的__name__
属性的值取决于如何使用这个模块,
如果import module,那么__name__
属性的值是模块的名字
如果直接执行这个模块的话,那么__name__
属性的值就是默认值__main_
_。
可变数据类型和不可变数据类型
- 可变数据类型:列表list和字典dict
允许变量的值发生变化,即如果对变量进行append、+=等这种操作后,只是改变了变量的值,而不会新建一个对象,变量引用的对象的地址也不会变化
不过对于相同的值的不同对象,在内存中则会存在不同的对象,即每个对象都有自己的地址,相当于内存中对于同值的对象保存了多份,这里不存在引用计数,是实实在在的对象。
a = [1, 2]
b = [1, 2]
print("ID a=", id(a)) # ID a= 1236557349896
print("ID b=", id(b)) # ID b= 1236556806216
print(a == b) # 判断值是否相等 True
print(a is b) # 判断地址是否一样 False
- 不可变数据类型:数值型、字符串型string和元组tuple
不允许变量的值发生变化,如果改变了变量的值,相当于是新建了一个对象,而对于相同的值的对象,在内存中则只有一个对象(一个地址)
a = 3
b = 3
print("ID a=", id(a)) # ID a= 1570991200
print("ID b=", id(b)) # ID b= 1570991200
print(a == b) # 判断值是否相等 True
print(a is b) # 判断地址是否一样 True
再看一个例子:
# 列表是可变的
a = [1, 2, 3, 4]
a[0] = 100
a.insert(3, 300)
print(a)
# 字符串是不可变的
s = "Hello world"
# s[0] = "z"
# print(s) # TypeError: 'str' object does not support item assignment
# 字符串replace方法只是返回一个新字符串,并不改变原来的值
print(s.replace('world', 'Mars'))
print(s)
# 如果想改变字符串的值,可以用重新赋值的方法
s = s.replace('world', 'Mars')
print(s)
# 或者使用bytearray代替字符串
c = bytearray('abcde', 'utf-8')
c[1:3] = b'12'
print(c) # bytearray(b'a12de')
- 总结:
可变数据类型:
list, dictionary, set, numpy array, user defined objects
不可变数据类型
integer, float, long, complex, string, tuple, frozenset
- 字符串不可变的原因
1.列表可以通过以下的方法改变,而字符串不支持这样的变化
a = [1, 2, 3, 4]
# 此时a 和 b 指向同一块区域,改变 b 的值,a 也会同时改变
b = a
print("ID a=", id(a))
print("ID b=", id(b))
b[0] = 100
print(a) # [100, 2, 3, 4]
print(b) # [100, 2, 3, 4]
2.`字符串与整数浮点数一样被认为是基本类型,而基本类型在Python中是不可变的。
闭包
什么是闭包
一个闭包就是你调用了一个函数A,这个函数A返回了一个函数B给你。
这个返回的函数B就叫做闭包。你在调用函数A的时候传递的参数就是自由变量。
也就是说闭包是嵌套函数,内部函数调用外部函数的变量
例子:
def outer():
a = 1
def inner():
print(a)
print(inner.__closure__)
return inner
inn = outer()
inn()
返回结果:
1
(<cell at 0x000001E6396FD078: int object at 0x000000005DA36C20>, <cell at 0x000001E6396FD0A8: function object at 0x000001E6396FBC80>)
这里的返回值 两个cell的意思是:内部函数引用了外部函数的变量, int和func类型
这里inner函数就是一个闭包,并且拥有该闭包持有的自由变量a
总结创建一个闭包必须满足以下几点:
1.必须有一个内嵌函数
2.内嵌函数必须引用外部函数中的变量
3.外部函数的返回值必须是内嵌函数
为什么要使用闭包
两个特点:
- 封装
- 代码复用
def set_passline(passline):
def cmp(val):
if val >= passline:
print("pass")
else:
print("failed")
return cmp
f_100 = set_passline(60)
f_100(89)
f_100(59)
怎么使用闭包
- 在python中很重要也很常见的一个使用场景就是装饰器
装饰器是特殊的闭包形式
def decorator_func(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator_func
def func(name):
print('my name is', name)
# 等价于
# decorator_func(func)
func("tony")
- 惰性求值
惰性求值属性可以在对象被使用的时候才进行计算,这样可以减少一些资源消耗,提高程序效率
def out_func(a):
def inner_func():
return a
return inner_func
func = out_func(1)
print(func()) # 1
- 需要对某个函数的参数提前赋值的情况
当然在Python中已经有了很好的解决访问 functools.parial,但是用闭包也能实现
def partial(**outer_kwargs):
def wrapper(func):
def inner(*args, **kwargs):
for k, v in outer_kwargs.items():
kwargs[k] = v
return func(*args, **kwargs)
return inner
return wrapper
@partial(age=15)
def say(name=None, age=None):
print(name, age)
say(name="tony")
# 当然用functools比这个简单多了
# import functools
# functools.partial(say, age=15)(name='tony')
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。