到目前为止,我们所有代码都写在一个 .py 文件里”从上到下”跑。这只适合短脚本,写稍微大点的程序就会痛苦——重复代码到处都是、变量名相互干扰、想改一处得翻半天。函数是解决这些问题的第一步:”给一段代码起个名字,以后想用就调用“。这一篇讲清楚函数的最基本形态:定义、调用、返回值、docstring、类型注解。参数详解和作用域下一篇再展开。
一、定义与调用
Python 用 def 关键字定义函数:
1 | def greet(name): |
结构:
def:关键字;greet:函数名,遵循 snake_case;(name):参数列表(可以为空());- 冒号 + 缩进:函数体;
- 调用:
greet("咖飞")就像加个括号。
二、返回值:return
函数用 return 返回值:
1 | def add(a, b): |
没有 return 或空 return 的函数返回 None:
1 | def hello(): |
一个函数可以有多个 return——遇到第一个匹配的就退出:
1 | def sign(n): |
三、返回多个值(其实是一个元组)
1 | def stats(nums): |
Python 并没有”多返回值”,只是打包成元组再解包。想返回带名字的多值,用 namedtuple 或 dataclass(类与对象基础)。
四、文档字符串 docstring
函数体第一行如果是字符串,就是 docstring:
1 | def circle_area(r): |
好处:
- IDE 自动显示;
help()直接可读;- 生成 API 文档时被 Sphinx 等工具抓取。
推荐的 docstring 风格:Google 风格(如上例)或 NumPy 风格。选一个,团队里保持一致。
五、类型注解(type hints)
Python 3.5+ 支持给参数和返回值加类型注解:
1 | def add(a: int, b: int) -> int: |
注解不改变运行时行为——传 add("1", "2") 依然能跑(结果 "12")。它是给 IDE / mypy / pylance 看的,能大幅减少低级错误。
复杂类型:
1 | def top_k(items: list[int], k: int) -> list[int]: |
Python 3.10+ 才能用 X | Y 表示 Union。老版本要 from typing import Optional, Union。
完整的类型注解见 类型注解 type hints 实战。
六、函数是”一等公民”
Python 里函数就是普通对象,可以:
- 赋值给变量;
- 作为参数传递;
- 作为返回值;
- 存在容器里;
- 有属性(
__name__、__doc__等)。
1 | def double(x): |
这个特性让 Python 天然支持函数式编程,也让”高阶函数””装饰器”成为可能——这些后面几篇会展开。
七、函数属性
1 | def foo(): |
八、匿名函数 lambda
对于极简函数,可以用 lambda:
1 | double = lambda x: x * 2 |
lambda 只能写一个表达式,不能有语句。复杂逻辑还是用 def。详细见 匿名函数 lambda 与高阶函数。
九、纯函数 vs 有副作用的函数
- 纯函数:不依赖外部状态,不修改外部状态,同样输入总得到同样输出;
- 有副作用:修改全局变量、打印、写文件、发网络请求都算副作用。
1 | # 纯函数 |
能写纯函数就写纯函数——好测试、好组合、好并发。副作用集中在少数入口处。
十、默认参数与短小函数
默认参数的完整讲解在 参数详解,这里预告:
1 | def greet(name, greeting="你好"): |
十一、写函数的几条铁律
作为一个 Python 老手,我觉得写函数最重要的几点:
- 一个函数只做一件事(Single Responsibility);
- 函数不超过 30 行——超过就该拆;
- 参数不超过 4 个——多了改成传字典 / 类;
- 优先纯函数;
- 函数名是动词:
get_user、save_config、is_valid; - 必写 docstring(哪怕只有一行);
- 给参数和返回值加类型注解;
- 不写”神奇”的默认参数,尤其是可变默认值(见下节陷阱)。
十二、常见陷阱
陷阱 1:可变默认参数
1 | def append_item(item, lst=[]): |
原因:默认参数 [] 只在函数定义时求值一次,后续所有调用共享同一个列表。正确做法:
1 | def append_item(item, lst=None): |
这是 Python 面试的经典题,一定要记住。
陷阱 2:忘了 return
1 | def largest(nums): |
函数没 return 就返回 None,用返回值时 None 会导致下游报错。
陷阱 3:修改传入的可变对象
1 | def sort_items(lst): |
如果你不想让调用方感知这个改动:lst = sorted(lst)。
陷阱 4:函数名与内置冲突
1 | def list(x): |
避免用 list、dict、str、type、id、input 等作为变量或函数名。
陷阱 5:返回不一致
1 | def find(items, target): |
明确写 return None,或者抛异常。让”找不到”的语义清晰。
十三、一个完整例子:温度转换工具
1 | # temperature.py |
代码里演示了:函数拆分、类型注解、docstring、raise 抛错、assert 自测、convert 总调度。
十四、小结与延伸阅读
def name(params):定义函数;return返回值,没写等于返回 None;- 函数是一等公民——可以传递、赋值、放容器里;
- 别用可变对象做默认参数;
- 给函数加 docstring 和类型注解;
- 一个函数一件事,30 行封顶;
- 避免函数名遮蔽内置。
延伸阅读:
- 官方函数定义:https://docs.python.org/zh-cn/3/tutorial/controlflow.html#defining-functions
- PEP 257 Docstring 规范:https://peps.python.org/pep-0257/
下一篇 参数详解:默认参数、可变参数、关键字参数 我们把函数的”参数”话题彻底讲透。