Python函数进阶——嵌套函数、可调用对象、偏函数、闭包和装饰器

嵌套函数

嵌套函数定义

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()

上面代码的输出如下图所示,可以看出模块的全局作用域和局部作用域等同,作用域是从变量定义后开始考量的。

globals和locals输出

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()

上面的实例可以看出当我们在函数或嵌套函数内部改变变量值(这里指的是将变量引用的对象改为其他对象)时,并不会影响到全局或外部函数的同名变量的值

如果我们需要改变函数或嵌套函数内部变量值,全局或外部函数的同名变量的值也会跟着改变的情况时,可以使用globalnonlocal关键字。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

如果声明了globalnonlocal但没有找到会产生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)

注意,globalnonlocal一定要在对象使用前声明,否则会产生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函数依然可以正常输出userlst,甚至删除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__)

本文参考

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值