上一篇文章谈妖:Python函数知识大杂烩(一)定义及参数zhuanlan.zhihu.com
回顾了Python函数的基础部分,本篇将提供进阶部分的总结。
返回值和作用域
另一个非常重要的概念是作用域。一般来讲,作用域是指一个变量起作用的范围。我们可以通过一个简单的案例来了解这个概念:
def func1():
x = 5
print(x)
def func2():
print(x)
>>>func1()
5
>>>func2()
Traceback (most recent call last):
File "", line 1, in
File "", line 2, in func2
NameError: name 'x' is not defined
在上个案例中,func1 函数可以正常返回值,而 func2 却返回错误信息。
原因就在于,变量x在函数func1内定义,因此它的作用域仅限于func1内部,func2调用时无法使用。
当我们在函数或类的内部定义变量时,变量只能在其内部使用,这就是所谓的局部作用域。当我们在整个程序的运行环境中定义变量,则在整个程序中都可以使用,这就是所谓的全局作用域。
在上例中,我们可以将func1中定义的变量x放到全局中进行定义,func2就不会出错。
接下来我们看另外一种情况:
def func3():
x=10
def func4():
x=15
print('func4 {}'.format(x))
func4()
print('func3 {}'.format(x))
>>> func3()
func4 15
func3 10
当一个函数内部还有另外一个函数时,我们将其称为嵌套函数。可以发现在嵌套函数内部,变量同样会受到作用域的限制,我们在func4中的定义的变量x并没有覆盖外层的x。
接下来我们再看一个案例:
x = 5
def func5():
x += 1
print(x)
>>> func5()
Traceback (most recent call last):
File "", line 1, in
File "", line 2, in func5
UnboundLocalError: local variable 'x' referenced before assignment
错误提示显示,本地变量 x 在被分配前即被引用。我们不是已经在全局定义了一个 x 吗?为什么此时显示还没有定义呢?
原因在于,在Python中,赋值即定义。我们在函数内部使用“=”时,相当于在本地作用域中定义了一个x,语句“x+=1”等价于“x=x+1”,此时等式右边的x在本地作用域还不存在,于是就出现了上述错误。
想要解决这种错误,就要引入两个新概念global和nonlocal。
global 和 nonlocal
global关键字将允许函数使用全局变量。
x = 5
def func6():
global x
x += 1
print(x)
>>> func6()
6
大部分时候我们不推荐使用global关键字。
nonlocal关键字允许函数使用上级作用域中的变量,但不能是全局变量。
def func7():
x = 5
def func8():
nonlocal x
x += 1
print(x)
func8()
print(x)
>>> func7()
6
6
上面的示例运行正常。
递归函数
当一个函数直接或间接调用自身的时候,就是一个递归函数。
现在,我们采用循环的方式生成一个10位的斐波那契数列:
per = 0
cur = 1
print(cur,end=',')
for i in range(9):
per,cur = cur,per+cur
print(cur,end=',')
>>>1,1,2,3,5,8,13,21,34,55,
如果用递归的方式:
def fib(n):
return 1 if n<2 else fib(n-1) + fib(n-2)
for i in range(10):
print(fib(i), end=',')
>>>1,1,2,3,5,8,13,21,34,55,
可以看到,递归的代码更简洁。
使用递归有几个注意事项:
1.递归函数必须有退出条件,无限递归就是死循环
2.递归深度不宜过深,否则栈溢出,python会抛出异常
3.一般来说递归性能会比较差
函数递归还有另外一种情况:间接递归。
举个简单的例子:
def first(n):
return second(n) if n>1 else 1
def second(n):
n -= 1
return first(n)
有时候,间接递归可以有效防止栈帧溢出。
匿名函数
除了递归之外,python还有一种可以大幅减少代码量的函数语法,就是匿名函数。
匿名函数使用 lambda 关键字作为标识。整个表达式由“lambda关键字”+“参数”+“表达式”组成。
在下面的示例中,两种写法完全等价。
lambda x,y:x+y
def add(x,y):
return x+y
匿名函数只能写在同一行,无法跨行。这种函数形式一般用于像高阶函数传参时使用。
生成器
有时候我们使用递归函数,每次只对一个结果进行操作,操作完成后才会对下一个结果进行操作。这种情况下,递归函数一次性生成所有结果会出现性能浪费。针对这种情况,我们可以使用生成器函数来解决。
生成器函数是指表达式中包含 yeild 关键字的函数,这种情况下函数不会一次性返回所有结果,而是会在每次调用的时候产生一个结果,直至求值结束,即所谓“惰性求值”。
看下面的示例:
def inc():
for i in range(5):
yield i
x = inc()
>>> next(x)
0
>>> next(x)
1
注意:直接调用 next(inc())每次都会返回0,原因在于这种形式相当于每次都在重新调用inc函数,生成新的对象。
yield 与 return 的不同在于:
return 会终止整个函数运行, yield 相当于是暂停函数运行。简单来说,一个函数只有一个 return 值,但却可以有多个 yield 。
当没有多余的 yield 语句可供执行之后,调用next会抛出StopIteration异常。
在 python3.3 版本之后,还有一种新的语法: yield from 。
示例如下:
def inc():
yield from range(5)
该示例与前一个示例等价。
当我们熟练掌握生成器函数之后,还可以借助它实现协程(coroutine),当然我们也可以使用 asyncio 标准库,在3.5之后还可以直接使用 async 和 await 关键字,这个我们以后再说。
高阶函数
Python的高级函数是指接受一个或多个函数作为参数,或者输出一个函数的函数。
示例:
def counter(base):
def inc(step =1):
nonlocal base
base += step
return base
return inc
a = counter(3)
a()
柯里化
柯里化是指将接受两个参数的函数修改为接受一个参数的过程。
示例:
def counter1(base,step=1):
base += step
return base
查看高阶函数中的示例。
counter 可以视为 counter1 柯里化之后的函数。
装饰器
正常情况下,业务函数是不建议修改的,简单修改很可能引发连锁反应。
那么,在业务函数无法修改,但又确实需要增强某些功能的时候,我们可以使用函数装饰器。
假设我们需要在函数调用时输出一些日志信息。
那么我们可以构造一个可以打印日志的高阶函数,调用时传入一个函数即可:
def logger(fn):
def wrapper(*args,**kwargs):
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
以上就是一个装饰器的雏形。
调用时,传入一个函数,再传入实参就可以。
日常使用中,python提供一个语法糖:
@logger
def add(x,y):
return x+y
>>> add(3,4)
begin
end
7
使用时,直接调用add函数即可。
装饰器有一个副作用,会将原函数文档和属性进行替换,一般而言我们是不允许它进行变动的。
def logger(fn):
def wrapper(*args,**kwargs):
'''
wrapper
'''
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
@logger
def add(x,y):
'''
add
'''
return x+y
>>> print('name:{},doc:{}'.format(add.__name__,add.__doc__))
name:wrapper,doc:
wrapper
可见add函数的属性已经被替换了。
解决方案并不难,我们可以在装饰器logger上再进行一层装饰,将原函数属性复制到装饰器属性就可以。
def copy_properties(src):
def _copy(dst):
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
return dst
return _copy
def logger(fn):
@copy_properties(fn)
def wrapper(*args,**kwargs):
'''
wrapper
'''
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
@logger
def add(x,y):
'''
add
'''
return x+y
>>> print('name:{},doc:{}'.format(add.__name__,add.__doc__))
name:add,doc:
add
我们的示例实现的功能,其实可以通过functools模块内的functools.update_wrapper和functools.wraps实现(详情请查看源码)。
Python函数总结完毕。