嵌套函数
嵌套函数定义
Python允许将一个或多个函数放置在另外一个函数内部,只需将我们需要的函数定义在现存的某个函数定义中即可。其中函数内部的函数叫嵌套函数,定义嵌套函数的函数为外部函数,嵌套函数只属于外部函数。如下:
def outer(**kwargs):
a = 10
def inner():
print("inner:", a)
print("outer:", a)
inner()
outer() # 输出 outer: 10和inner: 10
inner() # 错误 NameError: name 'inner' is not defined
作用域和命名空间
当程序的不同地方都出现同名变量a
时,Python是通过作用域和命名空间访问顺序的规则来控制变量的存取的,如下不同地方出现同名变量实例:
a = 1
def outer():
a = 2
def inner():
a = 3
print(a)
print(a)
inner()
print(a)
outer()
# 1
# 2
# 3
在Python程序中定义、改变、查找变量名时,都是在一个保存变量名的命名空间中进行中,此命名空间亦称为变量的作用域。Python的作用域是静态的,在代码中变量名被赋值的位置决定了该变量能被访问的范围。即Python变量的作用域由变量所在源代码中的位置决定。Python的作用域共有四种:
- L - Local 局部作用域,函数中定义的名称和变量
- E - Enclosing 嵌套作用域,定义在此函数的上一层父级函数(外部函数)的名称和变量
- G - Global 全局作用域,模块中定义的名称和变量
- B - Built-in 内置作用域,Python语言内置的名称和变量
Python可以通过globals()
函数以字典的方式返回全局作用域的名称和变量,也可以通过locals()
以字典的方式返回局部作用域的名称和变量,如下:
a = 1
print("函数定义前模块全局:")
print(f"\tglobals: {globals().keys()}, {globals()['a']}")
print(f"\tlocals: {locals().keys()}, {locals()['a']}")
def outer():
a = 2
def inner():
a = 3
print("inner函数")
print(f"\tglobals: {globals().keys()}, {globals()['a']}")
print(f"\tlocals: {locals().keys()}, {locals()['a']}")
print("outer函数:")
print(f"\tglobals: {globals().keys()}, {globals()['a']}")
print(f"\tlocals: {locals().keys()}, {locals()['a']}")
inner()
print("函数定义后模块全局:")
print(f"\tglobals: {globals().keys()}, {globals()['a']}")
print(f"\tlocals: {locals().keys()}, {locals()['a']}")
outer()
上面代码的输出如下图所示,可以看出模块的全局作用域和局部作用域等同,作用域是从变量定义后开始考量的。
Python引用变量的顺序为LEGB
即局部作用域
->嵌套作用域
->全局作用域
->内置作用域
,如下程序的不同地方出现同名变量的几个实例:
# 实例一
a = 1
def outer1():
a = 2
def inner1():
a = 3
print(a) # 输出3,局部作用域有a,取局部作用域的值
print(a) # 输出2,局部作用域有a,取局部作用域
inner1()
print(a) # 输出1
outer1()
# 实例二
b = 11
def outer2():
b = 22
def inner2():
print(b) # 输出22,局部作用域没有b,访问嵌套作用域,嵌套作用域有b
print(b) # 输出22,局部作用域有b,取局部作用域
inner2()
print(b) # 输出11
outer2()
# 实例三
c = 111
def outer3():
def inner3():
print(c) # 输出111,局部作用域没有c,访问嵌套作用域,嵌套作用域也没有c,访问全局作用域
print(c) # 输出111,局部作用域没有c,没有嵌套作用域就直接访问全局作用域
inner3()
print(c) # 输出111
outer3()
# 实例四
def outer4():
def inner4():
print(d) # name 'd' is not defined
print(d) # name 'd' is not defined
inner4()
print(d) # name 'd' is not defined
outer4()
上面的实例可以看出当我们在函数或嵌套函数内部改变变量值(这里指的是将变量引用的对象改为其他对象)时,并不会影响到全局或外部函数的同名变量的值。
如果我们需要改变函数或嵌套函数内部变量值,全局或外部函数的同名变量的值也会跟着改变的情况时,可以使用global
和nonlocal
关键字。global
关键字声明该变量来自全局作用域而不要在当前作用域创建变量,而nonlocal
关键字声明该变量来自嵌套作用域而不要当前作用域创建变量,如下几个实例:
# 实例一
a = 1
def outer1():
global a
a = 2
print(locals().keys()) # locals里没有a了
print(a) # 输出2
print("outer1调用前:", a) # 输出1
outer1()
print("outer1调用后:", a) # 输出2
# 实例二
b = 11
def outer2():
b = 22
def inner2():
global b
print(b) # 输出11
b = 33
print(b) # 输出33
print("inner2调用前:", b) # 输出22
inner2()
print("inner2调用后:", b) # 输出22
print("outer2调用前:", b) # 输出11
outer2()
print("outer2调用后:", b) # 输出33
# 实例三
c = 111
def outer3():
c = 222
def inner3():
nonlocal c
print(c) # 输出222
c = 333
print(c) # 输出333
print("inner3调用前:", c) # 输出222
inner3()
print("inner3调用后:", c) # 输出333
print("outer3调用前:", c) # 输出111
outer3()
print("outer3调用后:", c) # 输出111
如果声明了global
或nonlocal
但没有找到会产生SyntaxError错误,如下
# 实例一,会出错,因为inner的嵌套作用域里没有a,因为外部函数的局部作用域没有a,a是全局的
# SyntaxError: no binding for nonlocal 'a' found
a = 1111
def outer():
global a
a = 2222
def inner():
nonlocal a
print(a)
a = 3333
print(a)
print("inner4调用前:", a)
inner()
print("inner4调用后:", a)
print("outer调用前:", a)
outer()
print("outer调用后:", a)
注意,global
和nonlocal
一定要在对象使用前声明,否则会产生SyntaxError错误,如下所示:
# 实例一,SyntaxError: name 'a' is used prior to global declaration
a = 1
def outer1():
print(a)
global a
# 实例二,SyntaxError: name 'b' is assigned to before global declaration
b = 11
def outer2():
b = 22
global b
# 实例三,SyntaxError: name 'c' is used prior to nonlocal declaration
c = 111
def outer3():
c = 222
def inner():
print(c)
nonlocal c
# 实例四,SyntaxError: name 'd' is assigned to before nonlocal declaration
d = 1111
def outer4():
d = 2222
def inner():
d = 3333
nonlocal d
同样地,如果先在函数或嵌套函数内部读取了变量值,后改变变量值也会出现错误,不过这个需要调用函数时才会抛出UnboundLocalError错误,如下所示:
# 实例一,UnboundLocalError: local variable 'a' referenced before assignment
a = 1
def outer1():
print(a)
a = 10
outer1()
# 实例二,UnboundLocalError: local variable 'b' referenced before assignmen
def outer2():
b = 11
def inner2():
print(b)
b = 22
inner2()
outer2()
注意,上面所说的变量改变值,指的是将变量引用的对象改为其他对象,与列表这种可变数据类型添加元素,改变元素值这种不同。因为Python的变量可以理解为是对象的引用,改变变量值相当于改变了对象的引用,而列表添加元素,改变元素值并没有改变变量对列表的引用。因此如下几个实例所面临的情况并不属于上面所说的改变变量值:
lst = [1, 2, 3, 4, 5, 6] def outer(): print(lst) # 下面的操作并没有改变变量值 lst.append(10) lst[0] = 15 outer() print(lst)
lambda表达式
当需要定义一个简单的函数时,lambda表达式是一个很好的选择,lambda表达式的语法如下:
lambda parameters:expression
其中,parameters是可选的,与def语句支持的完整参数语法格式一样。expression是一个表达式,不能包含分支或循环(可以使用条件表达式),也不能包含return语句。lambda表达式定义时也不支持注解和文档字符串。
lambda表达式返回一个匿名函数,因此有需要的时候可以将返回的函数赋值给一个变量。调用lambda函数时,返回的结果是对expression计算产生的结果,如下几个实例:
s = lambda (x:int): "" if x == 1 else "s"
print("{} file{}".format(1, s(1))) # 1 file
print("{} file{}".format(2, s(2))) # 2 files
hello = lambda user="": print(f"hello {user}")
hello() # hello
print_args = lambda *args: print(args)
print_args(1, 2, 3, 4, 5)
print_kwargs = lambda **kwargs: print(kwargs)
print_kwargs(user="Tom")
可调用对象
Python一切皆对象,因此函数也是成对象。既然如此,函数也可以赋值给变量,也可以作为参数调用函数,也可以作为返回值。比如lambda表达式就经常被赋值给变量或作为参数调用函数。这样的对象,我们称为可调用对象。如下几个实例:
# 函数赋值给变量实例
import math
def heron(a, b, c):
s = (a + b + c) / 2
return math.sqrt(s * (s - a) * (s - b) * (s - c))
area = heron # 将heron函数赋值给area,而不是调用heron后返回值赋值给area
print(area(3, 4, 5), heron(3, 4, 5)) # 都输出6.0,area可以像函数一样调用
# 函数作为参数实例
# 内置函数sorted,有一个参数key用于控制排序时的比较方法
# key参数需要一个可调用对象,这个可调用对象接收一个参数并返回用于比较的值
print("".join(sorted("ABabcC"))) # 输出ABCabc
print("".join(sorted("ABabcC", key=lambda x: x.upper()))) # 输出AaBbcC 忽略大小写比较字符串
# 函数作为返回值实例
def outer():
print("outer")
def inner():
print("inner")
return inner
func = outer() # 输出outer
print("---")
func() # 输出inner
除了函数和lambda表达式以外,还有其他类型的可调用对象,比如实现了__call__
方法的类实例,如下:
import math
class Heron:
def __call__(self, a, b, c):
s = (a + b + c) / 2
return math.sqrt(s * (s - a) * (s - b) * (s - c))
heron = Heron()
print(heron(3, 4, 5)) # 输出6.0
偏函数
偏函数是指用现存函数以及某些参数来创建函数,新建函数与原函数执行的功能相同,但是某些参数是固定的,因此调用者不需要传递这些参数。Python可以使用functools
标准库的partial
函数来实现偏函数,如下是一个将删除ASCII字母后的字符串的排序:
import string
import functools
""" functools.partial(func, /, *args, **kwargs)
"""
# 删除字符串中的指定字符
def remove_character(text, characters=string.ascii_letters):
characters = frozenset(characters)
res = []
for character in text:
if character not in characters:
res.append(character)
return "".join(res)
lst = ["你好,Python", "abcde", "偏函数", "1234"]
# 删除ASCII字母后的字符串排序
remove_letters = functools.partial(remove_character, characters=string.ascii_letters)
print(sorted(lst, key=remove_letters))
# 使用默认参数也可以
print(sorted(lst, key=remove_character))
# 但是如果改为删除数字后的排序,就需要用偏函数了
remove_digits = functools.partial(remove_character, characters=string.digits)
print(sorted(lst, key=remove_digits))
闭包
在一个函数中定义了一个嵌套函数,嵌套函数里运用了外部函数的临时变量,并且外部函数的返回值是嵌套函数的引用,这样就构成了一个闭包。如下:
def outer(user):
lst = [1, 2, 3]
def inner():
print(user)
print(lst)
return inner
func = outer("Tom")
通常来说,函数中的变量为局部变量,一但函数执行完毕,其中的变量就不可用了。但闭包略有不同,如下:
def outer1(user):
lst = [1, 2, 3]
outer1("Tom")
print(user) # NameError: name 'user' is not defined
print(lst) # NameError: name 'lst' is not defined
def outer2(user):
lst = [1, 2, 3]
def inner2():
print(user)
print(lst)
return inner2
func = outer2("Tom")
# Tom
# [1, 2, 3]
func()
可以看到func函数依然可以正常输出user和lst,甚至删除outer2,也可以输出,如下:
del outer2
# Tom
# [1, 2, 3]
func()
这是闭包的特性,形容为外层变量被闭包捕获,而这种被捕获的变量称为自由变量。自由变量取值被保存在函数对象的__closure__
属性中,该属性是一个元组,如下所示:
def outer2(user):
lst = [1, 2, 3]
print(f"user:{hex(id(user))}") # user:0x2936400d970 数值可能不一样
print(f"lst:{hex(id(lst))}") # lst:0x2936400bd40 数值可能不一样
def inner2():
print(user)
print(lst)
return inner2
func = outer2("Tom")
print(func.__closure__)
""" 数值可能不一样,但是和上面的id值一一对应
(<cell at 0x00000282AD466C70: list object at 0x000002936400BD40>,
<cell at 0x00000282AD466790: str object at 0x000002936400D970>)
"""
因为闭包会将自由变量存储到__closure__
中,所以闭包可以持有状态,即自由变量在闭包存在的期间,其中的值也会一直存在,如下:
def outer():
lis = []
def inner(x):
lis.append(x)
print(lis)
return inner
func = outer()
func(1) # [1]
func(2) # [1, 2]
func(3) # [1, 2, 3]
同样因为外部函数每次调用都会创建局部变量,所以闭包与闭包之间的状态是隔离的,如下:
def outer():
lis = []
def inner(x):
lis.append(x)
print(lis)
return inner
func1 = outer()
func2 = outer()
func1(1) # [1]
func1(2) # [1, 2]
func1(3) # [1, 2, 3]
func2(4) # [4]
func2(5) # [4,5]
装饰器
装饰器是一个函数,接受一个函数作为唯一的参数,并返回一个新函数,其中整合了装饰后的函数,并附带了一些额外的功能。如下,添加一个计时功能:
import time
def timing(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{end_time - start_time}")
return result
return wrapper
def loop(sze):
for i in range(sze):
time.sleep(1)
print("执行loop...")
loop(2)
print("结束loop")
loop_time = timing(loop)
print("执行loop_time...")
loop_time(2) # 输出2.0246033668518066 可能略有差异
print("结束loop_time")
可以将装饰器的返回值赋值给被修饰的函数变量名,如下:
loop = timing(loop)
print("执行loop...")
loop(2) # 输出2.022785186767578 可能略有差异
print("结束loop")
Python对于上面这种修饰提供了语法糖,通过在函数名上面使用@decorate_name
即可,如下:
@timing
def loop(sze):
for i in range(sze):
time.sleep(1)
print("执行loop...")
loop(2) # 输出2.0079243183135986 可能略有差异
print("结束loop")
装饰器也可以参数化,我们可以在调用某函数时使用我们需要的参数,之后返回一个装饰器,并用其装饰跟随其后的函数即可,如下:
def bounded(minimum, maximum):
def decorator(function):
def wrapper(*args, **kwargs):
result = func(*args,**kwargs)
if result<minimum:
return minimum
elif result>maximum:
return maximum
return result
return wrapper
return decorator
@bounded(0,100)
def percent(amount, total):
return (amount/total)*100
装饰后的函数的函数名和函数文档字符串也会相应的被改变,如下:
@timing
def loop(sze):
""" loop函数
循环一次休息一秒
"""
for i in range(sze):
time.sleep(1)
# None, wrapper
print(loop.__doc__, loop.__name__)
为了不改变文档字符串和函数名,需要对装饰器函数做出调整,可以手动的修改,也可以使用functools
标准库的wraps
装饰器装饰,如下:
# 手动修改
import time
import functools
def timing(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{end_time - start_time}")
return result
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
@timing
def loop(sze):
""" loop函数
循环一次休息一秒
"""
for i in range(sze):
time.sleep(1)
print(loop.__doc__, loop.__name__)
# 使用functools标准库的wraps装饰器
import time
import functools
def timing(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{end_time - start_time}")
return result
return wrapper
@timing
def loop(sze):
""" loop函数
循环一次休息一秒
"""
for i in range(sze):
time.sleep(1)
print(loop.__doc__, loop.__name__)