函数定义与调用

到目前为止,我们所有代码都写在一个 .py 文件里”从上到下”跑。这只适合短脚本,写稍微大点的程序就会痛苦——重复代码到处都是、变量名相互干扰、想改一处得翻半天。函数是解决这些问题的第一步:”给一段代码起个名字,以后想用就调用“。这一篇讲清楚函数的最基本形态:定义、调用、返回值、docstring、类型注解。参数详解和作用域下一篇再展开。

一、定义与调用

Python 用 def 关键字定义函数:

1
2
3
4
5
def greet(name):
print(f"你好,{name}")


greet("咖飞") # 你好,咖飞

结构:

  • def:关键字;
  • greet:函数名,遵循 snake_case;
  • (name):参数列表(可以为空 ());
  • 冒号 + 缩进:函数体;
  • 调用:greet("咖飞") 就像加个括号。

二、返回值:return

函数用 return 返回值:

1
2
3
4
5
def add(a, b):
return a + b

result = add(3, 5)
print(result) # 8

没有 return 或空 return 的函数返回 None

1
2
3
4
5
def hello():
print("hi")

result = hello()
print(result) # None

一个函数可以有多个 return——遇到第一个匹配的就退出:

1
2
3
4
5
6
def sign(n):
if n > 0:
return "正"
if n < 0:
return "负"
return "零"

三、返回多个值(其实是一个元组)

1
2
3
4
5
def stats(nums):
return min(nums), max(nums), sum(nums) / len(nums)

low, high, avg = stats([1, 2, 3, 4, 5])
print(low, high, avg) # 1 5 3.0

Python 并没有”多返回值”,只是打包成元组再解包。想返回带名字的多值,用 namedtupledataclass类与对象基础)。

四、文档字符串 docstring

函数体第一行如果是字符串,就是 docstring:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def circle_area(r):
"""计算圆的面积。

Args:
r: 半径,非负实数

Returns:
面积,float
"""
return 3.14159 * r * r


print(circle_area.__doc__)
help(circle_area) # 在 REPL 里查文档

好处:

  • IDE 自动显示;
  • help() 直接可读;
  • 生成 API 文档时被 Sphinx 等工具抓取。

推荐的 docstring 风格:Google 风格(如上例)或 NumPy 风格。选一个,团队里保持一致。

五、类型注解(type hints)

Python 3.5+ 支持给参数和返回值加类型注解:

1
2
3
4
5
def add(a: int, b: int) -> int:
return a + b

def greet(name: str) -> None:
print(f"你好,{name}")

注解不改变运行时行为——传 add("1", "2") 依然能跑(结果 "12")。它是给 IDE / mypy / pylance 看的,能大幅减少低级错误。

复杂类型:

1
2
3
4
5
def top_k(items: list[int], k: int) -> list[int]:
return sorted(items, reverse=True)[:k]

def get_user(id: int) -> dict[str, str] | None:
...

Python 3.10+ 才能用 X | Y 表示 Union。老版本要 from typing import Optional, Union

完整的类型注解见 类型注解 type hints 实战

六、函数是”一等公民”

Python 里函数就是普通对象,可以:

  • 赋值给变量;
  • 作为参数传递;
  • 作为返回值;
  • 存在容器里;
  • 有属性(__name____doc__ 等)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def double(x):
return x * 2

f = double # 变量名指向函数对象
print(f(5)) # 10

# 传给别的函数
list(map(double, [1, 2, 3])) # [2, 4, 6]

# 存进字典(策略模式)
ops = {
"add": lambda a, b: a + b,
"sub": lambda a, b: a - b,
}
print(ops["add"](3, 4)) # 7

这个特性让 Python 天然支持函数式编程,也让”高阶函数””装饰器”成为可能——这些后面几篇会展开。

七、函数属性

1
2
3
4
5
6
7
8
9
def foo():
"""foo 的说明"""
pass

print(foo.__name__) # 'foo'
print(foo.__doc__) # 'foo 的说明'

foo.custom = 123 # 甚至能挂自定义属性
print(foo.custom) # 123

八、匿名函数 lambda

对于极简函数,可以用 lambda

1
2
3
4
5
double = lambda x: x * 2
add = lambda a, b: a + b

# 常用在 sort 的 key
sorted(users, key=lambda u: u.age)

lambda 只能写一个表达式,不能有语句。复杂逻辑还是用 def。详细见 匿名函数 lambda 与高阶函数

九、纯函数 vs 有副作用的函数

  • 纯函数:不依赖外部状态,不修改外部状态,同样输入总得到同样输出;
  • 有副作用:修改全局变量、打印、写文件、发网络请求都算副作用。
1
2
3
4
5
6
7
8
9
10
# 纯函数
def add(a, b):
return a + b

# 有副作用
total = 0
def add_to_total(x):
global total
total += x
print(f"当前总和 {total}")

能写纯函数就写纯函数——好测试、好组合、好并发。副作用集中在少数入口处。

十、默认参数与短小函数

默认参数的完整讲解在 参数详解,这里预告:

1
2
3
4
5
def greet(name, greeting="你好"):
print(f"{greeting}{name}")

greet("咖飞") # 你好,咖飞
greet("小王", "早上好") # 早上好,小王

十一、写函数的几条铁律

作为一个 Python 老手,我觉得写函数最重要的几点:

  1. 一个函数只做一件事(Single Responsibility);
  2. 函数不超过 30 行——超过就该拆;
  3. 参数不超过 4 个——多了改成传字典 / 类;
  4. 优先纯函数
  5. 函数名是动词get_usersave_configis_valid
  6. 必写 docstring(哪怕只有一行);
  7. 给参数和返回值加类型注解
  8. 不写”神奇”的默认参数,尤其是可变默认值(见下节陷阱)。

十二、常见陷阱

陷阱 1:可变默认参数

1
2
3
4
5
6
def append_item(item, lst=[]):
lst.append(item)
return lst

print(append_item(1)) # [1]
print(append_item(2)) # [1, 2] !! 上一次的 lst 还在

原因:默认参数 [] 只在函数定义时求值一次,后续所有调用共享同一个列表。正确做法

1
2
3
4
5
def append_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst

这是 Python 面试的经典题,一定要记住。

陷阱 2:忘了 return

1
2
3
4
def largest(nums):
m = max(nums) # 忘了 return

print(largest([1, 2, 3])) # None

函数没 return 就返回 None,用返回值时 None 会导致下游报错。

陷阱 3:修改传入的可变对象

1
2
3
def sort_items(lst):
lst.sort() # 就地修改,调用方的 lst 也变了
return lst

如果你不想让调用方感知这个改动:lst = sorted(lst)

陷阱 4:函数名与内置冲突

1
2
3
4
def list(x):
return x

print(list([1, 2, 3])) # 现在 list 是我们的函数,内置 list 被覆盖

避免用 listdictstrtypeidinput 等作为变量或函数名。

陷阱 5:返回不一致

1
2
3
4
5
def find(items, target):
for x in items:
if x == target:
return x
# 找不到时忘了 return,隐式返回 None

明确写 return None,或者抛异常。让”找不到”的语义清晰。

十三、一个完整例子:温度转换工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# temperature.py
"""温度单位转换工具集。"""


def celsius_to_fahrenheit(c: float) -> float:
"""摄氏 → 华氏。"""
return c * 9 / 5 + 32


def fahrenheit_to_celsius(f: float) -> float:
"""华氏 → 摄氏。"""
return (f - 32) * 5 / 9


def celsius_to_kelvin(c: float) -> float:
"""摄氏 → 开尔文。"""
return c + 273.15


def kelvin_to_celsius(k: float) -> float:
"""开尔文 → 摄氏。"""
return k - 273.15


def convert(value: float, from_unit: str, to_unit: str) -> float:
"""通用转换函数。所有非摄氏单位先转成摄氏,再从摄氏转到目标单位。"""
from_unit = from_unit.upper()
to_unit = to_unit.upper()

# 先转成摄氏
if from_unit == "C":
c = value
elif from_unit == "F":
c = fahrenheit_to_celsius(value)
elif from_unit == "K":
c = kelvin_to_celsius(value)
else:
raise ValueError(f"未知单位:{from_unit}")

# 再从摄氏转出
if to_unit == "C":
return c
if to_unit == "F":
return celsius_to_fahrenheit(c)
if to_unit == "K":
return celsius_to_kelvin(c)
raise ValueError(f"未知单位:{to_unit}")


if __name__ == "__main__":
# 冰水混合物
assert celsius_to_fahrenheit(0) == 32
assert convert(100, "C", "F") == 212
assert convert(212, "F", "C") == 100

print(f"25°C = {convert(25, 'C', 'F'):.1f}°F")
print(f"25°C = {convert(25, 'C', 'K'):.2f}K")

代码里演示了:函数拆分、类型注解、docstring、raise 抛错、assert 自测、convert 总调度。

十四、小结与延伸阅读

  • def name(params): 定义函数;
  • return 返回值,没写等于返回 None;
  • 函数是一等公民——可以传递、赋值、放容器里;
  • 别用可变对象做默认参数;
  • 给函数加 docstring 和类型注解;
  • 一个函数一件事,30 行封顶;
  • 避免函数名遮蔽内置。

延伸阅读:

下一篇 参数详解:默认参数、可变参数、关键字参数 我们把函数的”参数”话题彻底讲透。