学习函数
Python官方中文教程 5
转自 Python官方中文教程
1. 普通函数创建与调用
函数是一种仅在调用时运行的代码块。您可以将数据(称为参数)传递到函数中,然后由函数可以把数据作为结果返回。
如果将函数比喻成蛋糕店的话,那么函数的参数就是生产蛋糕的原材料,而函数的返回值就是蛋糕成品。
1.1 函数的创建
在 Python 中,使用 def
关键字定义函数
def 函数名(参数):
# 内部代码
return 表达式
举个例子,我这边手动实现一个计算两个数平均值的函数,这边这样子写
def get_average(a, b):
'''
计算平均值
'''
result = (a + b)/2
return result
在定义函数的过程中,需要注意以下几点:
函数代码块以def
关键词开头,一个空格
之后接函数标识符名称
和圆括号()
,再接个冒号。
任何传入的参数必须放在圆括号中间。
函数的第一行语句后可以选择性地使用文档字符串—用于存放函数说明。
函数内容以:
起始,并且缩进
。
使用return
,返回值给调用者,并结束函数。return
关键并不是必须要加,可根据实际需要决定是否要写,若不写的话,默认返回None
。
return
语句依然在函数体内部,不能回退缩进
。直到函数的所有代码写完,才回退缩进
,表示函数体结束。
1.2 函数的调用
函数编写出来就是给人调用的。要调用一个函数,必须使用函数名
后跟()
的方式才能调用函数。
调用的同时要根据函数的定义体,提供相应个数和类型的参数,每个参数之间用,
分隔。
def get_average(a, b):
'''
计算平均值
'''
result = (a + b)/2
return result
average = get_average(2, 6)
print(average) # output: 4
1.3 函数的返回
函数的返回值
,可以是多种多样的,非常灵活:
可以是任意类型的对象
,比如字符串
,数值
,列表
,字典
等等
def demo_func():
return 10
可以是一个表达式
,函数会直接运行表达式,然后返回
def get_average(a, b):
return (a + b)/2
可以是函数本身,利用这点可以实现递归
调用。
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
另外还可以返回多个值
def demo_func():
return 1,2.3
可以是其他函数
,利用这点可以实现装饰器
。
这部分属于进阶内容,感兴趣可查看另一章节内容:10 装饰器的六种写法
def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper
2. 11个案例讲解函数参数
2.1 参数分类
函数,在定义的时候,可以有参数的,也可以没有参数。
从函数定义的角度来看,参数可以分为两种:
必选参数
:调用函数时必须要指定的参数,在定义时没有等号可选参数
:也叫默认参数,调用函数时可以指定也可以不指定,不指定就默认的参数值来。
例如下面的代码中,a
和b
属于必选参数,c
和 d
属于可选参数
def func(a,b,c=0, d=1):
pass
从函数调用的角度来看,参数可以分为两种:
关键字参数
:调用时,使用key=value
形式传参的,这样传递参数就可以不按定义顺序来。位置参数
:调用时,不使用关键字参数的key-value
形式传参,这样传参要注意按照函数定义时参数的顺序来。
def func(a,b,c=0, d=1):
pass
# 关键字参数传参方法
func(a=10, c=30, b=20, d=40)
# 位置参数传参方法
func(10, 20, 30, 40)
最后还有一种非常特殊的参数,叫做可变参数
。
意思是参数个数可变,可以是 0
个或者任意个
,但是传参时不能指定参数名,通常使用*args
和 **kw
来表示:
1.*args
:按照位置接收传递进来的所有参数,是一个元组类型
2. **kw
:按照关键字接收传递进来的所有参数,是一个字典类型
def func(*args, **kw):
print(args)
print(kw)
func(10, 20, c=20, d=40)
输出如下
(10, 20)
{'c': 20, 'd': 40}
2.2 十一个案例
案例一:在下面这个函数中,a
是必选参数
,是必须要指定的
>>> def demo_func(a):
... print(a)
...
>>> demo_func(10)
10
>>> demo_func() # 不指定会报错
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: demo_func() missing 1 required positional argument: 'a'
案例二:在下面这个函数中,b
是可选参数
(默认参数),可以指定也可以不指定,不指定的话,默认为10
>>> def demo_func(b=10):
... print(b)
...
>>> demo_func(20)
20
>>> demo_func()
10
案例三:在下面这个函数中, name
和 age
都是必选参数,在调用指定参数时,如果不使用关键字参数
方式传参,需要注意顺序
>>> def print_profile(name, age):
... return f"我的名字叫{name},今年{age}岁了"
...
>>> print_profile("王炳明", 27)
'我的名字叫王炳明,今年27岁了'
如果参数太多,你不想太花精力去注意顺序,可以使用关键字参数
方式传参,在指定参数时附上参数名,比如这样:
>>> print_profile(age=27, name="王炳明")
'我的名字叫王炳明,今年27岁了'
案例四:在下面这个函数中,args
参数和上面的参数名不太一样,在它前面有一个 *,这就表明了它是一个可变参数,可以接收任意个数的不指定参数名的参数。
>>> def demo_func(*args):
... print(args)
...
>>>
>>> demo_func(10, 20, 30)
(10, 20, 30)
案例五:在下面这个函数中,kw
参数和上面的 *args
还多了一个 *
,总共两个 **
,这个意思是kw
是一个可变关键字参数
,可以接收任意个数的带参数名的参数。
>>> def demo_func(**kw):
... print(kw)
...
>>> demo_func(a=10, b=20, c=30)
{'a': 10, 'b': 20, 'c': 30}
案例六:在定义时,必选参数
一定要在可选参数
的前面,不然运行时会报错
>>> def demo_func(a=1, b):
... print(a, b)
...
File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
>>>
>>> def demo_func(a, b=1):
... print(a, b)
...
>>>
案例七:在定义时,可变位置参数
一定要在可变关键字参数
前面,不然运行时也会报错
>>> def demo_func(**kw, *args):
File "<stdin>", line 1
def demo_func(**kw, *args):
^
SyntaxError: invalid syntax
>>>
>>> def demo_func(*args, **kw):
... print(args, kw)
...
>>>
案例八:可变位置参数
可以放在必选参数
前面,但是在调用时,必选参数
必须要指定参数名
来传入,否则会报错
>>> def demo_func(*args, b):
... print(args)
... print(b)
...
>>> demo_func(1, 2, 100)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: demo_func() missing 1 required keyword-only argument: 'b'
>>>
>>> demo_func(1, 2, b=100)
(1, 2)
100
案例九:可变关键字参数
则不一样,可变关键字参数
一定得放在最后,下面三个示例中,不管关键字参数
后面接位置参数
,还是默认参数
,还是可变参数
,都会报错。
>>> def demo_func(**kw, a):
File "<stdin>", line 1
def demo_func(**kw, a):
^
SyntaxError: invalid syntax
>>>
>>> def demo_func(**kw, a=1):
File "<stdin>", line 1
def demo_func(**kw, a=1):
^
SyntaxError: invalid syntax
>>>
>>> def demo_func(**kw, *args):
File "<stdin>", line 1
def demo_func(**kw, *args):
^
SyntaxError: invalid syntax
案例十:将上面的知识点串起来,四种参数类型可以在一个函数中出现,但一定要注意顺序
def demo_func(arg1, arg2=10, *args, **kw):
print("arg1: ", arg1)
print("arg2: ", arg2)
print("args: ", args)
print("kw: ", kw)
试着调用这个函数,输出如下:
>>> demo_func(1,12, 100, 200, d=1000, e=2000)
arg1: 1
arg2: 12
args: (100, 200)
kw: {'d': 1000, 'e': 2000}
案例十一:使用单独的*
,当你在给后面的位置参数传递时,对你传参的方式有严格要求,你在传参时必须要以关键字参数
的方式传参数,要写参数名,不然会报错。
>>> def demo_func(a, b, *, c):
... print(a)
... print(b)
... print(c)
...
>>>
>>> demo_func(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: demo_func() takes 2 positional arguments but 3 were given
>>>
>>> demo_func(1, 2, c=3)
1
2
3
2.3 传参的坑
函数参数传递的是实际对象的内存地址
。如果参数是引用类型的数据类型(列表
、字典
等),在函数内部修改后,就算没有把修改后的值返回
回去,外面的值其实也已经发生了变化。
>>> def add_item(item, source_list):
... source_list.append(item)
...
>>> alist = [0,1]
>>> add_item(2, alist)
>>> alist
[0, 1, 2]
3. 匿名函数的使用
匿名函数(英语:anonymous function)是指一类无需定义标识符(函数名)的函数。通俗来说呢,就是它可以让我们的函数,可以不需要函数名
。
正常情况下,我们定义一个函数,使用的是def
关键字,而当你学会使用匿名函数
后,替代def
的是 lambda
。
这边使用def
和 lambda
分别举个例子,你很快就能理解。
def mySum(x, y):
return x+y
mySum(2, 3)
# 5
(lambda x, y: x+y)(2, 4)
# 6
从上面的示例,我们可以看到匿名函数
直接运行,省下了很多行的代码,有没有?
接下来,我们的仔细看一下它的用法
带if
/else
>>>( lambda x, y: x if x < y else y )( 1, 2 )
1
嵌套函数
>>>( lambda x: ( lambda y: ( lambda z: x + y + z )( 1 ) )( 2 ) )( 3 )
6
递归函数
>>> func = lambda n:1 if n == 0 else n * func(n-1)
>>> func(5)
120
或者
>>> f = lambda func, n: 1 if n == 0 else n * func( func, n - 1 )
>>> f(f,4)
24
从以上示例来看,lambda
表达式和常规的函数相比,写法比较怪异,可读性相对较差。除了可以直接运行之外,好像并没有其他较为突出的功能,为什么在今天我们要介绍它呢?
首先我们要知道lambda
是一个表达式
,而不是一个语句。正因为这个特点,我们可以在一些特殊的场景中去使用它。具体是什么场景呢?接下来我们会介绍到几个非常好用的内置函数
。
4. 必学高阶函数
4.1 map 函数
map
函数,它接收两个参数,第一个参数是一个函数对象
(当然也可以是一个lambda
表达式),第二个参数是一个序列
。
它可以实现怎样的功能呢,我举个例子你就明白了。
>>> map(lambda x: x*2, [1,2,3,4,5])
[2, 4, 6, 8, 10]
可以很清楚地看到,它可以将后面序列
中的每一个元素做为参数
传入lambda
中。
当我们不使用 map
函数时,你也许会这样子写。
mylist=[]
for i in [1,2,3,4,5]:
mylist.append(i*2)
4.2 filter 函数
filter
函数,和 map
函数相似。同样也是接收两个参数,一个lambda
表达式,一个序列
。它会遍历
后面序列
中每一个元素
,并将其做为参数
传入lambda
表达式中,当表达式返回 True
,则元素
会被保留下来,当表达式返回False
,则元素
会被丢弃。
下面这个例子,将过滤出一个列表中小于0
的元素。
>>>filter(lambda x: x < 0, range(-5, 5))
[-5, -4, -3, -2, -1]
4.3 reduce 函数
reduce 不可以直接调用,而是要先导入才能使用
from functools import reduce
reduce
函数,也是类似的。它的作用是先对序列中的第 1、2 个元素进行操作,得到的结果再与第三个数据用lambda
函数运算,将其得到的结果再与第四个元素进行运算,以此类推下去直到后面没有元素了。
这边举个例子你也就明白了。
>>>reduce(lambda x,y: x+y, [1,2,3,4,5])
15
它的运算过程分解一下是这样的。
1+2=3
3+3=6
6+4+10
10+5=15
4.4 注意点
以上几个函数,熟练的掌握它们的写法,可以让我们的代码看起来更加的 Pythonic ,在某一程度上代码看起来更加的简洁。
如果你是新手呢,你需要注意的是,以上示例是在 Python2.x 环境下演示的。而在 Python3.x 中,却有所不同,你可以自己尝试一下。
这里总结一下:
第一点,map
和filter
函数返回的都不再是一个列表,而是一个迭代器
对象。这里以map
为例
>>> map_obj = map(lambda x: x*2, [1,2,3,4,5])
>>> from collections.abc import Iterator
>>> isinstance(map_obj, Iterator)
True
>>> next(map_obj)
2
>>> list(map_obj)
[4, 6, 8, 10]
第二点,reduce
不可以直接调用,而是要先导入才能使用,
from functools import reduce
5. 反射函数的使用
自省,在我们日常生活中,通常是自我反省的意思。
但在计算机编程中,自省并不是这个意思,它的英文单词是 introspection,表示的是自我检查的行为或能力。
它的内容包括
告诉别人,我是谁
告诉别人,我能做什么
Python 是一门动态语言,有了自省,就能让程序在运行时能够获知对象的类型以及该对象下有哪些方法等。
5.1 学习 Python 模块的入口
help()
在 console
模式下,输入help()
,可以看到输出了一段帮助文档,教你如何使用这个 help
,当你看到提示符变成了 help>
时,这时候就进入了help
模式。
此时你可以键入你想要了解的模块、语法等,help
告诉你如何使用。
比如我输入keywords
,就可以看到 Python 里所有的关键字
。再输入modules
就可以查看 Python 中所有的内置模块
。
输入 modules
+ 指定包名
,就可以查看这个包下有哪些模块
如果你想学习某个包要如何使用,可以直接在 help
模式下输入包名
,就像下面这样,我就可以获得一份json
的帮助文档。
如果你想学习某个关键字
的用法,可以在help
模式下直接键入 关键字
查询用法,比如我直接键入for
。
查完后,使用 quit
就可以退出 help
模式了。
如果你觉得进入help
模式太麻烦,可以在 console
模式下直接查询
>>> help("json")
dir()
dir()
函数可能是 Python 自省机制中最著名的部分了。它返回传递给它的任何对象的属性名称经过排序的列表。如果不指定对象,则 dir()
返回当前作用域中的名称。让我们将 dir()
函数应用于 keyword
模块,并观察它揭示了什么:
5.2 应用到实际开发中
type()
type()
函数有助于我们确定对象是字符串
还是整数
,或是其它类型的对象。它通过返回类型对象来做到这一点,可以将这个类型对象与types
模块中定义的类型相比较:
>>> type(42)
<class 'int'>
>>> type([])
<class 'list'>
hasattr()
使用 dir()
函数会返回一个对象的属性列表。
但是,有时我们只想测试一个或多个属性是否存在。如果对象具有我们正在考虑的属性,那么通常希望只检索该属性。这个任务可以由hasattr()
来完成.
>>> import json
>>> hasattr(json, "dumps")
True
>>>
getattr()
使用hasattr
获知了对象拥有某个属性后,可以搭配 getattr()
函数来获取其属性值。
>>> getattr(json, "__path__")
['/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json']
>>>
使用 getattr
获取函数后,可以很方便地使用这个函数,比如下面这样,可以不再使写json.dumps
这么字。
>>> dumps = getattr(json, "dumps")
>>> dumps({"name": "MING"})
'{"name": "MING"}'
>>>
# 当然你还有更简单的方法
>>> mydumps = json.dumps
>>> mydumps({"name": "MING"})
'{"name": "MING"}'
id()
id()
函数返回对象的唯一标识符
,标识符是一个整数
。
>>> a = "hello"
>>> b = "world"
>>>
>>> id(a)
4470767944
>>> id(b)
4499487408
>>>
isinstance()
使用isinstance()
函数可以确定一个对象是否是某个特定类型或定制类的实例。
>>> isinstance("python", str)
True
>>> isinstance(10, int)
True
>>> isinstance(False, bool)
True
callable()
使用 callable
可以确定一个对象是否是可调用的,比如函数,类这些对象都是可以调用的对象。
>>> callable("hello")
False
>>>
>>> callable(str)
True
>>>
5.3 模块(Modules)
__doc__
使用 __doc__
这个魔法方法,可以查询该模块
的文档,它输出的内容和help()
一样。
__name__
始终是定义时的模块名
;即使你使用import .. as
为它取了别名,或是赋值
给了另一个变量名
。
>>> import json
>>> json.__name__
'json'
>>>
>>> import json as js
>>> js.__name__
'json'
__file__
包含了该模块
的文件路径
。需要注意的是内建的模块
没有这个属性,访问它会抛出异常!
>>> import json
>>> json.__file__
'/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/__init__.py'
__dict__
包含了模块
里可用的属性名-属性
的字典;也就是可以使用模块名.属性名
访问的对象。
5.4 类(Class)
__doc__
文档字符串。如果类没有文档,这个值是None
。
>>> class People:
... '''
... people class
... '''
...
>>> p = People()
>>>
>>> print(p.__doc__)
people class
>>>
__name__
始终是定义时的类名
。
>>> People.__name__
'People'
__dict__
包含了类
里可用的属性名-属性
的字典;也就是可以使用类名.属性名
访问的对象。
>>> People.__dict__
mappingproxy({'__module__': '__main__', '__doc__': '\n people class\n ', '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>})
__module__
包含该类
的定义的模块名
;需要注意,是字符串
形式的模块名
而不是模块对象
。
由于我是在 交互式命令行的环境下,所以模块是 __main__
>>> People.__module__
'__main__'
如果将上面的代码放入demo.py
,并且从 people 模块
导入People 类
,其值就是 people 模块
__bases__
直接父类对象
的元组
;但不包含继承树更上层的其他类,比如父类的父类。
>>> class People: pass
...
>>> class Teenager: pass
...
>>> class Student(Teenager): pass
...
>>> Student.__bases__
(<class '__main__.Teenager'>,)
>>>
6. 偏函数的妙用
假如一个函数定义了多个位置参数
,那你每次调用时,都需要把这些个参数一个一个地传递进去。
比如下面这个函数,是用来计算x
的n
次方 的。
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
那我每次计算 x
的 n
次方,都要传递两个参数
>>> power(2, 2)
4
>>> power(3, 2)
9
后来我发现,我很多时候都是计算平方值,很多会去计算三次方,四次方。
那有什么办法可以偷个懒吗?
答案是,有。可以使用 偏函数
。
偏函数
(Partial Function),可以将某个函数的常用参数进行固定,避免每次调用时都要指定。
使用偏函数
,需要导入functools.partial
,然后利用它创建一个新函数,新函数的n
固定等2
。
具体使用请看下面的示例
>>> from functools import partial
>>> power_2=partial(power, n=2)
>>> power_2(2)
4
>>> power_2(3)
9
7. 【进阶】泛型函数的使用
根据传入参数类型的不同而调用不同的函数逻辑体
,这种实现我们称之为泛型
。在 Python 中叫做 singledispatch
。
singledispatch
是 PEP443
中引入的,如果你对此有兴趣,PEP443
应该是最好的学习文档:https://www.python.org/dev/peps/pep-0443/
它使用方法极其简单,只要被singledispatch
装饰的函数,就是一个single-dispatch
的泛函数(generic functions
)。
- 单分派:根据一个参数的类型,以不同方式执行相同的操作的行为。
- 多分派:可根据多个参数的类型选择专门的函数的行为。
- 泛函数:多个函数绑在一起组合成一个泛函数。
这边举个简单的例子。
from functools import singledispatch
@singledispatch
def age(obj):
print('请传入合法类型的参数!')
@age.register(int)
def _(age):
print('我已经{}岁了。'.format(age))
@age.register(str)
def _(age):
print('I am {} years old.'.format(age))
age(23) # int
age('twenty three') # str
age(['23']) # list
执行结果
我已经23岁了。
I am twenty three years old.
请传入合法类型的参数!
说起泛型,其实在 Python 本身的一些内建函数中并不少见,比如 len()
, iter()
,copy.copy()
,pprint()
等
你可能会问,它有什么用呢?实际上真没什么用,你不用它或者不认识它也完全不影响你编码。
我这里举个例子,你可以感受一下。
大家都知道,Python 中有许许多的数据类型,比如str
,list
, dict
, tuple
等,不同数据类型的拼接方式各不相同,所以我这里我写了一个通用的函数,可以根据对应的数据类型对选择对应的拼接方式拼接,而且不同数据类型我还应该提示无法拼接。以下是简单的实现。
def check_type(func):
def wrapper(*args):
arg1, arg2 = args[:2]
if type(arg1) != type(arg2):
return '【错误】:参数类型不同,无法拼接!!'
return func(*args)
return wrapper
@singledispatch
def add(obj, new_obj):
raise TypeError
@add.register(str)
@check_type
def _(obj, new_obj):
obj += new_obj
return obj
@add.register(list)
@check_type
def _(obj, new_obj):
obj.extend(new_obj)
return obj
@add.register(dict)
@check_type
def _(obj, new_obj):
obj.update(new_obj)
return obj
@add.register(tuple)
@check_type
def _(obj, new_obj):
return (*obj, *new_obj)
print(add('hello',', world'))
print(add([1,2,3], [4,5,6]))
print(add({'name': 'wangbm'}, {'age':25}))
print(add(('apple', 'huawei'), ('vivo', 'oppo')))
# list 和 字符串 无法拼接
print(add([1,2,3], '4,5,6'))
输出结果如下
hello, world
[1, 2, 3, 4, 5, 6]
{'name': 'wangbm', 'age': 25}
('apple', 'huawei', 'vivo', 'oppo')
【错误】:参数类型不同,无法拼接!!
如果不使用singledispatch
的话,你可能会写出这样的代码。
def check_type(func):
def wrapper(*args):
arg1, arg2 = args[:2]
if type(arg1) != type(arg2):
return '【错误】:参数类型不同,无法拼接!!'
return func(*args)
return wrapper
@check_type
def add(obj, new_obj):
if isinstance(obj, str) :
obj += new_obj
return obj
if isinstance(obj, list) :
obj.extend(new_obj)
return obj
if isinstance(obj, dict) :
obj.update(new_obj)
return obj
if isinstance(obj, tuple) :
return (*obj, *new_obj)
print(add('hello',', world'))
print(add([1,2,3], [4,5,6]))
print(add({'name': 'wangbm'}, {'age':25}))
print(add(('apple', 'huawei'), ('vivo', 'oppo')))
# list 和 字符串 无法拼接
print(add([1,2,3], '4,5,6'))
输出如下
hello, world
[1, 2, 3, 4, 5, 6]
{'name': 'wangbm', 'age': 25}
('apple', 'huawei', 'vivo', 'oppo')
【错误】:参数类型不同,无法拼接!!
8. 变量的作用域
8.1 作用域
Python的作用域可以分为四种:
- L (Local)
局部作用域
- E (Enclosing)
闭包函数外的函数中
- G (Global)
全局作用域
- B (Built-in)
内建作用域
变量/函数 的查找顺序: L –> E –> G –>B
意思是,在局部找不到的,便去局部外的局部作用域找(例如 闭包
),再找不到的就去全局作业域里找,再找不到就去内建作业域中找。
会影响 变量
/函数
作用范围的有
函数:def
或 lambda
类:class
关键字:global noglobal
文件:*py
推导式:[]
,{}
,()
等,仅限Py3.x中,Py2.x会出现变量泄露。
1、赋值在前,引用在后
# ------同作用域内------
name = "MING"
print(name)
# ------不同作用域内------
name = "MING"
def main():
print(name)
2、引用在前,赋值在后(同一作用域内)
print(name)
name = "MING"
# UnboundLocalError: local variable 'name' referenced before assignment
3、赋值在低层,引用在高层
# L -> E -> G -> B
# 从左到右,由低层到高层
def main():
name = "MING"
print(name)
# NameError: name 'name' is not defined
8.2 闭包
闭包
这个概念很重要噢。你一定要掌握。
在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量
,并且外函数的返回值
是内函数的引用。这样就构成了一个闭包
。其实装饰函数,很多都是闭包
。
好像并不难理解,为什么初学者会觉得闭包难以理解呢?
我解释一下,你就明白了。
一般情况下,在我们认知当中,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量
都会消失。但是闭包
是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量
将来会在内部函数中用到,就把这个临时变量
绑定给了内部函数,然后自己再结束。
你可以看下面这段代码,就构成了闭包
。在内函数里可以引用外函数的变量。
def deco():
name = "MING"
def wrapper():
print(name)
return wrapper
deco()()
# 输出:MING
8.3 改变作用域
变量的作用域
,与其定义(或赋值)的位置有关,但不是绝对相关。 因为我们可以在某种程度上去改变向上的作用范围。
关键字:global
将 局部变量
变为全局变量
关键字:nonlocal
可以在闭包
函数中,引用并使用闭包
外部函数的变量(非全局
的噢)
global
好理解,这里只讲下nonlocal
。
先来看个例子
def deco():
age = 10
def wrapper():
age += 1
return wrapper
deco()()
运行一下,会报错。
# UnboundLocalError: local variable 'age' referenced before assignment
但是这样就OK
def deco():
age = 10
def wrapper():
nonlocal age
age += 1
return wrapper
deco()()
# 输出:11
其实,你如果不使用 +=
、-=
等一类的操作,不加nonlocal
也没有关系。这就展示了闭包
的特性。
def deco():
age = 10
def wrapper():
print(age)
return wrapper
deco()()
# 输出:10
8.4 变量集合
在Python中,有两个内建函数,你可能用不到,但是需要掌握它们。
globals()
:以dict
的方式存储所有全局变量
locals()
:以dict
的方式存储所有局部变量
globals()
def foo():
print("I am a func")
def bar():
foo="I am a string"
foo_dup = globals().get("foo")
foo_dup()
bar()
# 输出
# I am a func
locals()
other = "test"
def foobar():
name = "MING"
gender = "male"
for key,value in locals().items():
print(key, "=", value)
foobar()
# 输出
# name = MING
# gender = male
9. 【进阶】上下文管理器
当你准备从一个文件中读取内容时,通常来说,都是这么写的。
>>> file=open('test.txt')
>>>
>>> print(file.readlines()) # 读取并打印
Hello, Python
>>>
>>> file.close() # 关闭文件句柄
上面这种方法,需要你手动关闭文件句柄,但是很多时候,程序员是会忘记这一操作的。
因为推荐你使用下面这种方法,使用 with
这个关键字,可以在文件读取结束后,自动关闭文件句柄
。
with open('test.txt') as file:
print(file.readlines())
使用 Python 的专业术语来说,with
的这个用法叫做 上下文管理器
。
9.1 什么是上下文管理器?
基本语法
with EXPR as VAR:
代码块
从上面这个语法中,先理清几个概念:
- 上下文表达式:
with open('test.txt') as file:
- 上下文管理器:
open('test.txt')
file
不是上下文管理器,应该是资源对象。
9.2 如何写上下文管理器?
要手动实现一个上下文管理器
,需要你有对类
有一些了解,至少需要知道什么是类
,怎么定义类
。对于类
的知识,我放在了第七章,因此你可以先前往学习下第七章的的第一节内容:7.1 类
的理解与使用 。
学习了类
的基本知识,想要自己实现这样一个上下文管理
,就简单了。
你只要在一个类里实现上下文管理协议,简单点说,就是在一个类里,定义了__enter__
和__exit__
的方法,这个类的实例就是一个上下文管理器
。
例如这个示例:
class Resource():
def __enter__(self):
print('===connect to resource===')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('===close resource connection===')
def operate(self):
print('===in operation===')
with Resource() as res:
res.operate()
我们执行一下,通过日志的打印顺序。可以知道其执行过程。
===connect to resource===
===in operation===
===close resource connection===
从这个示例可以很明显的看出,在编写代码时,可以将资源的连接或者获取放在__enter__
中,而将资源的关闭写在__exit__
中。
9.3 为什么需要上下文管理器?
学习时多问自己几个为什么,养成对一些细节的思考,有助于加深对知识点的理解。
为什么要使用上下文管理器
?
在我看来,这和 Python 崇尚的优雅风格有关。
- 可以以一种更加优雅的方式,操作(创建/获取/释放)资源,如文件操作、数据库连接;
- 可以以一种更加优雅的方式,处理异常;
- 第一种,我们上面已经以资源的连接为例讲过了。
而第二种,会被大多数人所忽略。这里会重点讲一下。
大家都知道,处理异常
,通常都是使用try...execept..
来捕获处理的。这样做一个不好的地方是,在代码的主逻辑里,会有大量的异常处理
代理,这会很大的影响我们的可读性。
好一点的做法呢,可以使用 with
将异常的处理隐藏起来。
仍然是以上面的代码为例,我们将1/0
这个一定会抛出异常的代码写在operate
里
class Resource():
def __enter__(self):
print('===connect to resource===')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('===close resource connection===')
return True
def operate(self):
1/0
with Resource() as res:
res.operate()
运行一下,惊奇地发现,居然不会报错。
这就是上下文管理
协议的一个强大之处,异常可以在__exit__
进行捕获并由你自己决定如何处理,是抛出呢还是在这里就解决了。在__exit__
里返回 True
(没有return 就默认为 return False
),就相当于告诉 Python解释器,这个异常我们已经捕获了,不需要再往外抛了。
在 写__exit__
函数时,需要注意的事,它必须要有这三个参数:
exc_type
:异常类型exc_val
:异常值exc_tb
:异常的错误栈信息
当主逻辑代码没有报异常时,这三个参数将都为None
。
9.4 学会使用 contextlib
在上面的例子中,我们只是为了构建一个上下文管理器
,却写了一个类
。如果只是要实现一个简单的功能,写一个类
未免有点过于繁杂。这时候,我们就想,如果只写一个函数就可以实现上下文管理器就好了。
这个点Python早就想到了。它给我们提供了一个装饰器
,你只要按照它的代码协议来实现函数内容,就可以将这个函数对象变成一个上下文管理器
。
我们按照contextlib
的协议来自己实现一个打开文件(with open
)的上下文管理器
。
import contextlib
@contextlib.contextmanager
def open_func(file_name):
# __enter__方法
print('open file:', file_name, 'in __enter__')
file_handler = open(file_name, 'r')
# 【重点】:yield
yield file_handler
# __exit__方法
print('close file:', file_name, 'in __exit__')
file_handler.close()
return
with open_func('/Users/MING/mytest.txt') as file_in:
for line in file_in:
print(line)
在被装饰函数里,必须是一个生成器
(带有yield
),而yield
之前的代码,就相当于__enter__
里的内容。yield
之后的代码,就相当于__exit__
里的内容。
上面这段代码只能实现上下文管理器
的第一个目的(管理资源),并不能实现第二个目的(处理异常
)。
如果要处理异常
,可以改成下面这个样子。
import contextlib
@contextlib.contextmanager
def open_func(file_name):
# __enter__方法
print('open file:', file_name, 'in __enter__')
file_handler = open(file_name, 'r')
try:
yield file_handler
except Exception as exc:
# deal with exception
print('the exception was thrown')
finally:
print('close file:', file_name, 'in __exit__')
file_handler.close()
return
with open_func('/Users/MING/mytest.txt') as file_in:
for line in file_in:
1/0
print(line)
好像只要讲到上下文管理器
,大多数人都会谈到打开文件这个经典的例子。
但是在实际开发中,可以使用到上下文管理器
的例子也不少。我这边举个我自己的例子。
在OpenStack
中,给一个虚拟机创建快照时,需要先创建一个临时文件夹,来存放这个本地快照镜像,等到本地快照镜像创建完成后,再将这个镜像上传到Glance
。然后删除这个临时目录。
这段代码的主逻辑是创建快照,而创建临时目录,属于前置条件,删除临时目录,是收尾工作。
虽然代码量很少,逻辑也不复杂,但是“创建临时目录,使用完后再删除临时目录”
这个功能,在一个项目中很多地方都需要用到,如果可以将这段逻辑处理写成一个工具函数作为一个上下文管理器
,那代码的复用率也大大提高。
代码是这样的
9.5 总结起来
使用上下文管理器
有三个好处:
- 提高代码的复用率;
- 提高代码的优雅度;
- 提高代码的可读性;
10.【进阶】装饰器的六种写法
Hello,装饰器
装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。
它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。
装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
装饰器的使用方法很固定
- 先定义一个装饰器(帽子)
- 再定义你的业务函数或者类(人)
- 最后把这装饰器(帽子)扣在这个函数(人)头上
就像下面这样子
# 定义装饰器
def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper
# 定义业务函数并进行装饰
@decorator
def function():
print("hello, decorator")
实际上,装饰器并不是编码必须性,意思就是说,你不使用装饰器完全可以,它的出现,应该是使我们的代码
- 更加优雅,代码结构更加清晰
- 将实现特定的功能代码封装成装饰器,提高代码复用率,增强代码可读性
接下来,我将以实例讲解,如何编写出各种简单及复杂的装饰器。
第一种:普通装饰器
首先咱来写一个最普通的装饰器,它实现的功能是:
- 在函数执行前,先记录一行日志
- 在函数执行完,再记录一行日志
# 这是装饰器函数,参数 func 是被装饰的函数
def logger(func):
def wrapper(*args, **kw):
print('我准备开始执行:{} 函数了:'.format(func.__name__))
# 真正执行的是这行。
func(*args, **kw)
print('主人,我执行完啦。')
return wrapper
假如,我的业务函数是,计算两个数之和。写好后,直接给它带上帽子。
@logger
def add(x, y):
print('{} + {} = {}'.format(x, y, x+y))
然后执行一下 add 函数。
add(200, 50)
来看看输出了什么?
我准备开始执行:add 函数了:
200 + 50 = 250
我执行完啦。
第二种:带参数的函数装饰器
通过上面两个简单的入门示例,你应该能体会到装饰器的工作原理了。
不过,装饰器的用法还远不止如此,深究下去,还大有文章。今天就一起来把这个知识点学透。
回过头去看看上面的例子,装饰器是不能接收参数的。其用法,只能适用于一些简单的场景。不传参的装饰器,只能对被装饰函数,执行固定逻辑。
装饰器本身是一个函数,做为一个函数,如果不能传参,那这个函数的功能就会很受限,只能执行固定的逻辑。这意味着,如果装饰器的逻辑代码的执行需要根据不同场景进行调整,若不能传参的话,我们就要写两个装饰器,这显然是不合理的。
比如我们要实现一个可以定时发送邮件的任务(一分钟发送一封),定时进行时间同步的任务(一天同步一次),就可以自己实现一个 periodic_task
(定时任务)的装饰器,这个装饰器可以接收一个时间间隔的参数,间隔多长时间执行一次任务。
可以这样像下面这样写,由于这个功能代码比较复杂,不利于学习,这里就不贴了。
@periodic_task(spacing=60)
def send_mail():
pass
@periodic_task(spacing=86400)
def ntp()
pass
那我们来自己创造一个伪场景,可以在装饰器里传入一个参数,指明国籍,并在函数执行前,用自己国家的母语打一个招呼。
# 小明,中国人
@say_hello("china")
def xiaoming():
pass
# jack,美国人
@say_hello("america")
def jack():
pass
那我们如果实现这个装饰器,让其可以实现 传参
呢?
会比较复杂,需要两层嵌套。
def say_hello(contry):
def wrapper(func):
def deco(*args, **kwargs):
if contry == "china":
print("你好!")
elif contry == "america":
print('hello.')
else:
return
# 真正执行函数的地方
func(*args, **kwargs)
return deco
return wrapper
来执行一下
xiaoming()
print("------------")
jack()
看看输出结果。
你好!
------------
hello.
第三种:不带参数的类装饰器
以上都是基于函数实现的装饰器,在阅读别人代码时,还可以时常发现还有基于类实现的装饰器。
基于类装饰器的实现,必须实现 __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")
执行一下,看看输出
[INFO]: the function say() is running...
say hello!
第四种:带参数的类装饰器
上面不带参数的例子,你发现没有,只能打印INFO
级别的日志,正常情况下,我们还需要打印DEBUG WARNING
等级别的日志。 这就需要给类装饰器传入参数,给这个函数指定级别了。
带参数和不带参数的类装饰器有很大的不同。
__init__
:不再接收被装饰函数,而是接收传入参数。__call__
:接收被装饰函数,实现装饰逻辑。
class logger(object):
def __init__(self, level='INFO'):
self.level = level
def __call__(self, func): # 接受函数
def wrapper(*args, **kwargs):
print("[{level}]: the function {func}() is running..."\
.format(level=self.level, func=func.__name__))
func(*args, **kwargs)
return wrapper #返回函数
@logger(level='WARNING')
def say(something):
print("say {}!".format(something))
say("hello")
我们指定WARNING
级别,运行一下,来看看输出。
[WARNING]: the function say() is running...
say hello!
第五种:使用偏函数与类实现装饰器
绝大多数装饰器都是基于函数和闭包实现的,但这并非制造装饰器的唯一方式。
事实上,Python 对某个对象是否能通过装饰器(@decorator
)形式使用只有一个要求:decorator
必须是一个“可被调用(callable
)的对象。
对于这个 callable
对象,我们最熟悉的就是函数了。
除函数之外,类也可以是 callable
对象,只要实现了__call__
函数(上面几个例子已经接触过了)。
还有容易被人忽略的偏函数其实也是 callable
对象。
接下来就来说说,如何使用 类和偏函数结合实现一个与众不同的装饰器。
如下所示,DelayFunc
是一个实现了__call__
的类,delay
返回一个偏函数,在这里delay
就可以做为一个装饰器。(以下代码摘自 Python工匠:使用装饰器的小技巧)
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 eager_call(self, *args, **kwargs):
print('Call without delay')
return self.func(*args, **kwargs)
def delay(duration):
"""
装饰器:推迟某个函数的执行。
同时提供 .eager_call 方法立即执行
"""
# 此处为了避免定义额外函数,
# 直接使用 functools.partial 帮助构造 DelayFunc 实例
return functools.partial(DelayFunc, duration)
我们的业务函数很简单,就是相加
@delay(duration=2)
def add(a, b):
return a+b
来看一下执行过程
>>> add # 可见 add 变成了 Delay 的实例
<__main__.DelayFunc object at 0x107bd0be0>
>>>
>>> add(3,5) # 直接调用实例,进入 __call__
Wait for 2 seconds...
8
>>>
>>> add.func # 实现实例方法
<function add at 0x107bef1e0>
第六种:能装饰类的装饰器
用 Python 写单例模式的时候,常用的有三种写法。其中一种,是用装饰器来实现的。
以下便是我自己写的装饰器版的单例写法。
instances = {}
def singleton(cls):
def get_instance(*args, **kw):
cls_name = cls.__name__
print('===== 1 ====')
if not cls_name in instances:
print('===== 2 ====')
instance = cls(*args, **kw)
instances[cls_name] = instance
return instances[cls_name]
return get_instance
@singleton
class User:
_instance = None
def __init__(self, name):
print('===== 3 ====')
self.name = name
可以看到我们用singleton
这个装饰函数来装饰User
这个类。装饰器用在类上,并不是很常见,但只要熟悉装饰器的实现过程,就不难以实现对类的装饰。在上面这个例子中,装饰器就只是实现对类实例的生成的控制而已。
其实例化的过程,你可以参考我这里的调试过程,加以理解。