【Python 初级函数详解】—— 参数沙漠与作用域丛林的求生指南

欢迎来到ZyyOvO的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡

本文由ZyyOvO原创✍️,感谢支持❤️!请尊重原创📩!欢迎评论区留言交流🌟

个人主页 👉 ZyyOvO

本文专栏➡️Python 算法研究所

快速复习👉【Python 速览 】 ------ 课前甜点,打开你的味蕾

课前导入

我们知道数学中的函数,我们输入一个数,在通过对应的映射关系得到另一个数,如下图给出了两个简单的数学函数:

什么是函数

那在Python编程中函数是什么呢?

在编程中,函数(Function) 是一段被命名、可重复使用的代码块,用于执行特定任务,它通过接收输入(参数),处理逻辑,并返回输出(结果),将复杂的程序拆分为模块化的组件,让代码更简洁、高效且易于维护。

函数的优势

在 Python 中,函数是编程的核心工具之一,它通过将代码逻辑封装为可重复使用的模块,显著提升了代码的可维护性、复用性和可读性。

避免代码重复:DRY 原则(Don't Repeat Yourself)

  • 问题场景: 当同一段代码需要多次使用时,复制粘贴会导致代码臃肿,且后续修改需要逐一调整所有副本。
  • 函数解决方案: 将重复代码封装为函数,通过一次定义、多次调用来实现逻辑复用。
python 复制代码
# 重复代码示例(计算圆面积)
radius1 = 5
area1 = 3.14 * radius1 ** 2

radius2 = 7
area2 = 3.14 * radius2 ** 2

# 使用函数优化
def calculate_circle_area(radius):
    return 3.14 * radius ** 2

area1 = calculate_circle_area(5)
area2 = calculate_circle_area(7)
  • 修改逻辑只需调整函数内部代码(例如将 3.14 改为 math.pi)。
  • 减少代码量,避免冗余。

模块化编程:分解复杂问题

  • 问题场景: 一个大型程序如果写成连续的代码块,会难以理解和维护。
  • 函数解决方案: 将程序拆分为多个函数,每个函数负责单一职责。
python 复制代码
# 未使用函数的复杂逻辑
data = [1, 2, 3, 4, 5]
sum = 0
for num in data:
    sum += num
average = sum / len(data)
print(f"平均值: {average}")

# 使用函数模块化
def calculate_average(data):
    return sum(data) / len(data)

data = [1, 2, 3, 4, 5]
print(f"平均值: {calculate_average(data)}")
  • 代码逻辑清晰,每个函数聚焦一个子任务。
  • 便于团队协作开发。

提升代码可读性

  • 问题场景: 长段代码缺乏注释时,阅读者需要逐行理解逻辑。
  • 函数解决方案: 通过函数名称直接表明代码意图。
python 复制代码
# 难读的代码
x = 5
y = 10
result = (x ** 2 + y ** 2) ** 0.5

# 使用函数自解释
def calculate_hypotenuse(a, b):
    return (a**2 + b**2) ** 0.5

result = calculate_hypotenuse(5, 10)
  • 函数名(如 calculate_hypotenuse)直接说明功能。
  • 减少对注释的依赖。

参数化与灵活性

  • 问题场景: 相似逻辑需要处理不同输入数据时,硬编码值会限制灵活性。
  • 函数解决方案: 通过参数动态接收输入,返回处理结果。
python 复制代码
# 硬编码的欢迎语
print("欢迎,张三!")
print("欢迎,李四!")

# 参数化函数
def greet(name):
    print(f"欢迎,{name}!")

greet("张三")
greet("李四")
  • 同一函数适应不同输入。
  • 支持动态数据处理。

错误隔离与调试效率

  • 问题场景: 代码错误可能出现在任意位置,调试时需全局排查。
  • 函数解决方案: 将代码划分为函数后,错误通常能被隔离到特定函数内。
python 复制代码
def load_data(filename):
    # 假设此处可能抛出文件不存在的错误
    with open(filename, 'r') as f:
        return f.read()

def process_data(data):
    # 此处可能处理数据时出错
    return data.upper()
    
try:
    data = load_data("data.txt")
    result = process_data(data)
except Exception as e:
    print(f"错误发生在: {e}")
  • 快速定位问题函数。
  • 通过单元测试单独验证每个函数。

支持代码复用与库开发

  • 问题场景: 通用功能(如数据验证、数学计算)需要在多个项目中重复实现。
  • 函数解决方案: 将通用函数封装为模块(.py 文件),供其他程序导入。
python 复制代码
# utils.py
def is_prime(n):
    if n <= 1:
        return False
    for i in range(2, int(n**0.5)+1):
        if n % i == 0:
            return False
    return True

# main.py
from utils import is_prime
print(is_prime(17))  # 输出: True
  • 积累个人代码库,提升开发效率。
  • 促进开源社区协作(如 numpyrequests 等库)。

实现递归算法

  • 问题场景: 某些问题(如阶乘、斐波那契数列)天然适合递归解决。
  • 函数解决方案: 函数可以调用自身,简化递归逻辑。
python 复制代码
def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)

print(factorial(5))  # 输出: 120
  • 以简洁方式解决复杂递归问题。
  • 代码更接近数学定义。
应用场景 函数的核心作用
代码重复 封装重复逻辑,实现 DRY 原则
复杂问题 分解为模块化组件,降低认知负担
可读性差 通过命名自解释代码意图
动态数据处理 参数化输入,提升灵活性
错误调试 隔离问题范围,简化排查流程
代码复用 构建可共享的代码库
递归问题 实现自我调用的算法逻辑

通过函数,Python 代码从"一次性脚本"升级为可维护、可扩展的工程化项目。它是构建大型应用、开源库和高效算法的基石。


定义函数

例如,我们写一段代码来求斐波那契数列:

python 复制代码
def fib(n):    # 打印小于 n 的斐波那契数列
    """Print a Fibonacci series less than n."""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b

定义 函数使用关键字 def,后跟函数名与括号内的形参列表。函数语句从下一行开始,并且必须缩进

函数内的第一条语句是字符串时,该字符串就是文档字符串,也称为 docstring。Python 开发者最好养成在代码中加入文档字符串的好习惯。

函数在 执行 时使用函数局部变量符号表,所有函数变量赋值都存在局部符号表中;引用变量时,首先,在局部符号表里查找变量,然后,是外层函数局部符号表,再是全局符号表,最后是内置名称符号表。因此,尽管可以引用全局变量和外层函数的变量,但最好不要在函数内直接赋值(除非是 global 语句定义的全局变量,或 nonlocal 语句定义的外层函数变量)。


函数对象的概念

在调用函数时会将实际参数(实参)引入到被调用函数的局部符号表中;因此,实参是使用 按值调用 来传递的(其中的 值 始终是对象的 引用 而不是对象的值)。 当一个函数调用另外一个函数时,会为该调用创建一个新的局部符号表。

在Python中遵循着一切皆对象的原则,无论是变量,函数,亦或是类,在Python看来都是对象,因此,函数定义在当前符号表中把函数名与函数对象关联在一起。解释器把函数名指向的对象作为用户自定义函数。

还可以使用其他名称指向同一个函数对象,并访问访该函数,例如我们上面写的求斐波那契数列的函数:

python 复制代码
def fib(n):    # 打印小于 n 的斐波那契数列
    """Print a Fibonacci series less than n."""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b

f = fib #将函数对象赋值给f

f(100) #调用f
print()
fib(100) #调用fib()

输出:

python 复制代码
0 1 1 2 3 5 8 13 21 34 55 89 
0 1 1 2 3 5 8 13 21 34 55 89 

如果你学过其他语言,比如C语言或者C++,你可能会认为 fib 不是函数而是一个过程语句,因为它没有返回值。 事实上,即使没有 return 语句的函数也有返回值, 这个值被称为 None (是一个内置名称,Python中函数的默认返回值)。 通常解释器会屏蔽单独的返回值 None。 如果你确有需要可以使用 print() 查看它:

python 复制代码
print(fib(0))
#输出
None

编写不直接输出斐波那契数列运算结果,而是返回运算结果列表的函数也非常简单:只需要在函数中定义列表,将斐波那契数使用append接口加入到列表中,最后返回列表对象即可:

python 复制代码
def fib2(n):  # 返回斐波那契数组直到 n
    """Return a list containing the Fibonacci series up to n."""
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)    # 见下
        a, b = b, a+b
    return result

f100 = fib2(100)    # 调用它
f100                # 输出结果
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

函数定义详解

函数定义支持可变数量的参数。这里列出三种可以组合使用的形式。

默认值参数

为参数指定默认值是非常有用的方式。调用函数时,可以使用比定义时更少的参数,例如:

python 复制代码
def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        reply = input(prompt)
        if reply in {'y', 'ye', 'yes'}:
            return True
        if reply in {'n', 'no', 'nop', 'nope'}:
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)

该函数有三种调用方式:

  • 只给出必选实参:ask_ok('Do you really want to quit?')
  • 给出一个可选实参:ask_ok('OK to overwrite the file?', 2)
  • 给出所有实参:
python 复制代码
ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or
   no!')

retriesreminder默认参数 。默认参数在函数定义时指定了默认值,调用函数时如果没有为这些参数提供值,就会使用默认值。而prompt位置参数,调用函数时必须按照顺序传递参数,否则就会导致错误!

使用示例:

python 复制代码
try:#异常捕获
    result = ask_ok("Do you want to continue? ")
    if result:
        print("You agreed!")
    else:
        print("You disagreed!")
except ValueError as e:
    print(e)

默认值在 定义 作用域里的函数定义中求值 所以:

python 复制代码
i = 5

def f(arg=i):
    print(arg)

i = 6
f()

上例输出的是 5。为什么呢???

这里创建了一个全局变量 i,并将其赋值为 5。在定义函数 f 时,默认参数 arg 的值被设定为当前全局变量 i 的值,也就是 5。需要注意的是,Python 函数的默认参数值是在函数定义时就确定下来的,而不是在函数调用时确定。

将全局变量 i 的值修改为 6,但这并不会影响函数 f 中默认参数 arg 的值,因为 arg 的默认值在函数定义时就已经确定为 5 了。

值得注意的是: 默认值只计算一次。默认值为列表、字典或类实例等可变对象时,会产生与该规则不同的结果。例如,下面的函数会累积后续调用时传递的参数:

python 复制代码
def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

输出结果如下:

python 复制代码
[1]
[1, 2]
[1, 2, 3]

这里定义了函数 f,其中参数 L 的默认值是一个空列表 []。需要特别注意的是,在 Python 中,函数的默认参数是在函数定义时就被创建的,并且只创建一次。 也就是说,无论函数被调用多少次,这个默认的列表 L 始终是同一个对象。

如果不想在后续调用之间共享默认值时,应以如下方式编写函数:

python 复制代码
def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

这样修改后,每次调用函数时,如果没有传入 L 参数,都会创建一个新的空列表,输出结果将分别为:

python 复制代码
[1]
[2]
[3]

关键字参数

key=value 形式的 关键字参数 也可以用于调用函数。函数示例如下:

python 复制代码
def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")

该函数接受一个必选参数(voltage)和三个可选参数(state, action 和 type)。该函数可用下列方式调用:

python 复制代码
parrot(1000)                                          # 1 个位置参数
parrot(voltage=1000)                                  # 1 个关键字参数
parrot(voltage=1000000, action='VOOOOOM')             # 2 个关键字参数
parrot(action='VOOOOOM', voltage=1000000)             # 2 个关键字参数
parrot('a million', 'bereft of life', 'jump')         # 3 个位置参数
parrot('a thousand', state='pushing up the daisies')  # 1 个位置参数,1 个关键字参数

以下调用函数的方式都无效:

python 复制代码
parrot()                     # 缺失必需的参数
parrot(voltage=5.0, 'dead')  # 关键字参数后存在非关键字参数
parrot(110, voltage=220)     # 同一个参数重复的值
parrot(actor='John Cleese')  # 未知的关键字参数

函数调用时,关键字参数必须跟在位置参数后面.

例如,以下调用方式是错误的:

python 复制代码
# 错误示例,位置参数应在关键字参数之前
parrot(state='dead', 300)

所有传递的关键字参数都必须匹配一个函数接受的参数(比如,actor 不是函数 parrot 的有效参数),关键字参数的顺序并不重要。这也包括必选参数,(比如,parrot(voltage=1000) 也有效)。不能对同一个参数多次赋值,下面就是一个因此限制而失败的例子:

python 复制代码
def function(a):
    pass

function(0, a=0)

最后一个形参为 **name 形式时,接收一个字典,(映射类型 --- dict),该字典包含与函数中已定义形参对应之外的所有关键字参数。**name 形参可以与 *name 形参(下一小节介绍)组合使用(*name 必须在 **name 前面), *name 形参接收一个 元组,该元组包含形参列表之外的位置参数。例如,可以定义下面这样的函数:

python 复制代码
def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])

参数解释:

  • kind:这是一个普通的位置参数,调用函数时必须提供该参数的值,用于指定奶酪的种类。
  • *arguments:这是一个可变位置参数,它会将函数调用时传入的除了第一个位置参数(即 kind)之外的所有位置参数收集到一个元组中。在函数内部,可以通过遍历这个元组来处理这些参数。
  • **keywords:这是一个可变关键字参数,它会将函数调用时传入的所有未在函数定义中明确指定的关键字参数收集到一个字典中。字典的键是关键字参数的名称,值是对应的参数值。

该函数可以用如下方式调用:

python 复制代码
cheeseshop("Limburger", "It's very runny, sir.",
           "It's really very, VERY runny, sir.",
           shopkeeper="Michael Palin",
           client="John Cleese",
           sketch="Cheese Shop Sketch")

第一个参数 "Limburger" 被赋值给 kind

接下来的两个字符串 "It's very runny, sir.""It's really very, VERY runny, sir." 被收集到 arguments 元组中。

最后三个关键字参数 shopkeeper="Michael Palin"、client="John Cleese"sketch="Cheese Shop Sketch" 被收集到 keywords 字典中。

输出结果如下:

python 复制代码
-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir.
It's really very, VERY runny, sir.
----------------------------------------
shopkeeper : Michael Palin
client : John Cleese
sketch : Cheese Shop Sketch

在 Python 中,*arguments(可变位置参数)必须放在 **keywords(可变关键字参数)之前,否则会导致语法错误,为什么呢???

Python 函数在调用时,会按照特定的顺序解析传入的参数,具体顺序为:

bash 复制代码
位置参数 -> 可变位置参数 -> 关键字参数 -> 可变关键字参数。
  • 位置参数:是按照参数定义的顺序依次接收传入的值。
  • 可变位置参数:会收集所有剩余的位置参数(即除了已被普通位置参数接收的部分)到一个元组中。
  • 关键字参数:通过参数名来指定传入的值。
  • 可变关键字参数:会收集所有未在函数定义中明确指定的关键字参数到一个字典中。

如果 **keywords 放在 *arguments 之前,当函数调用时,Python 解释器在解析参数时就会遇到问题。因为 **keywords 会优先收集所有的关键字参数,而此时如果还有剩余的位置参数,就无法正确地将它们收集到 *arguments 中,因为 *arguments 是用来收集位置参数的,而在 **keywords 之后,解释器已经无法区分哪些是位置参数了。

注意,关键字参数在输出结果中的顺序与调用函数时的顺序一致。


特殊参数

默认情况下,参数可以按位置或显式关键字传递给 Python 函数。为了让代码易读、高效,最好限制参数的传递方式,这样,开发者只需查看函数定义,即可确定参数项是仅按位置、按位置或关键字,还是仅按关键字传递。

函数定义如下:

python 复制代码
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        位置或关键字   |
        |                                - 仅限关键字
         -- 仅限位置

/ 和 * 是可选的。这些符号表明形参如何把参数值传递给函数:位置、位置或关键字、关键字。关键字形参也叫作命名形参。

位置或关键字参数

函数定义中未使用 /* 时,参数可以按位置或关键字传递给函数。

仅位置参数

此处再介绍一些细节,特定形参可以标记为 仅限位置。仅限位置 时,形参的顺序很重要,且这些形参不能用关键字传递。仅限位置形参应放在 / (正斜杠)前。/ 用于在逻辑上分割仅限位置形参与其它形参。如果函数定义中没有 /,则表示没有仅限位置形参。
/ 后可以是 位置或关键字 或 仅限关键字 形参。

仅限关键字参数

把形参标记为 仅限关键字,表明必须以关键字参数形式传递该形参,应在参数列表中第一个 仅限关键字 形参前添加 *

简单的函数示例

注意 / 和 * 标记:

python 复制代码
def standard_arg(arg):
    print(arg)

def pos_only_arg(arg, /):
    print(arg)

def kwd_only_arg(*, arg):
    print(arg)

def combined_example(pos_only, /, standard, *, kwd_only):
    print(pos_only, standard, kwd_only)

第一个函数定义 standard_arg 是最常见的形式,对调用方式没有任何限制,可以按位置也可以按关键字传递参数:

位置参数:

python 复制代码
standard_arg(2)
#输出
2

关键字参数:

python 复制代码
standard_arg(arg=2)
#输出
2

第二个函数 pos_only_arg 的函数定义中有 /,仅限使用位置形参

位置形参:

python 复制代码
pos_only_arg(1)
#输出
1

关键字参数:报错!

python 复制代码
pos_only_arg(arg=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'

第三个函数 kwd_only_arg 如在函数定义中通过 * 所指明的那样只允许关键字参数。

位置参数:报错

python 复制代码
kwd_only_arg(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given

关键字参数:

python 复制代码
kwd_only_arg(arg=3)
#输出
3

最后一个函数在同一个函数定义中,使用了全部三种调用惯例:

全部位置参数:报错!

python 复制代码
combined_example(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given

位置参数和关键字参数:

python 复制代码
combined_example(1, 2, kwd_only=3)
#输出
1 2 3

combined_example(1, standard=2, kwd_only=3)
#输出
1 2 3

全部关键字参数:报错!

python 复制代码
combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() got some positional-only arguments passed as keyword arguments: 'pos_only'

下面的函数定义中,kwdsname 当作键,因此,可能与位置参数 name 产生潜在冲突:

python 复制代码
def foo(name, **kwds):
    return 'name' in kwds

调用该函数不可能返回 True,因为关键字 'name' 总与第一个形参绑定。例如:

python 复制代码
foo(1, **{'name': 2})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'

代码报错原因:

当执行 foo(1, **{'name': 2}) 时:

首先,Python 会把位置参数 1 赋值给 name,这是按照位置参数的传递规则来的。

接着,要把字典 {'name': 2} 解包传递给函数。但是这时 Python 发现,name 这个参数已经被位置参数 1 赋值过了,现在又有一个关键字参数 name=2 要进来,就会产生冲突。因为在 Python 里,一个参数不能被多次赋值,所以就会抛出 TypeError: foo() got multiple values for argument 'name' 异常。

正确做法:

  • 加上 / (仅限位置参数)后,就可以了。此时,函数定义把 name 当作位置参数,'name' 也可以作为关键字参数的键:
python 复制代码
def foo(name, /, **kwds):
    return 'name' in kwds

foo(1, **{'name': 2})
#输出
True
  • 这里的 / 起到了关键作用,它把 name 变成了仅限位置参数。这就规定了 name
    只能通过位置的方式来传值,不能用关键字参数的方式传值。

调用过程分析

当调用 foo(1, **{'name': 2}) 时:

位置参数 1 会被正常赋值给仅限位置参数 name,这是符合仅限位置参数的传递规则的。

对于字典 {'name': 2},由于 name 是仅限位置参数,字典里的 'name' 键就不会和函数定义里的 name 参数冲突了。**kwds 会把这个 {'name': 2} 字典收集起来,成为一个包含 'name': 2 的字典。

函数内部的 'name' in kwds 语句会去检查 'name' 这个键是否在 kwds 字典里,因为 kwds 字典里确实有 'name' 键,所以函数就会返回 True

可以把这种情况想象成有两个不同的 "区域",一个区域(仅限位置参数 name)只能通过位置传递的方式放东西进去;另一个区域(**kwds 字典)可以接收关键字参数。这样就避免了参数名的冲突。

换句话说,仅限位置形参的名称可以在 **kwds 中使用,而不产生歧义。

小结

以下用例决定哪些形参可以用于函数定义:

python 复制代码
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):

说明:

  • 使用仅限位置形参,可以让用户无法使用形参名。形参名没有实际意义时,强制调用函数的实参顺序时,或同时接收位置形参和关键字时,这种方式很有用。
  • 当形参名有实际意义,且显式名称可以让函数定义更易理解时,阻止用户依赖传递实参的位置时,才使用关键字。
  • 对于 API,使用仅限位置形参,可以防止未来修改形参名时造成破坏性的 API 变动。

函数的变量

变量的作用域

在 Python 中,作用域定义了变量和函数的可见性与生命周期,它决定了在程序的哪些部分可以访问特定的变量或函数。Python 中有四种主要的作用域,按照搜索变量的顺序依次为:局部作用域(Local)闭包作用域(Enclosing)全局作用域(Global)内置作用域(Built-in),通常用 LEGB 规则来描述这个搜索顺序。下面为你详细介绍这四种作用域。

内置作用域(Built-in)

定义:内置作用域是 Python 预先定义好的,包含了所有内置的函数、异常和对象,如 print()len()int 等。这些名称在整个 Python 程序中都是可用的,无需额外导入。

示例:

python 复制代码
# 直接使用内置函数 print 和 len
print(len([1, 2, 3]))

在这个例子中,printlen 都是内置作用域中的函数,可以直接使用。


全局作用域(Global)

定义:全局作用域是在整个模块(即 .py 文件)中定义的作用域。在模块顶层定义的变量和函数都属于全局作用域,它们可以在模块的任何地方被访问,但在函数内部如果要修改全局变量,需要使用 global 关键字。

示例:

python 复制代码
# 定义全局变量
global_variable = 10

def print_global_variable():
    # 可以在函数内部访问全局变量
    print(global_variable)

print_global_variable()  # 输出: 10

def modify_global_variable():
    global global_variable
    global_variable = 20

modify_global_variable()
print(global_variable)  # 输出: 20

在上述代码中,global_variable 是全局变量,可以在函数 print_global_variable 中直接访问。而在 modify_global_variable 函数中,如果要修改全局变量的值,需要使用 global 关键字声明。

闭包作用域(Enclosing)

定义:闭包作用域也称为嵌套作用域,它出现在嵌套函数中。当一个函数内部定义了另一个函数时,内部函数可以访问外部函数的局部变量,这些外部函数的局部变量就处于闭包作用域中。在 Python 中,闭包是一种特殊的对象,它可以捕获并保存外部函数的局部变量状态。

示例:

python 复制代码
def outer_function():
    enclosing_variable = 5
    def inner_function():
        # 内部函数可以访问外部函数的局部变量
        print(enclosing_variable)
    return inner_function

closure = outer_function()
closure()  # 输出: 5

在这个例子中,inner_function 是嵌套在 outer_function 内部的函数,enclosing_variable 是 outer_function 的局部变量,对于 inner_function 来说,它处于闭包作用域中,因此 inner_function 可以访问这个变量。

局部作用域(Local)

定义:局部作用域是在函数内部定义的作用域。函数内部定义的变量和参数都属于局部作用域,它们只能在函数内部被访问,函数执行结束后,局部变量会被销毁。

示例:

python 复制代码
def local_scope_example():
    local_variable = 3
    print(local_variable)

local_scope_example()  # 输出: 3
# print(local_variable)  # 会报错,因为 local_variable 是局部变量,外部无法访问

local_scope_example 函数内部定义的 local_variable 是局部变量,只能在函数内部访问,在函数外部尝试访问会引发 NameError 异常。


函数中变量的查找顺序

在 Python 中,函数内部的变量查找遵循 LEGB 规则,即按照以下顺序逐层查找变量:
LEGB

python 复制代码
Local(局部作用域) → Enclosing(闭包外层) → Global(全局) → Built-in(内置)
  1. Local(局部作用域)

优先级最高:首先在函数内部查找变量

示例:

python 复制代码
def func():
    x = 10        # 局部变量
    print(x)      # 输出 10(优先查找局部)

func()
  1. Enclosing(闭包外层)

适用于嵌套函数:在外层函数的作用域中查找

示例:

python 复制代码
def outer():
    y = 20
    def inner():
        print(y)  # 查找闭包外层的 y → 20
    inner()

outer()
  1. Global(全局作用域)

模块级变量:在函数外部定义的变量

示例:

python 复制代码
z = 30
def func():
    print(z)  # 查找全局的 z → 30

func()
  1. Built-in(内置作用域)

Python 内置函数/变量:如 len, str, True

示例:

python 复制代码
def func():
    print(len)  # 找到内置函数 len → <built-in function len>

func()

关键注意

  • 赋值操作的影响

若在函数内部对变量赋值,Python 会默认将其视为局部变量(即使全局存在同名变量)

python 复制代码
x = 100
def func():
    x = 200    # 创建新的局部变量
    print(x)   # 输出 200

func()
print(x)       # 输出 100(全局变量未变)
  • global 关键字 强制使用全局变量
python 复制代码
x = 100
def func():
    global x
    x = 200    # 修改全局变量
    print(x)   # 输出 200

func()
print(x)       # 输出 200
  • nonlocal 关键字 在嵌套函数中使用外层非全局变量
python 复制代码
def outer():
    x = 10
    def inner():
        nonlocal x
        x = 20  # 修改闭包外层的 x
    inner()
    print(x)    # 输出 20

outer()
  • 变量未定义时的报错,若所有作用域均未找到变量:
python 复制代码
def func():
    print(undefined_var)  # 报错 NameError

func()

是 否 是 否 是 否 是 否 函数内访问变量 是局部变量? 使用局部变量 是闭包外层变量? 使用外层变量 是全局变量? 使用全局变量 是内置变量? 使用内置变量 抛出 NameError


本文小结

本文有关Python中初级函数话题到这里就结束了,后面的文章我们会展开继续 Python函数的更多话题,如内置高级函数,lambda表达式,任意实参列表等等... 感谢您的观看!

如果你觉得这篇文章对你有所帮助,请为我的博客 点赞👍收藏⭐️ 评论💬或 分享🔗 支持一下!你的每一个支持都是我继续创作的动力✨!🙏

如果你有任何问题或想法,也欢迎 留言💬 交流,一起进步📚!❤️ 感谢你的阅读和支持🌟!🎉

祝各位大佬吃得饱🍖,睡得好🛌,日有所得📈,逐梦扬帆⛵!

相关推荐
胡歌11 小时前
final 关键字在不同上下文中的用法及其名称
开发语言·jvm·python
程序员张小厨1 小时前
【0005】Python变量详解
开发语言·python
盖盖衍上2 小时前
Java 泛型(Generics)详解与使用
java·开发语言·windows
Hacker_Oldv2 小时前
Python 爬虫与网络安全有什么关系
爬虫·python·web安全
深蓝海拓3 小时前
PySide(PyQT)重新定义contextMenuEvent()实现鼠标右键弹出菜单
开发语言·python·pyqt
车载诊断技术3 小时前
人工智能AI在汽车设计领域的应用探索
数据库·人工智能·网络协议·架构·汽车·是诊断功能配置的核心
AuGuSt_814 小时前
【深度学习】Hopfield网络:模拟联想记忆
人工智能·深度学习
magic 2454 小时前
深入理解Java网络编程:从基础到高级应用
java·开发语言
cafehaus4 小时前
关于JavaScript性能问题的误解
开发语言·javascript·ecmascript
jndingxin4 小时前
OpenCV计算摄影学(6)高动态范围成像(HDR imaging)
人工智能·opencv·计算机视觉