2024 Python3.10 系统入门+进阶(十二):函数入门

函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,不需要编写大量重复的代码。函数可以提前保存起来,并给它起一个独一无二的名字,只要知道它的名字就能使用这段代码。函数还可以接收数据,并根据数据的不同做出不同的操作,最后再把处理结果反馈给我们。本文会介绍 Python 定义和使用函数的基本语法,后续文章中还有很多高级的函数用法(例如 lambda 匿名函数),都会为你一一详解。

一、Python函数(函数定义、函数调用)用法详解

提到函数,大家会想到数学函数吧,函数是数学最重要的一个模块,贯穿整个数学学习过程。在 Python 中,函数的应用非常广泛。在前面我们已经多次接触过函数。例如,用于输出的 print() 函数、用于输入的 input() 函数及用于生成一系列整数的 range() 函数,这些都是 Python 内置的标准函数,可以直接使用。除了可以直接使用的标准函数外,Python 还支持自定义函数。即通过将一段有规律的、重复的代码定义为函数,来达到一次编写、多次调用的目的。使用函数可以提高代码的重复利用率。在 Python 中,函数是一等对象。所谓的一等对象: 在运行时创建 能赋值给变量或数据结构中的元素 能作为参数传给函数 能作为函数的返回结果。在 Python 中,整数、字符串和字典都是一等对象-------没什么特别的,不过,由于一等函数是个非常有用的功能,因此像 JavaScript、Go 和 Java(自 JDK8 起) 这样的流行语言也采用了这种设计,但这些语言都不算是 "函数式语言"

函数的作用小结:

  1. 结构化编程对代码的最基本的封装,一般按照功能组织一段代码
  2. 封装的目的为了复用,减少冗余代码
  3. 代码更加简洁美观、可读易懂

1.1 创建一个函数

创建函数也称为定义函数,可以理解为创建一个具有某种用途的工具。使用 def 关键字实现,具体的语法格式如下:

def functionname([parameterlist]):
    ['''comments''']
    [functionbody]

参数说明:

  1. functionname:函数名称,在调用函数时使用。和标识符命名要求一样,本质其实就是标识符

  2. parameterlist:可选参数,用于指定向函数中传递的参数。如果有多个参数,各参数间使用逗号 , 分隔。如果不指定,则表示该函数没有参数,在调用时也不指定参数。ps: 即使函数没有参数,也必须保留一对空的 "()",否则将抛出异常。

  3. '''comments''':可选参数,表示为函数指定注释,注释的内容通常是说明该函数的功能、要传递的参数的作用等,可以为用户提供友好提示和帮助的内容。在定义函数时,如果指定了 '''comments''' 参数,在 Pycharm 中,鼠标悬空在函数名上,就会显示该函数的帮助信息,如下图所示:

    这些帮助信息就是通过定义的注释提供的。

  4. functionbody:可选参数,用于指定函数体,即该函数被调用后,要执行的功能代码。如果函数有返回值,可以使用 return 语句返回。Python 的函数若没有 return 语句,会隐式返回一个 None 值。

  5. 函数体 "functionbody" 和注释 '''comments''' 相对于 def 关键字必须保持一定的缩进。

示例代码:

# 定义一个空函数
def empty_function():
    pass


# 定义一个根据身高、体重计算BMI指数的函数fun_bmi(),该函数包括3个参数,分别用于指定姓名、身高和体重
def fun_bmi(person, height, weight):
    """
    功能: 根据身高和体重计算BMI指数
    :param person: 姓名
    :param height: 身高,单位: 米
    :param weight: 体重,单位: 千克
    :return: 
    """
    print(person + '的身高:' + str(height) + '米 \t 体重:' + str(weight) + '千克')
    bmi = weight / (height * height)  # 用于计算BMI指数,公式为"体重/身高的平方"
    print(person + '的BMI指数为:' + str(bmi))  # 输出BMI指数
    # 判断身材是否合理
    if bmi < 18.5:
        print('偏瘦 ~@_@~')
    elif bmi < 23.9:
        print('正常 (-_-)')
    elif bmi < 28:
        print('偏胖 ~@_@~')
    else:
        print('肥胖 ^@_@^')

1.2 调用函数

调用函数也就是执行函数。如果把创建的函数理解为创建一个具有某种用途的工具,那么调用函数就相当于使用该工具。调用函数的基本语法格式如下:

functionname([parametersvalue])

参数说明:

  1. functionname:函数名称,要调用的函数名称必须是已经创建好的。
  2. parametersvalue:可选参数,用于指定各个参数的值。如果需要传递多个参数值,则各参数值间使用逗号 "," 分隔。如果该函数没有参数,则直接写一对小括号即可。

调用 1.1 创建一个函数 小节中的 empty_function 函数与 fun_bmi 函数,示例代码如下:

empty_function()
fun_bmi('amo', 1.68, 52)

函数定义,只是声明了一个函数,它不能被执行,需要调用执行。在举一个例子:

def add(x, y):  # 函数定义
    result = x + y  # 函数体
    return result  # 返回值


out = add(4, 5)  # 函数调用,可能有返回值,使用变量接收这个返回值
print(out)  # print函数加上括号也是调用
print(callable(add))  # True

上面代码解释:

  1. 定义一个函数 add,及函数名是 add,能接受 2形式参数(2.1 形式参数和实际参数小节)
  2. 该函数计算的结果,通过返回值返回,需要 return 语句
  3. 调用时,通过函数名 add 后加 2实际参数(2.1 形式参数和实际参数小节), 返回值可使用变量接收
  4. 函数名也是标识符,返回值也是值
  5. 定义需要在调用前,也就是说调用时,已经被定义过了,否则抛 NameError 异常
  6. 函数是可调用的对象,callable(add) 返回 True。callable 函数见小节 1.3.2 callable()函数

1.3 补充:9种可调用对象以及callable()函数

1.3.1 9种可调用对象

除了函数,调用运算符 () 还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的 callable() 函数(见 1.3.2 callable()函数 小节)。数据模型文档列出了自 Python 3.9 起可用的 9 种可调用对象。

  1. 用户定义的函数。使用 def 语句或 lambda 表达式创建的函数
  2. 内置函数。使用 C 语言(Cpython) 实现的函数,例如 len 或 time.strftime
  3. 内置方法。使用 C 语言实现的方法,例如 dict.get
  4. 方法。在类主体中定义的函数
  5. 类。调用类时运行类的 __new__ 方法创建一个实例,然后运行 __init__ 方法,初始化实例,最后再把实例返回给调用方。Python 中没有 new 运算符,调用类就相当于调用函数
  6. 类的实例。如果类定义了 __call__ 方法,那么它的实例可以作为函数调用
  7. 生成器函数。主体中有 yield 关键字的函数或方法。调用生成器函数返回一个生成器对象
  8. 原生协程函数。使用 async def 定义的函数或方法。调用原生协程函数返回一个协程对象,Python 3.5 新增
  9. 异步生成器函数。使用 async def 定义,而且主体中有 yield 关键字的函数或方法,调用异步生成器函数返回一个异步生成器,供 async for 使用,Python 3.6 新增

1.3.2 callable()函数

https://blog.csdn.net/xw1680/article/details/141874922

二、函数参数

2.1 形式参数和实际参数

在调用函数时,大多数情况下,主调函数和被调用函数之间有数据传递关系,这就是有参数的函数形式。函数参数的作用是传递数据给函数使用,函数利用接收的数据进行具体的操作处理。函数参数在定义函数时放在函数名称的后面的一对小括号中,如下图所示:
在这里插入图片描述
在使用函数时,经常会用到形式参数和实际参数,二者都叫作参数,它们的区别将先通过形式参数与实际参数的作用来进行讲解,再通过一个比喻和实例进行深入探讨。

① 通过作用理解。 形式参数和实际参数在作用上的区别如下:

  1. 形式参数:在定义函数时,函数名后面括号中的参数为 "形式参数"
  2. 实际参数:在调用一个函数时,函数名后面括号中的参数为 "实际参数",也就是将函数的调用者提供给函数的参数称为实际参数。通过下图可以更好地理解:
    在这里插入图片描述

根据实际参数的类型不同,可以分为将实际参数的 传递给形式参数和将实际参数的 引用 传递给形式参数两种情况。其中,当实际参数为不可变对象时,进行值传递;当实际参数为可变对象时,进行的是引用传递。实际上,值传递和引用传递的基本区别就是,进行值传递后,改变形式参数的值,实际参数的值不变;而进行引用传递后,改变形式参数的值,实际参数的值也一同改变。例如,定义一个名称为 demo 的函数,然后为 demo() 函数传递一个字符串类型的变量作为参数(代表值传递),并在函数调用前后分别输出该字符串变量,再为 demo() 函数传递一下列表类型的变量作为参数(代表引用传递),并在函数调用前后分别输出该列表。代码如下:

# 定义函数
def demo(obj):
    print("原值:", obj)
    obj += obj


# 调用函数
print("=========值传递========")
mot = "唯有在被追赶的时候,你才能真正地奔跑。"
print("函数调用前:", mot)
demo(mot)  # 采用不可变对象―字符串
print("函数调用后:", mot)
print("=========引用传递 ========")
list1 = ['绮梦', '冷伊一', '香凝', '黛兰']
print("函数调用前:", list1)
demo(list1)  # 采用可变对象―列表
print("函数调用后:", list1)

上面代码的执行结果如下:

2.2 Python函数参数传递机制

Python 中,函数参数由实参传递给形参的过程,是由参数传递机制来控制的。通过学习 2.1 形式参数和实际参数 一节我们知道,根据实际参数的类型不同,函数参数的传递方式分为值传递和引用传递(又称为地址传递),本小节将对这两种传递机制做深度剖析。

2.2.1 Python函数参数的值传递机制

所谓值传递,实际上就是将实际参数值的副本(复制品)传入函数,而参数本身不会受到任何影响。

值传递的方式,类似于《西游记》里的孙悟空,它复制一个假孙悟空,假孙悟空具有的能力和真孙悟空相同,可除妖或被砍头。但不管这个假孙悟空遇到什么事,真孙悟空都不会受到任何影响。与此类似,传入函数的是实际参数值的复制品,不管在函数中对这个复制品如何操作,实际参数值本身不会受到任何影响。

下面程序演示了函数参数进行值传递的效果:

def swap(a, b):
    # 下面代码实现a、b变量的值交换
    a, b = b, a
    print("swap函数里,a的值是", a, ";b的值是", b)


a = 6
b = 9
swap(a, b)
print("交换结束后,变量a的值是", a, ";变量b的值是", b)

运行上面程序,将看到如下运行结果:
在这里插入图片描述
从上面的运行结果来看,在 swap() 函数里,a 和 b 的值分别是 9、6,交换结束后,在 swap() 函数外,变量 a 和 b 的值依然是 6、9。从这个运行结果可以看出,程序中实际定义的变量 a 和 b,并不是 swap() 函数里的 a 和 b 。正如前面所讲的,swap() 函数里的 a 和 b 只是主程序中变量 a 和 b 的复制品。下面通过示意图来说明上面程序的执行过程。上面程序开始定义了 a、b 两个局部变量,这两个变量在内存中的存储示意图如下图所示:
在这里插入图片描述
当程序执行 swap() 函数时,系统进入 swap() 函数,并将主程序中的 a、b 变量作为参数值传入 swap() 函数,但传入 swap() 函数的只是 a、b 的副本,而不是 a、b 本身。进入 swap() 函数后,系统中产生了 4 个变量,这 4 个变量在内存中的存储示意图如下图所示:
在这里插入图片描述
当在主程序中调用 swap() 函数时,系统分别为主程序和 swap() 函数分配两块栈区,用于保存它们的局部变量。将主程序中的 a、b 变量作为参数值传入 swap() 函数,实际上是在 swap() 函数栈区中重新产生了两个变量 a、b,并将主程序栈区中 a、b 变量的值分别赋值给 swap() 函数栈区中的 a、b 参数(就是对 swap() 函数的 a、b 两个变量进行初始化)。此时,系统存在两个 a 变量、两个 b 变量,只是存在于不同的栈区中而己。程序在 swap() 函数中交换 a、b 两个变量的值,实际上是对上图中灰色区域的 a、b 变量进行交换。交换结束后,输出 swap() 函数中 a、b 变量的值,可以看到 a 的值为 9,b 的值为 6,此时在内存中的存储示意图如下图所示:
在这里插入图片描述
对比该图与最开始的图,可以看到两个示意图中主程序栈区中 a、b 的值并未有任何改变,程序改变的只是 swap() 函数栈区中 a、b 的值。这就是值传递的实质:当系统开始执行函数时,系统对形参执行初始化,就是把实参变量的值赋给函数的形参变量,在函数中操作的并不是实际的实参变量。

2.2.2 Python函数参数的引用传递

如果实际参数的数据类型是可变对象(列表、字典),则函数参数的传递方式将采用引用传递方式。需要注意的是,引用传递方式的底层实现,采用的依然还是值传递的方式。下面程序示范了引用传递参数的效果:

def swap(dw):
    # 下面代码实现dw的a、b两个元素的值交换
    dw['a'], dw['b'] = dw['b'], dw['a']
    print("swap函数里,a元素的值是", dw['a'], ";b元素的值是", dw['b'])


dw = {'a': 6, 'b': 9}
swap(dw)
print("交换结束后,a元素的值是", dw['a'], ";b元素的值是", dw['b'])

运行上面程序,将看到如下运行结果:
在这里插入图片描述
从上面的运行结果来看,在 swap() 函数里,dw 字典的 a、b 两个元素的值被交换成功。不仅如此,当 swap() 函数执行结束后,主程序中 dw 字典的 a、b 两个元素的值也被交换了。这很容易造成一种错觉,即在调用 swap() 函数时,传入 swap() 函数的就是 dw 字典本身,而不是它的复制品。但这只是一种错觉,下面还是结合示意图来说明程序的执行过程。程序开始创建了一个字典对象,并定义了一个 dw 引用变量(其实就是一个指针)指向字典对象,这意味着此时内存中有两个东西:对象本身和指向该对象的引用变量。此时在系统内存中的存储示意图如下图所示:
在这里插入图片描述
接下来主程序开始调用 swap() 函数,在调用 swap() 函数时,dw 变量作为参数传入 swap() 函数,这里依然采用值传递方式:把主程序中 dw 变量的值赋给 swap() 函数的 dw 形参,从而完成 swap() 函数的 dw 参数的初始化。值得指出的是,主程序中的 dw 是一个引用变量(也就是一个指针),它保存了字典对象的地址值,当把 dw 的值赋给 swap() 函数的 dw 参数后,就是让 swap() 函数的 dw 参数也保存这个地址值,即也会引用到同一个字典对象。dw 字典传入 swap() 函数后的存储示意图:
在这里插入图片描述
从该图来看,这种参数传递方式是不折不扣的值传递方式,系统一样复制了 dw 的副本传入 swap() 函数。但由于 dw 只是一个引用变量,因此系统复制的是 dw 变量,并未复制字典本身。当程序在 swap() 函数中操作 dw 参数时,由于 dw 只是一个引用变量,故实际操作的还是字典对象。此时,不管是操作主程序中的 dw 变量,还是操作 swap() 函数里的 dw 参数,其实操作的都是它们共同引用的字典对象,它们引用的是同一个字典对象。因此,当在 swap() 函数中交换 dw 参数所引用字典对象的 a、b 两个元素的值后,可以看到在主程序中 dw 变量所引用字典对象的 a、b 两个元素的值也被交换了。为了更好地证明主程序中的 dw 和 swap() 函数中的 dw 是两个变量,在 swap() 函数的最后一行增加如下代码:

# 把 dw 直接赋值为None,让它不再指向任何对象
dw = None

运行上面代码,结果是 swap() 函数中的 dw 变量不再指向任何对象,程序其他地方没有任何改变。主程序调用 swap() 函数后,再次访问 dw 变量的 a、b 两个元素,依然可以输出 9、6。可见,主程序中的 dw 变量没有受到任何影响。实际上,当在 swap() 函数中增加 dw =None 代码后,在内存中的存储示意图如下图所示:
在这里插入图片描述
从上图来看,把 swap() 函数中的 dw 赋值为 None 后,在 swap() 函数中失去了对字典对象的引用,不可再访问该字典对象。但主程序中的 dw 变量不受任何影响,依然可以引用该字典对象,所以依然可以输出字典对象的 a、b 元素的值。

通过上面介绍可以得出如下两个结论:

  1. 不管什么类型的参数,在 Python 函数中对参数直接使用 = 符号赋值是没用的,直接使用 = 符号赋值并不能改变参数。
  2. 如果需要让函数修改某些数据,则可以通过把这些数据包装成列表、字典等可变对象,然后把列表、字典等可变对象作为参数传入函数,在函数中通过列表、字典的方法修改它们,这样才能改变这些数据。

2.3 实参传参方式

函数在定义时要定义好形式参数,调用时也提供足够的实际参数,一般来说,形参和实参个数要一致(可变参数除外)。实参传参方式总的来说分为两种:

  1. 位置传参。 定义时 def f(x, y, z), 调用使用 f(1, 3, 5),按照参数定义顺序传入实参
  2. 关键字传参。 定义时 def f(x, y, z),调用使用 f(x=1, y=3, z=5),使用形参的名字来传入实参的方式,如果使用了形参名字,那么传参顺序就可和定义顺序不同。要求位置参数必须在关键字参数之前传入,位置参数是按位置对应的

示例代码:

def add(x, y):
    print(x)
    print(y)
    print('-' * 30)


add(4, 5)
add(5, 4)  # 按顺序对应,反过来x和y值就不同
add(x=[4], y=(5,))
add(y=5.1, x=4.2)  # 关键字传参,按名字对应,无所谓顺序
add(4, y=5)  # 正确
# add(y=5, 4)  # 错误传参 SyntaxError: positional argument follows keyword argument

2.4 形参定义

切记:传参指的是 调用时 传入实参,就2种方式。下面讲的都是形参定义。

2.4.1 位置参数

位置参数就是根据位置关系把实参的值有序传递给形参。在一般情况下,实参和形参应该是一一对应的,不能错位传递,否则会引发异常或者运行错误。在调用函数时,实参和形参必须保持一致,具体说明如下:

  1. 在没有设置默认参数和可变参数的情况下,实参和形参的个数必须相同
  2. 在没有设置关键字参数和可变参数的情况下,实参和形参的位置必须对应
  3. 在一般情况下,实参和形参的类型必须保持一致

在 Python 中,内置函数会自动检查传入值的个数和类型,如果个数或类型不一致,则会引发异常。例如,调用内置函数 abs(),并传入字符串,则抛出 TypeError 异常。

In [2]: abs('b')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[2], line 1
----> 1 abs('b')

TypeError: bad operand type for abs(): 'str'

对于自定义函数,Python 会自动检查实参个数,如果实参和形参个数不一致,将抛出 TypeError 异常。但是 Python 不会检查传入值的类型与形参类型是否保持一致。

In [3]: def test(a, b):
   ...:     result = a + b
   ...:     print(result)
   ...:

In [4]:

In [4]: test(1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 test(1)

TypeError: test() missing 1 required positional argument: 'b'

如果实参与形参的位置顺序不对应,虽然 Python 不会自动检查,但是容易引发异常或者是逻辑错误。

2.4.2 关键字参数

在调用函数时,实参一般是按顺序传递给形参的。例如:

In [5]: def test(a, b, c):
   ...:     print(f'a={a}')
   ...:     print(f'b={b}')
   ...:     print(f'c={c}')
   ...:

In [6]: test(1, 2, 3)  # 实参和形参位置顺序相同,一一映射
a=1
b=2
c=3

实参 1,2,3 按顺序传入函数,函数能够按顺序把它们分配给形参变量 a,b,c。关键字参数能够打破参数的位置关系,根据关键字映射实现给形参赋值。例如:

In [7]: def test(a, b, c):
   ...:     print(f'a={a}')
   ...:     print(f'b={b}')
   ...:     print(f'c={c}')
   ...:
   ...:
   ...: test(c=3, a=1, b=2)  # 实参和形参位置不一致
a=1
b=2
c=3

关键字参数是针对调用函数时传递的实参而言,而位置参数是针对定义函数时设置的形参而言。 可以混合使用位置参数和关键字参数,一般位置参数在前,关键字参数在后。例如,第 1 个参数直接传递值,第 2、3 个参数使用关键字进行传递。

In [8]: def test(a, b, c):
   ...:     print(f'a={a}')
   ...:     print(f'b={b}')
   ...:     print(f'c={c}')
   ...:
   ...:
   ...: test(1, c=3, b=2)  # 混合传递参数
a=1
b=2
c=3

一旦使用关键字参数后,其后不能够使用位置参数。因为这样会重复为一个形参赋值,应确保形参和实参个数相同。例如,下面用法是错误的:
在这里插入图片描述

2.4.3 形参缺省值

缺省值也称为默认值,可以在函数定义时,为形参增加一个缺省值。其作用:

  1. 参数的默认值可以在未传入足够的实参的时候,对没有给定的参数赋值为默认值
  2. 参数非常多的时候,并不需要用户每次都输入所有的参数,简化函数调用

ps: 使用形参缺省值之后,位置参数必须放在前面,形参缺省值放在后面,位置参数和默认参数没有个数限制。示例:

In [11]: def add(x=4, y=5):
    ...:     return x + y
    ...:

In [12]: # 测试调用

In [13]: add()
Out[13]: 9

In [14]: add(x=5)
Out[14]: 10

In [15]: add(6, 10)
Out[15]: 16

In [16]: add(6, y=7)
Out[16]: 13

In [17]: add(x=5, y=7)
Out[17]: 12

In [18]: add(y=7, x=5)
Out[18]: 12

In [19]: add(x=5, 6)
  Cell In[19], line 1
    add(x=5, 6)
              ^
SyntaxError: positional argument follows keyword argument


In [20]: add(y=8, 4)
  Cell In[20], line 1
    add(y=8, 4)
              ^
SyntaxError: positional argument follows keyword argument


In [21]: add(11, x=20)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[21], line 1
----> 1 add(11, x=20)

TypeError: add() got multiple values for argument 'x'
In [22]: def add(x, y=5):
    ...:     return x + y
    ...:

In [23]: add(5, 5)
Out[23]: 10

In [24]: def add(x=5, y):
    ...:     return x + y
  Cell In[24], line 1
    def add(x=5, y):
                 ^
SyntaxError: non-default argument follows default argument

应避免使用可变对象作为参数的缺省值。 示例:

def _sum(num, scores=[]):
    scores.append(num)
    return scores


result = _sum(12)
print(result)  # [12]
result = _sum(24)
print(result)  # [12, 24]

预设程序运行的结果是 [12]、[24],但是实际结果是 [12]、[12, 24]。出现问题的原因在于 scores 参数的默认值是一个列表对象,而列表是一个可变类型,那么使用 append() 方法添加列表元素时,不会为 scores 重新创建一个新的列表,而是在原来对象的基础上执行操作。因此,对于默认参数,如果默认值是不可变类型,那么多次调用函数是不会相互干扰的;如果默认值是可变参数,那么在调用函数时就要重新初始化可变参数,避免多次调用的相互干扰,最好使用 None 作为可变对象的默认值。

2.4.4 可变参数

需求:写一个函数,可以对多个数累加求和

def get_sum(iterable):
    s = 0
    for x in iterable:
        s += x
    return s


print(get_sum([1, 3, 5]))
print(get_sum(range(4)))

上例,传入可迭代对象,并累加每一个元素。也可以使用可变参数完成上面的函数。

def get_sum(*nums):
    s = 0
    for x in nums:
        s += x
    return s


print(get_sum(1, 3, 5))
print(get_sum(1, 2, 3))

① 可变位置参数。 在形参前使用 * 表示该形参是可变位置参数,可以接受多个实参,它将收集来的实参组织到一个 tuple 中。示例:

def print_coffee(*coffee_name):  # 定义输出我喜欢的咖啡名称的函数
    print('\n我喜欢的咖啡有:')
    for item in coffee_name:
        print(item)  # 输出咖啡名称


# 调用3次上面的函数,分别指定不同的实际参数
print_coffee('蓝山')
print_coffee('蓝山', '卡布奇诺', '土耳其', '巴西', '哥伦比亚')
print_coffee('蓝山', '卡布奇诺', '曼特宁', '摩卡')

② 可变关键字参数。 在形参前使用 ** 表示该形参是可变关键字参数,可以接受多个关键字参数,它将收集来的实参的名称和值,组织到一个 dict 中。示例:

def print_sign(**sign):  # 定义输出姓名和星座的函数
    print()  # 输出一个空行
    for key, value in sign.items():  # 遍历字典
        print("[" + key + "] 的星座是:" + value)  # 输出组合后的信息


print_sign(绮梦='水瓶座', 冷伊一='射手座')
print_sign(香凝='双鱼座', 黛兰='双子座', 冷伊一='射手座')

小结:

  1. 有可变位置参数和可变关键字参数
  2. 可变位置参数在形参前使用一个星号 *,可变关键字参数在形参前使用两个星号 **
  3. 可变位置参数和可变关键字参数都可以收集若干个实参,可变位置参数收集形成一个 tuple,可变关键字参数收集形成一个 dict
  4. 混合使用参数的时候,普通参数需要放到参数列表前面,可变参数要放到参数列表的后面,可变位置参数需要在可变关键字参数之前

示例:

In [25]: def fn(x, y, *args, **kwargs):
    ...:     print(x, y, args, kwargs)
    ...:

In [26]: fn(3, 5, 7, 9, 10, a=1, b='abc')
3 5 (7, 9, 10) {'a': 1, 'b': 'abc'}

In [27]: fn(3, 5)
3 5 () {}

In [28]: fn(3, 5, 7)
3 5 (7,) {}

In [29]: fn(3, 5, a=1, b='abc')
3 5 () {'a': 1, 'b': 'abc'}

In [30]: fn(x=1, y=2, z=3)
1 2 () {'z': 3}

# 错在位置传参必须在关键字传参之前
In [31]: fn(x=3, y=8, 7, 9, a=1, b='abc')
  Cell In[31], line 1
    fn(x=3, y=8, 7, 9, a=1, b='abc')
                                   ^
SyntaxError: positional argument follows keyword argument

# 错在7和9已经按照位置传参了,x=3、y=5有重复传参了
In [32]: fn(7, 9, y=5, x=3, a=1, b='abc')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[32], line 1
----> 1 fn(7, 9, y=5, x=3, a=1, b='abc')

TypeError: fn() got multiple values for argument 'y'

2.4.5 keyword-only参数

先看一段代码:

def fn(*args, x, y, **kwargs):
    print(x, y, args, kwargs, sep='\n', end='\n\n')


# TypeError: fn() missing 2 required keyword-only arguments: 'x' and 'y'
# fn(3, 5)  #
# TypeError: fn() missing 2 required keyword-only arguments: 'x' and 'y'
# fn(3, 5, 7)  #
# TypeError: fn() missing 2 required keyword-only arguments: 'x' and 'y'
# fn(3, 5, a=1, b='abc')  #
fn(3, 5, y=6, x=7, a=1, b='abc')

在 Python3 之后,新增了 keyword-only 参数。keyword-only 参数: 在形参定义时,在一个 * 星号之后,或一个可变位置参数之后,出现的普通参数,就已经不是普通的参数了,称为 keyword-only 参数。示例:

def fn(*args, x):
    print(x, args, sep='\n', end='\n\n')


# TypeError: fn() missing 1 required keyword-only argument: 'x'
# fn(3, 5)  #
# TypeError: fn() missing 1 required keyword-only argument: 'x'
# fn(3, 5, 7)  #
fn(3, 5, x=7)

keyword-only 参数,言下之意就是这个参数必须采用关键字传参。可以认为,上例中,args 可变位置参数已经截获了所有位置参数,其后的变量 x 不可能通过位置传参传入了。思考:def fn(**kwargs, x) 可以吗?

In [34]: def fn(**kwargs, x):
    ...:     print(x, kwargs, sep='\n', end='\n\n')
  Cell In[34], line 1
    def fn(**kwargs, x):
                     ^
SyntaxError: invalid syntax

直接语法错误了。可以认为,kwargs 会截获所有关键字传参,就算写了 x=5,x 也没有机会得到这个值,所以这种语法不存在。keyword-only 参数另一种形式,* 星号后所有的普通参数都成了 keyword-only 参数。

def func(pos1, pos2, *, kwd1, kwd2):
    ...

# ① pos1, pos2: 这些参数是位置参数,可以通过位置传递
# ② kwd1, kwd2: 这些参数只能通过关键字传递,不能通过位置传递

示例:

def greet(name, *, greeting="Hello"):
    print(f"{greeting}, {name}!")


# 正确的调用方式
greet("Alice")  # Hello, Alice!
greet("Alice", greeting="Hi")  # Hi, Alice!
# 错误的调用方式
# TypeError: greet() takes 1 positional argument but 2 were given
greet("Alice", "Hi")

使用场景:

  1. 强制使用关键字:当你希望调用者必须显式指定参数名称时,关键字参数非常有用。
  2. 提高代码可读性:通过强制使用关键字,可以使代码更具自解释性,减少传参错误。
  3. 避免位置冲突:在函数定义中,避免由于位置参数引起的冲突和错误。

2.4.6 Positional-only参数

在 Python 3.8 引入的一项功能中,可以使用 / 来定义 "位置参数"(Positional-only parameters)。这些参数只能通过位置传递,不能通过关键字传递。这种功能可以在你想确保参数只能通过位置传递时很有用。基本语法:

def func(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
    ...
# ① pos1, pos2: 这些参数只能作为位置参数传递,不能通过关键字传递
# ② pos_or_kwd: 这个参数可以通过位置或关键字传递
# ③ kwd1, kwd2: 这些参数只能通过关键字传递

示例:

def greet(name, /, greeting="Hello"):
    print(f"{greeting}, {name}!")


# 正确的调用方式
greet("Alice")  # Hello, Alice!
greet("Alice", "Hi")  # Hi, Alice!
# 错误的调用方式
# TypeError: greet() got some positional-only arguments passed as keyword arguments: 'name'
greet(name="Alice")

何时使用:

  1. API 设计: 当你希望某些参数不能被关键字传递,保证 API 一致性时。
  2. 性能优化: 在某些情况下,通过位置传递参数可以略微提高函数的调用速度。

2.4.7 参数的混合使用

示例:

# 可变位置参数、keyword-only参数、缺省值
def fn1(*args, x=5):
    print(x)
    print(args)


# fn1()  # 等价于fn(x=5)
# fn1(5)
# fn1(x=6)
# fn1(1, 2, 3, x=10)


# 普通参数、可变位置参数、keyword-only参数、缺省值
# 普通参数、可变位置参数、keyword-only参数、缺省值
def fn2(y, *args, x=5):
    print('x={}, y={}'.format(x, y))
    print(args)


# fn2()  # TypeError: fn2() missing 1 required positional argument: 'y'
# fn2(5)
# fn2(5, 6)
# fn2(x=6)  # TypeError: fn2() missing 1 required positional argument: 'y'
# fn2(1, 2, 3, x=10)
# fn2(y=17, 2, 3, x=10)  # SyntaxError: positional argument follows keyword argument
# fn2(1, 2, y=3, x=10)  # TypeError: fn2() got multiple values for argument 'y'
# fn2(y=20, x=30)

# 普通参数、缺省值、可变关键字参数
def fn3(x=5, **kwargs):
    print('x={}'.format(x))
    print(kwargs)

# fn3()
# fn3(5)
# fn3(x=6)
# fn3(y=3, x=10)
# fn3(3, y=10)
# fn3(y=3, z=20)

参数列表参数一般顺序是:positional-only 参数、普通参数、缺省参数、可变位置参数、keyword-only 参数(可带缺省值)、可变关键字参数。注意:

  1. 代码应该易读易懂,而不是为难别人
  2. 请按照书写习惯定义函数参数

示例:

def fn(a, b, /, x, y, z=3, *args, m=4, n, **kwargs):
    print(a, b)
    print(x, y, z)
    print(m, n)
    print(args)
    print(kwargs)
    print('-' * 30)


def connect(host='localhost', user='admin', password='admin', port='3306',
            **kwargs):
    print('mysql://{}:{}@{}:{}/{}'.format(user, password, host, port, kwargs.get('db', 'test')))


connect(db='cmdb')  # 参数的缺省值把最常用的缺省值都写好了
connect(host='192.168.1.123', db='cmdb')
connect(host='192.168.1.123', db='cmdb', password='mysql')

定义最常用参数为普通参数,可不提供缺省值,必须由用户提供。注意这些参数的顺序,最常用的先定义。将必须使用名称的才能使用的参数,定义为 keyword-only 参数,要求必须使用关键字传参,如果函数有很多参数,无法逐一定义,可使用可变参数。如果需要知道这些参数的意义,则使用可变关键字参数收集。

2.5 参数解构

def add(x, y):
    print(x, y)
    return x + y


add(4, 5)
# add((4, 5))  # 可以吗? TypeError: add() missing 1 required positional argument: 'y'
t = 4, 5
add(t[0], t[1])
add(*t)
add(*(4, 5))
add(*[4, 5])
add(*{4, 5})  # 注意有顺序吗?
add(*range(4, 6))
add(*{'a': 10, 'b': 11})  # a b
# add(**{'a': 10, 'b': 11})  # 可以吗?TypeError: add() got an unexpected keyword argument 'a'
add(**{'x': 100, 'y': 110})  # 可以吗?100 110

参数解构:在给函数提供实参的时候,可以在可迭代对象前使用 * 或者 ** 来进行结构的解构,提取出其中所有元素作为函数的实参。使用 * 解构成位置传参,使用 ** 解构成关键字传参,提取出来的元素数目要和参数的要求匹配。示例:

def add(*nums):
    result = 0
    for x in nums:
        result += x
    return result


print(add(1, 2, 3))
print(add(*[1, 3, 5]))
print(add(*range(5)))


# 3.8以后,下面就不可以使用字典解构后的关键字传参了
def add(x, y, /):  # 仅位置形参
    print(x, y)
    return x + y

# TypeError: add() got some positional-only arguments passed as keyword arguments: 'x, y'
# add(**{'x': 10, 'y': 11})

三、函数返回值

到目前为止,我们创建的函数都只是为我们做一些事,做完了就结束。但实际上,有时还需要对事情的结果进行获取。这类似于主管向下级职员下达命令,职员去做,最后需要将结果报告给主管。为函数设置返回值的作用就是将函数的处理结果返回给调用它的程序。在 Python 中,可以在函数体内使用 return 语句为函数指定返回值,该返回值可以是任意类型,并且无论 return 语句出现在函数的什么位置,只要得到执行,就会直接结束函数的执行。语法格式:

return [value]

先看几个例子:

# return语句之后可以执行吗?
def show_plus(x):
    print(x)
    return x + 1
    print('~~end~~')  # return之后会执行吗? This code is unreachable


show_plus(5)


# 多条return语句都会执行吗
def show_plus(x):
    print(x)
    return x + 1
    return x + 2  # This code is unreachable


show_plus(5)


# 下例多个return可以执行吗?
def guess(x):
    if x > 3:
        return "> 3"
    else:
        return "<= 3"


print(guess(10))


# 下面函数执行的结果是什么
def fn(x):
    for i in range(x):
        if i > 3:
            return i
    else:
        print("{} is not greater than 3".format(x))


print(fn(5))  # 打印什么?4
# 3 is not greater than 3
# None
print(fn(3))  # 打印什么?

小结:

  1. Python 函数使用 return 语句返回 "返回值"
  2. 所有函数都有返回值,如果没有 return 语句,隐式调用 return None
  3. return 语句并不一定是函数的语句块的最后一条语句
  4. 一个函数可以存在多个 return 语句,但是只有一条可以被执行。如果没有一条 return 语句被执行到,隐式调用 return None
  5. 如果有必要,可以显示调用 return None,可以简写为 return
  6. 如果函数执行了 return 语句,函数就会返回,当前被执行的 return 语句之后的其它语句就不会被执行了
  7. 返回值的作用:结束函数调用、返回 "返回值"

能够一次返回多个值吗?

def show_values():
    return 1, 3, 5


show_values()  # 返回了多个值吗?
# ① 函数不能同时返回多个值
# ② return 1, 3, 5 看似返回多个值,隐式的被python封装成了一个元组
# ③ x, y, z = show_values() 使用解构提取返回值更为方便

四、函数作用域

4.1 作用域

一个标识符的可见范围,这就是标识符的作用域。一般常说的是变量的作用域。变量的作用域是指程序代码能够访问该变量的区域,如果超出该区域,再访问时就会出现错误。示例:

def foo():
    x = 100


print(x)  # 可以访问到吗

上例中 x 不可以访问到,会抛出异常(NameError: name 'x' is not defined),原因在于函数是一个封装,它会开辟一个作用域,x 变量被限制在这个作用域中,所以在函数外部 x 变量不可见。注意: 每一个函数都会开辟一个作用域。

4.2 作用域分类

在程序中,一般会根据变量的 "有效范围" 分为 "全局作用域""局部作用域"。全局作用域下的变量为 "全局变量",局部作用域下的变量为 "局部变量"

  1. 全局作用域。 在整个程序运行环境中都可见,全局作用域中的变量称为全局变量 global
  2. 局部作用域。 在函数、类等内部可见,局部作用域中的变量称为局部变量,其使用范围不能超过其所在局部作用域,也称为本地作用域 local

示例:

# 局部变量
def fn1():
    x = 1  # 局部作用域,x为局部变量,使用范围在fn1内


def fn2():
    print(x)  # x能打印吗?可见吗?为什么?


print(x)  # x能打印吗?可见吗?为什么?

# 全局变量
x = 5  # 全局变量,也在函数外定义


def foo():
    print(x)  # 可见吗?为什么?


foo()

一般来讲外部作用域变量可以在函数内部可见,可以使用,反过来,函数内部的局部变量,不能在函数外部看到。

五、嵌套函数

在一个函数中定义了另外一个函数。示例:

def outer():
    def inner():
        print("inner")

    inner()
    print("outer")


outer()  # 可以吗?可以
# inner()  # 可以吗?不可以 NameError: name 'inner' is not defined. Did you mean: 'iter'?

内部函数 inner 不能在外部直接使用,会抛 NameError 异常,因为它在函数外部不可见。其实,inner 不过就是一个标识符,就是一个函数 outer 内部定义的变量而已。

5.1 嵌套结构的作用域

对比下面嵌套结构,代码执行的效果:

def outer1():
    o = 65

    def inner():
        print('inner', o, chr(o))

    inner()
    print('outer', o, chr(o))


outer1()  # 执行后,打印什么


def outer2():
    o = 65

    def inner():
        o = 97
        print('inner', o, chr(o))

    inner()
    print('outer', o, chr(o))


outer2()  # 执行后,打印什么

程序运行结果如下图所示:
在这里插入图片描述
从执行的结果来看:外层变量在内部作用域可见,内层作用域 inner 中,如果定义了 o = 97 ,相当于在当前函数 inner 作用域中重新定义了一个新的变量 o,但是,这个 o 并不能覆盖掉外部作用域 outer2 中的变量 o。只不过对于 inner 函数来说,其只能可见自己作用域中定义的变量 o 了。关于示例代码中的 chr() 函数可以参考文章 Python 常用内置函数详解(三): 类型转换相关函数bin()函数、bool()函数、chr()函数等详解 进行学习。

5.2 一个赋值语句的问题

看下面 foo1 与 foo2 这 2 个函数:

x = 5


def foo1():
    print(x)


foo1()


def foo2():
    y = x + 1
    print(y)
    x += 1  # 打开这一句报错吗?为什么?换成x=1行吗
    print(x)


foo2()

foo1 函数正常执行,函数外部的变量在函数内部可见。foo2 执行错误,为什么?难道函数内部又不可见了?y = x + 1 可以正确执行,可是为什么 x += 1 却不能正确执行?仔细观察 foo2 返回的错误指向 x += 1,原因是什么呢?
在这里插入图片描述
原因分析:x += 1 其实是 x = x + 1,只要有 x= 出现,这就是赋值语句。相当于在 foo2 内部定义一个局部变量 x,那么 foo2 内部所有 x 都是这个局部变量 x 了,x = x + 1 相当于使用了局部变量 x,但是这个 x 还没有完成赋值,就被右边拿来做加 1 操作了。如何解决这个常见问题?

5.3 global语句

x = 5


def foo():
    global x  # 全局变量
    x += 1
    print(x)


foo()

使用 global 关键字的变量,将 foo 内的 x 声明为使用外部的全局作用域中定义的 x,全局作用域中必须有 x 的定义,如果全局作用域中没有 x 定义会怎样?

# 有错吗?
def foo():
    global x
    x += 1
    print(x)


foo()  # 报错 NameError: name 'x' is not defined


# 有错吗?
def foo():
    global x
    x = 10
    x += 1
    print(x)


foo()
print(x)  # 可以吗 可以

使用 global 关键字定义的变量,虽然在 foo 函数中声明,但是这将告诉当前 foo 函数作用域,这个 x 变量将使用外部全局作用域中的 x。即使是在 foo 中又写了 x = 10 ,也不会在 foo 这个局部作用域中定义局部变量 x 了。使用了 global,foo 中的 x 不再是局部变量了,它是全局变量。

小结:

  1. x+=1 这种是特殊形式产生的错误的原因?先引用后赋值,而 python 动态语言是赋值才算定义,才能被引用。解决办法,在这条语句前增加 x=0 之类的赋值语句,或者使用 global 告诉内部作用域,去全局作用域查找变量定义
  2. 内部作用域使用 x = 10 之类的赋值语句会重新定义局部作用域使用的变量 x,但是,一旦这个作用域中使用 global 声明 x 为全局的,那么 x=5 相当于在为全局作用域的变量 x 赋值

global 使用原则:

  1. 外部作用域变量会在内部作用域可见,但也不要在这个内部的局部作用域中直接使用,因为函数的目的就是为了封装,尽量与外界隔离
  2. 如果函数需要使用外部全局变量,请尽量使用函数的形参定义,并在调用传实参解决
  3. 一句话:尽量不使用 global。学习它就是为了深入理解变量作用域

六、闭包

在 Python 中,闭包(Closure)是一种特殊的函数。它指的是一个函数在其外部函数的作用域内被定义,并且能够访问这个外部函数作用域中的变量,即使外部函数已经执行完毕且其作用域已经结束。闭包的关键概念是:函数可以记住并访问其定义时所处的作用域,即使这个作用域在函数调用时已经不存在。

闭包的构成:

  1. 嵌套函数:在一个函数内部定义的另一个函数。
  2. 自由变量:嵌套函数中使用的来自外部函数的变量。
  3. 外部函数的返回值是嵌套函数:外部函数返回其内部定义的函数,使得这个内部函数形成了闭包。

示例:

def counter():
    c = [0]

    def inc():
        c[0] += 1  # 报错吗? 为什么 # line 5
        return c[0]

    return inc


foo = counter()  # line 11
print(foo(), foo())  # line 12
c = 100
print(foo())  # line 14

代码分析:

  1. 第 11 行会执行 counter 函数并返回 inc 对应的函数对象,注意这个函数对象并不释放,因为有 foo 记着
  2. 第 5 行会报错吗?为什么。不会报错,c 已经在 counter 函数中定义过了。而且 inc 中的使用方式是为 c 的元素修改值,而不是重新定义 c 变量
  3. 第 12 行打印什么结果?打印 1 2
  4. 第 14 行打印什么结果?打印 3。第 13 行的 c 和 counter 中的 c 不一样,而 inc 引用的是自由变量,正是 counter 中的变量 c

这是 Python 2 中实现闭包的方式,Python 3 还可以使用 nonlocal 关键字。再看下面这段代码,会报错吗?使用 global 能解决吗?

def counter():
    count = 0

    def inc():
        count += 1
        return count

    return inc


foo = counter()
print(foo(), foo())

上例一定出错,使用 gobal 可以解决:

def counter():
    global count
    count = 0

    def inc():
        global count
        count += 1
        return count

    return inc


foo = counter()
print(foo(), foo())  # 1 2
count = 100
print(foo())  # 打印几?101

上例使用 global 解决,这是全局变量的实现,而不是闭包了。如果要对这个普通变量使用闭包,Python 3 中可以使用 nonlocal 关键字。闭包比普通的函数多了一个 __closure__ 属性,该属性记录着自由变量的地址。当闭包被调用时,系统就会根据该地址找到对应的自由变量,完成整体的函数调用。以 nth_power() 为例,当其被调用时,可以通过 __closure__ 属性获取自由变量(也就是程序中的 exponent 参数)存储的地址,例如:

def nth_power(exponent):
    def exponent_of(base):
        return base ** exponent

    return exponent_of


square = nth_power(2)
# 查看 __closure__ 的值
print(square.__closure__)  # (<cell at 0x0000022A94686F20: int object at 0x0000022A94520110>,)

可以看到,显示的内容是一个 int 整数类型,这就是 square 中自由变量 exponent 的初始值。还可以看到,__closure__ 属性的类型是一个元组,这表明闭包可以支持多个自由变量的形式。

七、nonlocal语句

nonlocal: 将变量标记为不在本地作用域定义,而是在上级的某一级局部作用域中定义,但不能是全局作用域中定义。示例代码:

def counter():
    count = 0

    def inc():
        nonlocal count  # 声明变量count不是本地变量
        count += 1
        return count

    return inc


foo = counter()
print(foo(), foo())  # 1 2
count = 100
print(count)  # 100
print(foo())  # 3

count 是外层函数的局部变量,被内部函数引用。内部函数使用 nonlocal 关键字声明,count 变量在上级作用域而非本地作用域中定义。代码中内层函数引用外部局部作用域中的自由变量,形成闭包。错误示例:

def counter():
    nonlocal a # SyntaxError: no binding for nonlocal 'a' found 上一级作用域是全局作用域
    a += 1
    print(a)
    count = 0

    def inc():
        nonlocal count
        count += 1
        return count

    return inc


foo = counter()
foo()
foo()

上例是错误的,nonlocal 声明变量 a 不在当前作用域,但是往外就是全局作用域了,所以错误。

八、函数的销毁

定义一个函数就是生成一个函数对象,函数名指向的就是函数对象。可以使用 del 语句删除函数,使其引用计数减 1。可以使用同名标识符覆盖原有定义,本质上也是使其引用计数减 1。Python 程序结束时,所有对象销毁。函数也是对象,也不例外,是否销毁,还是看引用计数是否减为 0

九、变量名解析原则LEGB

在 Python 中,作用域可以分为四种类型,简单说明如下:

  1. L(local)级:局部作用域。 每当函数被调用时都会创建一个新的局部作用域,包括 def 函数和 lambda 表达式函数。如果是递归函数,每次调用也都会创建一个新的局部作用域。在函数体内,除非使用 global 关键字声明变量的作用域为全局作用域,否则默认都为局部变量。局部作用域不会持续存在,存在的时间依赖于函数的生命周期。所以,一般建议尽量避免定义全局变量,因为全局变量在模块运行的过程中会一直存在,并一直占用内存空间。
  2. E(enclosing)级:嵌套作用域。 嵌套作用域也是函数作用域,与局部作用域是相对关系。相对于上一层的函数而言,嵌套作用域也是局部作用域。对于一个函数而言,L 表示定义在此函数内部的局部作用域,而 E 是定义在此函数的上一层父级函数中的局部作用域。
  3. G(global)级:全局作用域。 每一个模块都是一个全局作用域,在模块中声明的变量都具有全局作用域。从外部来看,全局就是一个模块对象的属性。注意: 全局作用域的作用范围仅限于单个模块文件内。
  4. B(built-in)级: 内置作用域。在系统内置模块里定义的变量,如预定义在 built-in 模块内的变量。

变量名的 LEGB 解析规则: 当在函数中使用未确定的变量名时,Python 会按照优先级次搜索 4 个作用域,以此来确定该变量名的意义。

# 局部作用域>嵌套作用域>全局作用域>内置作用域

具体解析步骤如下:

  1. 在局部作用域(L)中搜索变量
  2. 如果在局部作用域中没有找到变量,则跳转到上一层嵌套结构中,访问 def 或 lambda 函数的嵌套作用域(E)
  3. 如果在函数作用域中没有找到同名变量,则向上访问全局作用域(G)
  4. 如果在全局作用域中也没有找到同名变量,最后访问内置作用域(B)。

根据上述顺序,在第一处找到的位置停止搜索,并读取变量的值。如果在整个作用域链上都没到,则会抛出 NameError 异常。

【示例1】嵌套函数与 nonlocal 的结合使用。

x = "global"


def outer():
    x = "enclosing outer"

    def inner():
        x = "local inner"

        def innermost():
            nonlocal x
            x = "modified by innermost"
            print("innermost:", x)  # 输出: modified by innermost

        innermost()
        print("inner:", x)  # 输出: modified by innermost

    inner()
    print("outer:", x)  # 输出: enclosing outer


outer()
print("global:", x)  # 输出: global

【示例2】多个嵌套作用域和全局变量。

x = "global"


def outer():
    x = "enclosing outer"

    def inner():
        nonlocal x
        x = "modified by inner"

        def innermost():
            global x
            x = "modified by innermost"
            print("innermost:", x)  # 输出: modified by innermost

        innermost()
        print("inner:", x)  # 输出: modified by inner

    inner()
    print("outer:", x)  # 输出: modified by inner


outer()
print("global:", x)  # 输出: modified by innermost

【示例3】函数参数与局部变量的冲突。

x = "global"


def outer(x):
    def inner():
        print("inner:", x)  # 输出: passed to outer

    inner()
    x = "modified in outer"
    print("outer:", x)  # 输出: modified in outer


outer("passed to outer")
print("global:", x)  # 输出: global

【示例4】复杂嵌套与作用域查找的顺序。

x = "global"


def outer():
    x = "enclosing outer"

    def middle():
        x = "enclosing middle"

        def inner():
            print("inner:", x)  # 输出: enclosing middle

        inner()

    middle()
    print("outer:", x)  # 输出: enclosing outer


outer()
print("global:", x)  # 输出: global

【示例5】函数调用链与不同作用域。

x = "global"

def a():
    x = "enclosing a"
    
    def b():
        x = "enclosing b"
        
        def c():
            x = "local c"
            print("c:", x)  # 输出: local c
        
        c()
        print("b:", x)  # 输出: enclosing b
    
    b()
    print("a:", x)  # 输出: enclosing a

a()
print("global:", x)  # 输出: global

十、globals()函数与locals()函数

globals() 和 locals() 是 Python 提供的两个内置函数,用于访问全局和局部命名空间中的变量。它们分别返回当前全局和局部作用域中的变量字典。理解并使用这两个函数可以帮助我们更好地管理和调试代码中的变量。

10.1 globals()函数

globals() 返回一个包含当前全局命名空间中所有变量的字典。这个字典的键是变量名,值是变量的值。使用场景: 当需要动态地获取、修改或添加全局变量时,可以使用 globals()。示例:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-04 9:00
# @Author  : AmoXiang
# @File: globals_demo.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

x = 10
y = 20


def example():
    global x
    x = 30
    print("In function, x:", x)
    print("Globals:", globals())


example()
print("Outside function, x:", x)

10.2 locals()函数

locals() 返回一个包含当前局部命名空间中所有变量的字典。这个字典的键是变量名,值是变量的值。使用场景: 当需要动态地获取局部变量,或者在函数内部调试时,使用 locals() 可以查看当前作用域内的变量。示例:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-04 9:03
# @Author  : AmoXiang
# @File: locals_demo.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680


def example():
    a = 10
    b = 20
    print("Locals inside function:", locals())
    return a + b


result = example()
print("Result:", result)

可以同时使用 globals() 和 locals() 来查看不同作用域中的变量。

x = 100


def outer():
    y = 200

    def inner():
        z = 300
        print("Locals in inner():", locals())  # 只显示 z
        print("Globals in inner():", globals())  # 显示 x 和函数定义等全局变量

    inner()
    print("Locals in outer():", locals())  # 显示 y 和 inner
    print("Globals in outer():", globals())  # 显示 x 和 outer 函数


outer()

使用 globals() 可以动态地修改全局变量,而使用 locals() 直接修改局部变量通常不会生效,因为 locals() 返回的字典在函数执行时只读。

# 修改全局变量
x = 5


def modify_global():
    globals()['x'] = 10


modify_global()
print("Modified global x:", x)  # 输出: 10


# 尝试修改局部变量
def modify_local():
    a = 5
    locals()['a'] = 10
    # 因为在函数执行过程中,locals() 返回的字典是只读的。
    print("Inside function, a:", a)  # 输出: 5, 并未修改成功


modify_local()

使用 globals() 可以在运行时动态创建全局变量:

def create_global_var():
    globals()['new_var'] = "I'm a new global variable"


create_global_var()
print(new_var)  # 输出: I'm a new global variable

小结:

  1. globals() 返回当前全局作用域中的变量字典,可以用于动态地获取、修改和创建全局变量。
  2. locals() 返回当前局部作用域中的变量字典,可以用于查看局部变量,通常在调试时非常有用。
  3. 虽然可以使用 globals() 动态修改全局变量,但 locals() 在函数执行过程中返回的字典是只读的,因此直接修改局部变量值通常不会生效。
  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Amo Xiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值