深入理解python的函数、参数与作用域
- 一、函数、参数与作用域
-
- [1. 定义与调用函数](#1. 定义与调用函数)
-
- [1.1 第一个函数](#1.1 第一个函数)
- [1.2 函数名称](#1.2 函数名称)
- [1.3 函数调用前,定义必须已经执行](#1.3 函数调用前,定义必须已经执行)
- [2. 参数与返回值](#2. 参数与返回值)
-
- [2.1 使用参数传入数据](#2.1 使用参数传入数据)
- [2.2 多个参数](#2.2 多个参数)
- [2.3 使用 return 返回结果](#2.3 使用 return 返回结果)
- [2.4 print 与 return 的区别](#2.4 print 与 return 的区别)
- [2.5 return 会结束当前函数调用](#2.5 return 会结束当前函数调用)
- [2.6 返回多个结果](#2.6 返回多个结果)
- [3. 常用参数形式](#3. 常用参数形式)
-
- [3.1 位置参数](#3.1 位置参数)
- [3.2 关键字参数](#3.2 关键字参数)
- [3.3 默认参数](#3.3 默认参数)
- [3.4 不要使用可变对象作为普通默认值](#3.4 不要使用可变对象作为普通默认值)
- [3.5 仅限关键字参数](#3.5 仅限关键字参数)
- [3.6 仅限位置参数](#3.6 仅限位置参数)
- [4. 可变数量参数与解包](#4. 可变数量参数与解包)
-
- [4.1 *args:接收额外的位置参数](#4.1 *args:接收额外的位置参数)
- [4.2 kwargs:接收额外的关键字参数](#4.2 kwargs:接收额外的关键字参数)
- [4.3 参数组合示例](#4.3 参数组合示例)
- [4.4 调用时使用 * 解包](#4.4 调用时使用 * 解包)
- [4.5 调用时使用 ** 解包](#4.5 调用时使用 ** 解包)
- [5. 参数传递与可变对象](#5. 参数传递与可变对象)
-
- [5.1 形参会绑定到传入的对象](#5.1 形参会绑定到传入的对象)
- [5.2 修改列表会影响调用方](#5.2 修改列表会影响调用方)
- [5.3 重新赋值不会替换调用方的变量](#5.3 重新赋值不会替换调用方的变量)
- [5.4 不可变对象通常通过返回新值更新](#5.4 不可变对象通常通过返回新值更新)
- [5.5 明确函数是否修改输入](#5.5 明确函数是否修改输入)
- [6. 变量作用域](#6. 变量作用域)
-
- [6.1 局部变量与全局变量](#6.1 局部变量与全局变量)
- [6.2 名称查找顺序:LEGB](#6.2 名称查找顺序:LEGB)
- [6.3 函数内赋值可能导致 UnboundLocalError](#6.3 函数内赋值可能导致 UnboundLocalError)
- [6.4 global:重新绑定全局变量](#6.4 global:重新绑定全局变量)
- [6.5 nonlocal:重新绑定外层函数变量](#6.5 nonlocal:重新绑定外层函数变量)
- [7. 函数也是对象](#7. 函数也是对象)
-
- [7.1 函数可以赋给变量](#7.1 函数可以赋给变量)
- [7.2 函数可以作为参数](#7.2 函数可以作为参数)
- [7.3 lambda:简单的匿名函数](#7.3 lambda:简单的匿名函数)
- [7.4 使用 key 指定排序规则](#7.4 使用 key 指定排序规则)
- [7.5 函数可以作为返回值:闭包初步](#7.5 函数可以作为返回值:闭包初步)
- [8. 文档字符串与类型注解](#8. 文档字符串与类型注解)
-
- [8.1 文档字符串](#8.1 文档字符串)
- [8.2 类型注解](#8.2 类型注解)
- [8.3 容器与可选返回值](#8.3 容器与可选返回值)
如需转载,请附上链接:https://zhenghuisheng.blog.csdn.net/article/details/166746555
一、函数、参数与作用域
上一篇,我们学习了列表、元组、字典和集合,并使用"列表 + 字典"完成了商品查询、筛选和统计。随着功能增加,代码也会越来越长。例如,查询商品时要遍历列表,统计库存时又要遍历列表。如果同一种操作需要在多个地方使用,直接复制代码会让修改和维护变得困难。
函数可以把一段具有明确用途的代码组织起来,为它起一个名字,然后在需要时调用。本篇将学习:
- 如何定义和调用函数。
- 如何传入参数、返回结果。
- 位置参数、关键字参数和默认参数。
*args、**kwargs与参数解包。- 变量作用域与可变对象传参。
- 函数对象、
lambda和类型注解。 - 如何把商品管理代码拆分成可复用函数。
1. 定义与调用函数
1.1 第一个函数
使用 def 定义函数:
python
def 函数名(参数列表):
函数体
例如:
python
def say_hello():
print("你好,Python!")
if __name__ == '__main__':
say_hello()
say_hello()
运行结果:
你好,Python!
你好,Python!
其中:
def表示定义函数。say_hello是函数名称。()中可以声明参数,本例没有参数。- 冒号后面缩进的代码属于函数体。
say_hello()表示调用函数。
定义函数不会立即执行函数体,调用时才会执行。
python
def say_hello():
print("你好,Python!")
if __name__ == '__main__':
print("程序开始")
这段代码只输出:
程序开始
1.2 函数名称
函数名遵循标识符规则,通常使用小写字母和下划线:
find_product
calculate_total
get_user_name
函数名应表达它负责的操作。例如:
def calculate_total():
...
比:
def handle():
...
更容易理解。也要避免使用内置函数名称作为自己的函数名或变量名,例如 list、sum、max,否则会遮蔽原来的内置名称。
1.3 函数调用前,定义必须已经执行
下面的顺序可以正常运行:
python
def greet():
print("你好")
greet()
反过来则会出现 NameError:
python
greet()
def greet():
print("你好")
这里的关键是运行顺序:执行 greet() 时,名称 greet 还没有绑定到函数。因此,常见的程序组织方式是:
python
def function_a():
...
def function_b():
if __name__ == '__main__':
function_a()
function_b()
2. 参数与返回值
2.1 使用参数传入数据
如果每次调用都要处理不同的数据,可以给函数定义参数:
python
def greet(name):
print(f"你好,{name}!")
if __name__ == '__main__':
greet("小林")
greet("小陈")
运行结果:
你好,小林!
你好,小陈!
定义中的 name 称为形参 ,调用时传入的 "小林"、"小陈" 称为实参。可以把形参理解为函数内部用于接收输入数据的名字。
2.2 多个参数
python
def show_product(name, price, stock):
print(f"{name}:¥{price:.2f},库存 {stock}")
if __name__ == '__main__':
show_product("矿泉水", 2, 100)
运行结果:
矿泉水:¥2.00,库存 100
按位置传参时,对应关系是:
| 实参 | 形参 |
|---|---|
"矿泉水" |
name |
2 |
price |
100 |
stock |
2.3 使用 return 返回结果
如果函数计算的结果需要交给其他代码继续使用,应当使用 return:
python
def calculate_total(price, quantity):
return price * quantity
if __name__ == '__main__':
total = calculate_total(8, 3)
print(f"商品金额:{total}")
print(f"加上 5 后:{total + 5}")
运行结果:
商品金额:24
加上 5 后:29
return 将结果交给调用方,调用方可以保存、显示或继续计算。
2.4 print 与 return 的区别
print() 用于输出信息,return 用于返回结果。
python
def add_and_print(a, b):
print(a + b)
def add_and_return(a, b):
return a + b
if __name__ == '__main__':
first = add_and_print(2, 3)
print(f"first = {first}")
second = add_and_return(2, 3)
print(f"second = {second}")
运行结果:
5
first = None
second = 5
第一个函数虽然输出了 5,但没有通过 return 返回这个值,因此调用结果是 None。函数正常执行到末尾而没有显式返回值时,会返回 **None****。**下面两种情况也都会返回 None:
python
def do_something():
pass
def do_something():
return
2.5 return 会结束当前函数调用
执行 return 后,当前函数调用就结束了,后面的普通语句不再执行。
python
def get_level(score):
if not 0 <= score <= 100:
return "成绩不合法"
if score >= 90:
return "优秀"
if score >= 80:
return "良好"
if score >= 60:
return "及格"
return "不及格"
if __name__ == '__main__':
print(get_level(85))
print(get_level(101))
运行结果:
良好
成绩不合法
这种写法先处理特殊情况,再处理正常逻辑,可以减少不必要的嵌套。注意:如果函数中包含循环,return 会直接结束整个函数调用,而不仅仅是退出循环。只想结束循环时,应使用 break。
2.6 返回多个结果
Python 可以使用逗号返回多个值:
python
def get_bounds(numbers):
return min(numbers), max(numbers)
if __name__ == '__main__':
result = get_bounds([8, 3, 12, 5])
print(result)
minimum, maximum = result
print(f"最小值:{minimum}")
print(f"最大值:{maximum}")
运行结果:
(3, 12)
最小值:3
最大值:12
实际上:
return min(numbers), max(numbers)
返回的是一个元组。调用方可以通过解包取得各个结果
3. 常用参数形式
3.1 位置参数
按照参数声明的顺序传入:
python
def introduce(name, age):
print(f"{name},{age} 岁")
if __name__ == '__main__':
introduce("小林", 25)
运行结果:
小林,25 岁
如果顺序写反,函数通常无法替你判断业务含义:
introduce(25, "小林")
结果会变成:
25,小林 岁
所以位置参数较多时,需要特别注意顺序。
3.2 关键字参数
调用时写出参数名称,可以让含义更明确:
python
def introduce(name, age):
print(f"{name},{age} 岁")
if __name__ == '__main__':
introduce(name="小林", age=25)
introduce(age=25, name="小林")
运行结果:
小林,25 岁
小林,25 岁
关键字参数按名称匹配,因此可以调整书写顺序。普通的位置参数和关键字参数可以混用,但直接书写时,应把位置参数放在关键字参数之前:
introduce("小林", age=25)
不要给同一个参数重复赋值:
introduce("小林", name="小陈", age=25)
这里 name 同时收到了位置实参和关键字实参,会抛出 TypeError。
3.3 默认参数
定义函数时,可以为参数设置默认值:
python
def greet(name, greeting="你好"):
return f"{greeting},{name}!"
if __name__ == '__main__':
print(greet("小林"))
print(greet("小陈", "早上好"))
print(greet("小周", greeting="欢迎"))
运行结果:
你好,小林!
早上好,小陈!
欢迎,小周!
没有传入 greeting 时,使用默认值 "你好";显式传入时,使用传入值。对于普通的位置或关键字参数,必填参数需要放在有默认值的参数前面:
def greet(name, greeting="你好"):
...
下面这种定义不合法:
def greet(greeting="你好", name):
...
3.4 不要使用可变对象作为普通默认值
下面的函数存在隐患:
python
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
if __name__ == '__main__':
print(add_tag("Python"))
print(add_tag("SQL"))
运行结果:
['Python']
['Python', 'SQL']
第二次调用没有得到新的空列表,而是继续使用上一次的列表。原因是:
默认参数表达式在执行函数定义时求值,并不是每次调用时重新求值。
因此,这个默认列表会被多次调用共享。通常应使用 None,再在函数内部创建列表:
python
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
if __name__ == '__main__':
print(add_tag("Python"))
print(add_tag("SQL"))
运行结果:
['Python']
['SQL']
这里应使用:
if tags is None:
而不是:
if not tags:
后者还会把调用方主动传入的空列表当成需要替换的情况。
3.5 仅限关键字参数
在参数列表中使用单独的 *,可以要求后面的参数只能按名称传入:
python
def create_user(name, *, active=True):
return {
"name": name,
"active": active
}
if __name__ == '__main__':
print(create_user("小林", active=False))
运行结果:
{'name': '小林', 'active': False}
下面的调用不允许:
create_user("小林", False)
因为 active 被声明为仅限关键字参数。
这种方式适合含义不够明显的开关:
create_user("小林", active=False)
比单独传入一个 False 更容易理解。
仅限关键字参数也可以是必填项:
def send_message(content, *, recipient):
print(f"发送给 {recipient}:{content}")
调用时必须显式提供 recipient。
3.6 仅限位置参数
Python 3.8 及以上支持使用 /,将它前面的参数设为仅限位置参数:
python
def divide(a, b, /):
return a / b
if __name__ == '__main__':
print(divide(10, 2))
运行结果:
5.0
不能这样调用:
divide(a=10, b=2)
将两种限制放在一起:
python
def example(a, /, b, *, c):
return a + b + c
对应关系:
| 参数 | 允许的传参方式 |
|---|---|
a |
只能按位置 |
b |
按位置或按关键字 |
c |
只能按关键字 |
合法调用:
example(1, 2, c=3)
example(1, b=2, c=3)
入门阶段不必在所有函数中使用这些限制,但阅读第三方函数签名时需要认识它们。
4. 可变数量参数与解包
4.1 *args:接收额外的位置参数
如果参数数量不固定,可以使用 *args:
python
def add_all(*numbers):
print(numbers)
return sum(numbers)
if __name__ == '__main__':
print(add_all(10, 20, 30))
print(add_all())
运行结果:
(10, 20, 30)
60
()
0
numbers 在函数内部是元组。
args 不是关键字,只是常见名称。真正起作用的是前面的 *,因此也可以写成:
def add_all(*values):
...
它可以与普通参数组合:
python
def show_scores(name, *scores):
print(f"{name}:{scores}")
if __name__ == '__main__':
show_scores("小林", 85, 92, 78)
运行结果:
小林:(85, 92, 78)
第一个实参交给 name,剩余的位置实参收集到 scores。
4.2 kwargs:接收额外的关键字参数
python
def show_options(**options):
print(options)
if __name__ == '__main__':
show_options(theme="dark", page_size=20)
运行结果:
{'theme': 'dark', 'page_size': 20}
options 在函数内部是字典。
与 args 一样,kwargs 也是命名惯例,不是固定关键字。** 才表示收集额外的关键字参数。
4.3 参数组合示例
python
def describe(name, *tags, active=True, **extra):
print(f"名称:{name}")
print(f"标签:{tags}")
print(f"启用:{active}")
print(f"其他:{extra}")
if __name__ == '__main__':
describe(
"小林",
"Python",
"SQL",
active=False,
city="深圳"
)
运行结果:
名称:小林
标签:('Python', 'SQL')
启用:False
其他:{'city': '深圳'}
参数对应关系:
name接收第一个位置实参。tags收集剩余位置实参。active是*tags后面的仅限关键字参数。extra收集尚未匹配的关键字实参。
不需要为了"灵活"而让所有函数都接收 *args 和 **kwargs。参数明确时,直接写出名称更容易理解,也更容易发现调用错误。
4.4 调用时使用 * 解包
定义函数时,* 可以收集参数;调用函数时,* 可以展开参数。
python
def add(a, b, c):
return a + b + c
if __name__ == '__main__':
numbers = [10, 20, 30]
print(add(*numbers))
运行结果:
60
这里:
add(*numbers)
相当于:
add(10, 20, 30)
如果直接写:
add(numbers)
则只传入了一个列表,并不会自动分配给三个参数。
4.5 调用时使用 ** 解包
python
def introduce(name, age):
return f"{name},{age} 岁"
if __name__ == '__main__':
user = {
"name": "小林",
"age": 25
}
print(introduce(**user))
运行结果:
小林,25 岁
相当于:
introduce(name="小林", age=25)
通过 ** 展开为关键字参数时,键必须是字符串,参数名称也需要符合函数定义;多余参数需要有相应的 **kwargs 接收。同一个参数不能通过不同方式重复传入,否则会抛出 TypeError。
5. 参数传递与可变对象
5.1 形参会绑定到传入的对象
Python 调用函数时,会让形参名称绑定到实参所引用的对象。可以先记住两条规则:
- 修改共享的可变对象,调用方能够看到变化。
- 给形参重新赋值,不会改变调用方变量的绑定。
5.2 修改列表会影响调用方
python
def add_product(products, name):
products.append(name)
if __name__ == '__main__':
items = ["矿泉水"]
add_product(items, "可乐")
print(items)
运行结果:
['矿泉水', '可乐']
函数中的 products 与外部的 items 引用同一个列表,append() 修改了这个列表。
5.3 重新赋值不会替换调用方的变量
python
def replace_products(products):
products = ["饼干"]
print(f"函数内部:{products}")
if __name__ == '__main__':
items = ["矿泉水"]
replace_products(items)
print(f"函数外部:{items}")
运行结果:
函数内部:['饼干']
函数外部:['矿泉水']
products = ["饼干"] 只是让局部名称 products 引用一个新列表,不会让外部的 items 跟着改变。
5.4 不可变对象通常通过返回新值更新
python
def increase(number):
number += 1
return number
if __name__ == '__main__':
count = 10
result = increase(count)
print(count)
print(result)
运行结果:
10
11
整数不可变,number += 1 使局部名称绑定到计算后的整数,不会修改原来的整数对象。如果希望更新外部变量,需要接收返回值:
count = increase(count)
5.5 明确函数是否修改输入
函数设计时,应尽量让调用者容易判断它是否会修改传入数据。
例如,返回一个新增标签后的列表,同时保留原列表:
python
def with_tag(tags, tag):
result = tags.copy()
result.append(tag)
return result
if __name__ == '__main__':
original = ["Python"]
updated = with_tag(original, "SQL")
print(original)
print(updated)
运行结果:
['Python']
['Python', 'SQL']
这里使用浅拷贝已经足够,因为只是向外层列表增加字符串。
如果还要修改内部嵌套的可变对象,就需要进一步考虑是否深拷贝。
6. 变量作用域
6.1 局部变量与全局变量
函数内部创建的变量通常是局部变量,只能在对应函数调用中使用。
python
message = "全局消息"
def show_message():
message = "局部消息"
print(message)
if __name__ == '__main__':
show_message()
print(message)
运行结果:
局部消息
全局消息
函数内部的 message 与外部的 message 是不同作用域中的名称。
6.2 名称查找顺序:LEGB
对于普通函数中的名称查找,可以按 LEGB 理解:
| 层级 | 含义 |
|---|---|
| Local | 当前函数的局部作用域 |
| Enclosing | 外层函数的作用域 |
| Global | 当前模块的全局作用域 |
| Built-in | 内置名称,例如 len、sum |
从近到远查找,找到后就使用对应的绑定。
python
label = "全局"
def outer():
label = "外层"
def inner():
print(label)
inner()
if __name__ == '__main__':
outer()
运行结果:
外层
inner() 内部没有定义 label,于是找到外层函数中的 label。
6.3 函数内赋值可能导致 UnboundLocalError
下面的代码会报错:
python
count = 10
def increase():
count += 1
if __name__ == '__main__':
increase()
原因是函数内存在对 count 的赋值,Python 因此将它视为局部名称。
count += 1
需要先读取局部 count,而它还没有被赋值,所以抛出 UnboundLocalError。
通常可以通过参数和返回值解决:
python
def increase(count):
return count + 1
if __name__ == '__main__':
count = 10
count = increase(count)
print(count)
运行结果:
11
6.4 global:重新绑定全局变量
如果确实需要在函数中给模块级变量赋值,可以声明 global:
python
count = 0
def increase():
global count
count += 1
if __name__ == '__main__':
increase()
increase()
print(count)
运行结果:
2
global 表示这里的名称指向当前模块的全局绑定。不过,大量函数共同修改全局变量,会让数据变化难以追踪。普通计算逻辑通常更适合使用参数和返回值。
另外,读取全局变量不需要 **global**。仅修改全局变量引用的可变对象,也不需要它:
python
logs = []
def add_log(message):
logs.append(message)
这里没有重新绑定名称 logs,而是在修改它引用的列表。
6.5 nonlocal:重新绑定外层函数变量
嵌套函数中,如果需要修改外层函数的变量,可以使用 nonlocal:
python
def outer():
count = 0
def increase():
nonlocal count
count += 1
return count
print(increase())
print(increase())
if __name__ == '__main__':
outer()
运行结果:
1
2
区别是:
global指向模块级作用域。nonlocal指向某个外层函数中已经存在的绑定。
nonlocal 不用于声明一个新的全局变量。
7. 函数也是对象
7.1 函数可以赋给变量
python
def square(number):
return number ** 2
if __name__ == '__main__':
operation = square
print(operation(5))
运行结果:
25
这里要区分:
operation = square
表示把函数对象交给变量。
result = square(5)
表示调用函数,把返回结果交给变量。有没有圆括号,含义不同。
7.2 函数可以作为参数
python
def square(number):
return number ** 2
def apply_to_all(numbers, operation):
return [operation(number) for number in numbers]
if __name__ == '__main__':
result = apply_to_all([1, 2, 3], square)
print(result)
运行结果:
[1, 4, 9]
apply_to_all() 不需要知道具体计算规则,只负责遍历数据,再调用传入的函数。这种写法让"遍历过程"和"处理规则"可以分别复用。
7.3 lambda:简单的匿名函数
lambda 可以定义一个只包含单个表达式的函数。
基本语法:
lambda 参数: 表达式
lambda number: number ** 2
相当于一个接收数字并返回平方的函数。它常用于临时传入简单规则:
python
def apply_to_all(numbers, operation):
return [operation(number) for number in numbers]
if __name__ == '__main__':
result = apply_to_all([1, 2, 3],lambda number: number ** 2)
print(result)
运行结果:
[1, 4, 9]
lambda 不写 return,表达式的结果就是返回值。需要多条语句、复杂分支或清晰的文档说明时,应使用 def。
7.4 使用 key 指定排序规则
上一篇出现过:
sorted(products, key=lambda product: product["price"])
现在可以把它完整理解为:将一个函数交给 sorted(),让它为每个商品提取价格作为排序依据。
python
products = [
{"name": "饼干", "price": 8},
{"name": "矿泉水", "price": 2},
{"name": "果汁", "price": 5}
]
def product_price(product):
return product["price"]
if __name__ == '__main__':
a = sorted(products, key=lambda product: product["price"])
# a = sorted(products, key=product_price)
for product in a:
print(f"{product['name']}:{product['price']}")
运行结果:
矿泉水:2
果汁:5
饼干:8
也可以使用普通函数:
def get_price(product):
return product["price"]
然后:
ordered = sorted(products, key=get_price)
这里传入的是 get_price,不是 get_price()。真正的调用由 sorted() 完成。
7.5 函数可以作为返回值:闭包初步
函数内部还可以定义并返回另一个函数:
python
def make_multiplier(factor):
def multiply(number):
return number * factor
return multiply
if __name__ == '__main__':
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))
print(triple(5))
运行结果:
10
15
make_multiplier(2) 返回的函数,仍然能够访问创建它时外层作用域中的 factor。这种内部函数携带外层变量绑定的现象称为闭包。
8. 文档字符串与类型注解
8.1 文档字符串
函数体的第一条语句可以是一个字符串,用来描述函数用途:
python
def calculate_total(price, quantity):
"""根据单价和数量计算总金额。"""
return price * quantity
这称为文档字符串,也就是 docstring。可以通过下面的方式查看:
print(calculate_total.__doc__)
运行结果:
根据单价和数量计算总金额。
也可以使用:
help(calculate_total)
查看函数帮助。
文档应优先说明调用者需要知道的内容:
- 函数做什么。
- 参数有什么要求。
- 返回什么结果。
- 是否修改输入数据。
- 什么情况下可能失败。
8.2 类型注解
类型注解可以说明预期的参数类型和返回类型:
python
def add(a: int, b: int) -> int:
return a + b
if __name__ == '__main__':
print(add(2, 3))
运行结果:
5
其中:
a: int表示预期a为整数。b: int表示预期b为整数。-> int表示预期返回整数。
类型注解能够帮助编辑器和类型检查工具理解代码,但Python 本身不会仅因为这些注解就自动执行类型校验或转换。
python
def add(a: int, b: int) -> int:
return a + b
if __name__ == '__main__':
print(add("A", "B"))
这段代码仍然会输出:
AB
虽然调用违背了注解表达的约定,但字符串支持 +,因此运行时完成了拼接。
8.3 容器与可选返回值
Python 3.9 及以上可以直接写:
list[int]
dict[str, int]
例如:
def total_scores(scores: list[int]) -> int:
return sum(scores)
Python 3.10 及以上可以使用 | 表示多种可能类型:
python
def find_index(items: list[str], target: str) -> int | None:
for index, item in enumerate(items):
if item == target:
return index
return None
if __name__ == '__main__':
print(find_index(["苹果", "香蕉"], "香蕉"))
print(find_index(["苹果", "香蕉"], "橙子"))
运行结果:
1
None
int | None 表示返回整数或 None。接收这种结果时,应明确判断:
python
index = find_index(["苹果", "香蕉"], "苹果")
if index is not None:
print(f"找到位置:{index}")
不要写成:
if index:
因为合法索引 0 的真假值也是假。