10 - 函数
函数是编程里的"积木"------把一段有特定功能的代码打包起来,起个名字,以后需要的时候直接用名字调用。
为什么要用函数
假设你要在程序里算三次圆的面积。不用函数的话:
python
r1 = 5
area1 = 3.14159 * r1 ** 2
r2 = 10
area2 = 3.14159 * r2 ** 2
r3 = 7.5
area3 = 3.14159 * r3 ** 2
每次都要写一遍公式,而且如果哪天要把精度改成 3.1415926,得改三处。
用函数:
python
def circle_area(radius):
return 3.14159 * radius ** 2
area1 = circle_area(5)
area2 = circle_area(10)
area3 = circle_area(7.5)
干净多了。改精度只需要改函数里一处。
定义和调用
python
def 函数名(参数):
"""文档字符串(可选)"""
# 函数体
return 返回值
一个简单的例子:
python
def greet(name):
"""打招呼"""
return f"你好,{name}!"
# 调用
message = greet("小明")
print(message) # 你好,小明!
几个要点:
def是定义函数的关键字- 参数放在括号里,可以有多个
return返回结果。没有return的话,函数返回None- 三引号的字符串叫文档字符串(docstring),用来描述函数干嘛的
参数
Python 函数的参数花样挺多的,这也是很多人学不明白的地方。别急,一个个来。
位置参数
最普通的参数,按顺序传:
python
def add(a, b):
return a + b
print(add(3, 5)) # 8
关键字参数
调用的时候指定参数名,顺序就无所谓了:
python
def describe(name, age, city):
print(f"{name},{age}岁,来自{city}")
describe(age=25, name="小明", city="北京") # 顺序随便写
位置参数和关键字参数可以混用,但位置参数必须在前面:
python
describe("小明", age=25, city="北京") # 对的
# describe(name="小明", 25, "北京") # 报错!位置参数不能在关键字参数后面
默认参数
给参数设个默认值,调用的时候可以不传:
python
def greet(name, greeting="你好"):
return f"{greeting},{name}!"
print(greet("小明")) # 你好,小明!
print(greet("John", "Hello")) # Hello,John!
注意一个经典坑:默认参数不要用可变对象!
python
# 错误示范
def add_item(item, my_list=[]):
my_list.append(item)
return my_list
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2]?!不是 [2]?
print(add_item(3)) # [1, 2, 3]?!
因为默认参数 [] 在函数定义时只创建了一次,后续调用都用的同一个列表。正确写法:
python
def add_item(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list
print(add_item(1)) # [1]
print(add_item(2)) # [2],正常了
可变参数(*args)
想传多少个参数都行:
python
def total(*numbers):
result = 0
for n in numbers:
result += n
return result
print(total(1, 2, 3)) # 6
print(total(1, 2, 3, 4, 5)) # 15
print(total()) # 0
*numbers 会把所有传入的参数打包成一个元组。
* 是个约定,numbers 这个名字可以随便起,但大家习惯用 args(arguments 的缩写):
python
def total(*args):
return sum(args)
可变关键字参数(**kwargs)
跟 *args 类似,但接收的是关键字参数,打包成字典:
python
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="小明", age=25, city="北京")
# name: 小明
# age: 25
# city: 北京
参数全组合
如果一个函数同时用了这几种参数,顺序必须是:
python
def func(pos_only, /, normal, *args, keyword_only, **kwargs):
pass
- 仅限位置参数(
/前面的,不常用) - 普通参数
*args- 仅限关键字参数(
*args后面的) **kwargs
平时写不会这么复杂,了解顺序就行。最常见的组合:
python
def func(a, b, *args, **kwargs):
print(f"a={a}, b={b}")
print(f"args={args}")
print(f"kwargs={kwargs}")
func(1, 2, 3, 4, name="小明")
# a=1, b=2
# args=(3, 4)
# kwargs={'name': '小明'}
返回值
返回多个值
Python 函数可以"返回多个值"(其实是返回一个元组):
python
def divide(a, b):
quotient = a // b
remainder = a % b
return quotient, remainder
q, r = divide(17, 5)
print(f"商:{q},余数:{r}") # 商:3,余数:2
没有返回值
没有 return 或 return 后面没东西,函数返回 None:
python
def say_hello(name):
print(f"Hello, {name}")
result = say_hello("小明") # Hello, 小明
print(result) # None
提前返回
return 可以在函数的任何位置,遇到就立即退出:
python
def check_age(age):
if age < 0:
return "年龄不能为负数"
if age < 18:
return "未成年"
return "成年人"
变量的作用域
函数里面定义的变量,外面看不到:
python
def my_func():
x = 10 # 局部变量
print(x)
my_func()
# print(x) # NameError! x 在外面不存在
如果函数里的变量名跟外面一样呢?
python
x = 10 # 全局变量
def my_func():
x = 20 # 这是一个新的局部变量,跟外面的 x 没关系
print(f"函数里:{x}")
my_func() # 函数里:20
print(f"函数外:{x}") # 函数外:10
如果要在函数里修改全局变量,需要用 global:
python
count = 0
def increment():
global count
count += 1
increment()
increment()
print(count) # 2
不过说实话,global 能不用就不用。全局变量用多了代码会变得很混乱,很难追踪哪个函数在什么时候改了什么变量。更好的做法是用参数和返回值:
python
def increment(count):
return count + 1
count = 0
count = increment(count)
count = increment(count)
print(count) # 2
lambda 表达式
lambda 是一种简写函数的方式,只能写一行:
python
# 普通函数
def square(x):
return x ** 2
# 等价的 lambda
square = lambda x: x ** 2
一般不用 lambda 定义变量(那不如直接写 def),它主要用在需要传函数做参数的地方:
python
# 排序时自定义规则
students = [("小明", 85), ("小红", 92), ("小刚", 78)]
students.sort(key=lambda s: s[1]) # 按分数排序
print(students) # [('小刚', 78), ('小明', 85), ('小红', 92)]
# 配合 map 和 filter
numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x**2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))
不过对于 map 和 filter,用列表推导式更 Pythonic:
python
squares = [x**2 for x in numbers]
evens = [x for x in numbers if x % 2 == 0]
闭包(Closure)
闭包是理解装饰器的前提,挺重要的一个概念。
先看个例子:
python
def outer():
x = 10
def inner():
print(x) # inner 引用了 outer 的变量 x
return inner
f = outer()
f() # 10
这里 inner 函数"记住了"外层函数 outer 的变量 x,即使 outer 已经执行完了,x 还是可以被 inner 访问。这就是闭包。
闭包 = 函数 + 它捕获的外部变量。
闭包的实际用途
工厂函数------生成定制化的函数:
python
def make_multiplier(factor):
def multiplier(x):
return x * factor # factor 被捕获了
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
double 和 triple 都是闭包,它们各自"记住"了不同的 factor 值。
保持状态(不用全局变量):
python
def make_counter():
count = 0
def counter():
nonlocal count # 声明要修改外层变量
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
nonlocal 关键字告诉 Python "这个变量不是局部的,去外层找"。跟 global 类似,但 nonlocal 是找外层函数(不是全局)。
闭包和装饰器的关系
回头看看装饰器的结构:
python
def log_call(func):
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
这不就是一个闭包吗?wrapper 捕获了外层的 func。所以装饰器本质上就是闭包的一个应用。
查看闭包捕获了什么
python
def make_adder(n):
def adder(x):
return x + n
return adder
f = make_adder(10)
print(f.__closure__) # 闭包对象
print(f.__closure__[0].cell_contents) # 10(捕获的 n 的值)
递归(Recursion)
递归就是函数调用自己。有些问题用递归描述特别自然。
经典例子:阶乘
python
def factorial(n):
if n <= 1: # 基线条件(什么时候停)
return 1
return n * factorial(n - 1) # 递归调用
print(factorial(5)) # 120(5 × 4 × 3 × 2 × 1)
递归函数必须有两部分:
- 基线条件:什么时候停下来(不然就无限递归了)
- 递归步骤:把问题缩小,调用自己
经典例子:斐波那契数列
python
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
这个写法很直观,但效率很差(大量重复计算)。后面学了 @lru_cache 就能一行解决性能问题。
实际场景:遍历目录
python
import os
def list_all_files(path):
"""递归列出所有文件"""
for item in os.listdir(path):
full_path = os.path.join(path, item)
if os.path.isdir(full_path):
list_all_files(full_path) # 目录就递归进去
else:
print(full_path)
递归的限制
Python 默认最大递归深度是 1000:
python
import sys
print(sys.getrecursionlimit()) # 1000
# 可以修改,但一般不建议
# sys.setrecursionlimit(5000)
如果递归太深会报 RecursionError。大多数情况下,如果你发现需要很深的递归,考虑改成用循环(迭代)来实现。
globals() 和 locals()
这两个函数让你查看当前作用域里的所有变量:
python
x = 10
y = "hello"
print(globals())
# {..., 'x': 10, 'y': 'hello', ...}
# 返回一个字典,包含所有全局变量
python
def my_func(a, b):
c = a + b
print(locals())
# {'a': 3, 'b': 5, 'c': 8}
# 返回一个字典,包含所有局部变量
my_func(3, 5)
有什么用?
说实话,日常开发中很少直接用它们。主要用途:
- 调试------快速查看当前有哪些变量
- 动态操作------根据字符串名字获取变量
python
# 根据字符串名字调用函数
def hello():
print("Hello!")
def goodbye():
print("Goodbye!")
name = "hello"
globals()[name]() # Hello!
不过这种动态操作一般有更好的方式(比如用字典做映射),不推荐滥用 globals()。
eval() 和 exec()
这两个函数可以动态执行 Python 代码。很强大,也很危险。
eval()------执行表达式,返回结果
python
result = eval("2 + 3 * 4")
print(result) # 14
# 可以访问当前作用域的变量
x = 10
print(eval("x * 2")) # 20
exec()------执行语句,不返回结果
python
exec("y = 100")
print(y) # 100
exec("""
def hello():
print("动态定义的函数")
""")
hello() # 动态定义的函数
安全风险
永远不要用 eval/exec 执行用户输入的内容!
python
# 危险示范!
user_input = input("输入表达式:")
# 如果用户输入 __import__('os').system('rm -rf /')
# 你的文件就没了...
result = eval(user_input)
这不是危言耸听,eval() 可以执行任何 Python 代码,包括删除文件、访问网络等。
那什么时候能用?只在你完全控制输入内容的时候。比如从配置文件里读取一个数学表达式,或者做一些 DSL(领域特定语言)的解析。
类型提示(Type Hints)
Python 3.5 开始支持类型提示,不影响运行,但能帮助你和编辑器理解代码:
python
def greet(name: str, age: int) -> str:
return f"你好 {name},你今年 {age} 岁"
name: str表示 name 应该是字符串-> str表示返回值是字符串
Python 本身不会检查这些类型(写错了也不会报错),但 VS Code、mypy 这类工具会帮你检查:
python
# mypy 或编辑器会提示这里类型不对
greet(123, "二十五") # 参数类型反了
在大型项目中类型提示非常有用。初学阶段不强制写,但建议从一开始就有这个意识。
typing 模块详解
基本的类型提示直接用 int、str 这些就够了,但复杂场景需要 typing 模块:
python
from typing import List, Dict, Tuple, Set, Optional, Union
# 列表里装什么类型
def process(items: List[str]) -> List[int]:
return [len(s) for s in items]
# 字典的键值类型
def get_config() -> Dict[str, int]:
return {"timeout": 30, "retries": 3}
# 元组每个位置的类型
def get_point() -> Tuple[float, float]:
return (3.0, 4.0)
# 可能是 None
def find_user(name: str) -> Optional[dict]:
# Optional[dict] 等价于 dict | None(Python 3.10+)
...
# 多种类型之一
def parse_value(s: str) -> Union[int, float, str]:
# Union[int, float, str] 等价于 int | float | str(Python 3.10+)
...
Python 3.10+ 可以用更简洁的语法:
python
# 不用 import,直接用 | 语法
def find_user(name: str) -> dict | None:
...
def parse_value(s: str) -> int | float | str:
...
# 列表等也直接用内置类型
def process(items: list[str]) -> list[int]:
...
def get_config() -> dict[str, int]:
...
类型别名
类型太复杂的时候,可以起个别名:
python
from typing import TypeAlias # Python 3.10+
# 或者直接用等号(3.10 之前也行)
UserId = int
UserDict = dict[str, str]
Matrix = list[list[float]]
def get_user(user_id: UserId) -> UserDict:
...
Callable------函数类型
python
from typing import Callable
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
# Callable[[int, int], int] 表示:接收两个 int 参数,返回 int 的函数
apply(lambda x, y: x + y, 3, 5) # 8
docstring 规范
前面简单提过 docstring(文档字符串),这里讲讲规范写法。好的 docstring 能让别人(和未来的你自己)快速理解函数的用途。
Google 风格(推荐)
最流行的 docstring 风格,可读性好:
python
def calculate_bmi(weight: float, height: float) -> float:
"""计算 BMI(身体质量指数)。
Args:
weight: 体重,单位千克,必须大于 0。
height: 身高,单位米,必须大于 0。
Returns:
BMI 值,保留一位小数。
Raises:
ValueError: 当 weight 或 height 小于等于 0 时抛出。
Examples:
>>> calculate_bmi(70, 1.75)
22.9
"""
if weight <= 0 or height <= 0:
raise ValueError("体重和身高必须大于 0")
return round(weight / height ** 2, 1)
关键部分
| 部分 | 说明 |
|---|---|
| 第一行 | 简要描述函数做什么(一句话) |
| Args | 每个参数的含义 |
| Returns | 返回值的含义 |
| Raises | 可能抛出的异常 |
| Examples | 使用示例 |
不是每个部分都要写,简单函数写一行就够了:
python
def is_even(n: int) -> bool:
"""判断一个数是否为偶数。"""
return n % 2 == 0
用 help() 查看文档
写了 docstring 之后,别人可以用 help() 查看:
python
help(calculate_bmi)
# 会显示完整的 docstring
VS Code 里鼠标悬停在函数名上也能看到。这也是写好 docstring 的动力之一------方便别人(和自己)使用。
一个综合例子
写一个密码强度检测器:
python
def check_password(password: str) -> dict:
"""检查密码强度,返回评估结果"""
score = 0
issues = []
# 长度检查
if len(password) < 8:
issues.append("密码太短,至少 8 位")
else:
score += 1
# 大写字母检查
if not any(c.isupper() for c in password):
issues.append("建议包含大写字母")
else:
score += 1
# 小写字母检查
if not any(c.islower() for c in password):
issues.append("建议包含小写字母")
else:
score += 1
# 数字检查
if not any(c.isdigit() for c in password):
issues.append("建议包含数字")
else:
score += 1
# 特殊字符检查
special = "!@#$%^&*"
if not any(c in special for c in password):
issues.append("建议包含特殊字符")
else:
score += 1
# 评级
levels = {5: "很强", 4: "较强", 3: "一般", 2: "较弱", 1: "很弱", 0: "极弱"}
return {
"score": score,
"level": levels.get(score, "极弱"),
"issues": issues
}
# 测试
result = check_password("Hello123!")
print(f"得分:{result['score']}/5")
print(f"强度:{result['level']}")
if result["issues"]:
print("建议:")
for issue in result["issues"]:
print(f" - {issue}")
这个例子用到了函数的参数、返回值(字典)、类型提示,还有 any() 这个内置函数(后面会讲)。
本章小结
def定义函数,return返回值,没有 return 返回 None- 参数类型:位置参数、关键字参数、默认参数、
*args、**kwargs - 默认参数不要用可变对象(用
None代替) - 函数内的变量是局部的,改全局变量需要
global(但尽量避免) - lambda 是一行函数的简写,主要用在传参数的场景
- 闭包 = 函数 + 捕获的外部变量,装饰器的底层原理
- 递归 = 函数调用自己,需要基线条件防止无限递归
globals()和locals()查看当前作用域的变量eval()/exec()动态执行代码,千万不要对用户输入使用- 类型提示用
typing模块处理复杂类型(List、Optional、Union等) - docstring 用 Google 风格写,
help()可以查看
面试题
Q1:*args 和 **kwargs 分别是什么?
点击查看答案
*args:收集所有多余的位置参数,打包成元组**kwargs:收集所有多余的关键字参数,打包成字典
python
def func(*args, **kwargs):
print(args) # 元组
print(kwargs) # 字典
func(1, 2, 3, name="小明")
# (1, 2, 3)
# {'name': '小明'}
* 和 ** 也可以用于解包:
python
args = [1, 2, 3]
func(*args) # 把列表解包成位置参数
Q2:为什么默认参数不能用可变对象?
点击查看答案
因为默认参数在函数定义时只创建一次,不是每次调用都创建。如果默认参数是可变对象(列表、字典等),所有调用共享同一个对象,上一次调用的修改会影响下一次。
python
def f(lst=[]):
lst.append(1)
return lst
f() # [1]
f() # [1, 1],不是 [1]!
正确做法:默认值设为 None,函数内创建新对象。
python
def f(lst=None):
if lst is None:
lst = []
lst.append(1)
return lst
Q3:Python 中函数是一等公民(first-class citizen),这是什么意思?
点击查看答案
一等公民意味着函数可以像普通变量一样被操作:
- 赋值给变量 :
f = print - 作为参数传递 :
sorted(items, key=len) - 作为返回值:函数可以返回另一个函数
- 存储在数据结构中 :
[func1, func2, func3]
这是函数式编程的基础,也是装饰器、回调函数等高级特性的前提。
Q4:lambda 和普通函数有什么区别?什么时候用 lambda?
点击查看答案
| lambda | def | |
|---|---|---|
| 表达式数量 | 只能有一个表达式 | 可以有多条语句 |
| 返回值 | 自动返回表达式的值 | 需要 return |
| 名字 | 匿名(没有名字) | 有名字 |
| 文档 | 不能有 docstring | 可以有 |
| 可读性 | 简单场景更简洁 | 复杂逻辑更清晰 |
用 lambda 的场景:需要一个简单的函数做参数(如 sort(key=...)、map、filter),且逻辑简单到一行就能写完。
其他情况一律用 def。不要用 lambda 赋值给变量(square = lambda x: x**2),这种写法没有好处。