前言

上一篇记录了 Python 基础语法(数据类型、循环、字符串、文件读写),这次进入第二阶段:函数进阶与模块化

这阶段最大的感受是:从”写脚本”进化到”写程序”。之前 BMI 计算器是自上而下一把梭,现在开始学会拆函数、拆模块、处理异常——代码从”能跑”变成”能维护”。

一、函数基础

1.1 定义与调用

1
2
3
4
5
def calculate_bmi(weight: float, height: float) -> float:
"""根据体重(kg)和身高(m)计算 BMI"""
if height <= 0 or weight <= 0:
raise ValueError("身高和体重必须大于0")
return weight / (height ** 2)

Python 函数相比 JS 的几个不同点:

多返回值(自动打包成 tuple):

1
2
3
4
5
6
def get_circle_info(radius: float) -> tuple[float, float]:
perimeter = 2 * 3.14159 * radius
area = 3.14159 * radius ** 2
return perimeter, area # 自动打包成 tuple

p, a = get_circle_info(5.0) # 解包接收

关键字参数(可读性极高):

1
2
3
4
def describe_pet(animal: str, name: str, age: int = 1) -> None:
print(f"我有一只{animal},叫{name}{age}岁")

describe_pet(name="咪咪", animal="猫", age=2) # 顺序无所谓,意图清晰

1.2 作用域 — LEGB 规则

Python 查找变量的顺序:Local → Enclosing → Global → Built-in

1
2
3
4
5
6
7
8
9
def demo() -> None:
message = "外层" # demo 的局部变量

def inner() -> None:
message = "内层" # 只影响 inner 自己
print(message)

inner()
print(message) # 还是"外层",inner 没改到它

global 关键字:在函数里修改全局变量,能不用就不用——改成参数传递和返回值更干净。

二、进阶参数 — 这阶段的重头戏

2.1 *args:接收任意多个位置参数

1
2
3
4
5
6
7
8
9
def add_all(*numbers: int) -> int:
"""*numbers 在函数内部是一个 tuple"""
total = 0
for n in numbers:
total += n
return total

add_all(1, 2, 3) # 6
add_all(1, 2, 3, 4, 5) # 15

对比 JS:相当于 function add(...numbers) { } 的 rest 参数,思路完全一样。

2.2 **kwargs:接收任意多个关键字参数

1
2
3
4
5
6
7
8
9
def create_user(name: str, age: int, **kwargs: object) -> dict:
"""**kwargs 在函数内部是一个 dict"""
user = {"name": name, "age": age}
for key, value in kwargs.items():
user[key] = value
return user

create_user("小红", 16, city="上海", hobby="画画")
# {'name': '小红', 'age': 16, 'city': '上海', 'hobby': '画画'}

2.3 解包运算符 * / **

调用函数时把列表/字典”展开”传入:

1
2
3
4
5
6
7
8
def introduce(name: str, age: int, city: str) -> str:
return f"{name}{age}岁,来自{city}"

person = ["小明", 15, "上海"]
introduce(*person) # 列表解包 → 位置参数

info = {"name": "小红", "age": 16, "city": "北京"}
introduce(**info) # 字典解包 → 关键字参数

2.4 ⚠️ 默认参数陷阱 — 新手必踩

这是 Python 最著名的坑之一,不要用可变对象(list/dict)做默认参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ❌ 错误:默认参数在函数定义时只创建一次,所有调用共享同一个对象!
def add_item_bad(item: str, items: list[str] = []) -> list[str]:
items.append(item)
return items

add_item_bad("苹果") # ['苹果']
add_item_bad("香蕉") # ['苹果', '香蕉'] ← 不是 ['香蕉']!上次的还在

# ✅ 正确:默认用 None,函数内部新建
def add_item_good(item: str, items: list[str] | None = None) -> list[str]:
if items is None:
items = [] # 每次调用都创建新列表
items.append(item)
return items

原因:Python 的默认参数在 def 执行时只求值一次,之后所有调用共享同一个列表对象。

三、模块化

3.1 import 的几种方式

1
2
3
4
5
6
7
8
import game_utils                  # 方式1:导入整个模块
game_utils.get_hint(5, 7)

from game_utils import get_hint # 方式2:只导入需要的函数
get_hint(5, 7)

import game_utils as gu # 方式3:起别名
gu.get_hint(5, 7)

最佳实践

  • 只 import 需要的名字,避免命名空间污染
  • 模块自己也有 __name__ 属性:被导入时是模块名,直接运行时是 "__main__"

3.2 __name__ == "__main__" — 模块的双重身份

这个惯用法让同一个文件既可以被导入,又可以独立运行

1
2
3
4
5
6
7
8
9
10
# game_utils.py
def get_hint(guess: int, target: int) -> str:
if guess == target:
return "恭喜你猜对了!"
...

if __name__ == "__main__":
# 直接 python game_utils.py 时才会执行
# 被别的文件 import 时不会执行
print("模块自测:", get_hint(5, 7))

对比 JS:Node 里也有类似概念(require.main === module),但 Python 的这个写法更常见,几乎每个文件结尾都能看到。

3.3 实战:模块拆分

把猜数字游戏拆成两个文件,职责分明:

1
2
3
4
5
6
02_functions/
├── game_utils.py # 工具层:纯计算函数
│ ├── generate_target() # 生成随机数
│ ├── get_hint() # 返回提示
│ └── get_valid_int() # 健壮输入验证
└── exercise_guess_game_module.py # 流程层:main() 游戏循环

工具层不关心游戏怎么玩,只管”给我数字,我返回提示”;流程层不关心数字怎么生成,只管调用。这就是模块化的核心思想。

四、异常处理

4.1 完整结构

1
2
3
4
5
6
7
8
try:
result = a / b # 可能出错的代码
except ZeroDivisionError as e:
print(f"出错: {e}") # 捕获并处理
else:
print(f"成功: {result}") # 没出错才执行
finally:
print("结束") # 无论对错都执行

记忆口诀try 试 → except 抓 → else 成了 → finally 收尾。

4.2 raise 主动抛出 + 自定义异常

1
2
3
4
5
6
7
8
9
class InsufficientFundsError(Exception):
"""余额不足"""
def __init__(self, balance: float, amount: float) -> None:
super().__init__(f"余额不足:当前{balance}元,需要{amount}元")

def withdraw(balance: float, amount: float) -> float:
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount

4.3 实战:健壮的用户输入

学异常处理之前,猜数字游戏只要用户输入字母就崩溃。学完之后的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
def get_valid_int(prompt: str, min_val: int, max_val: int) -> int:
"""反复要求输入,直到拿到合法整数"""
while True:
try:
value = int(input(prompt))
except ValueError:
print("请输入数字!")
continue
if not min_val <= value <= max_val:
print(f"请输入 {min_val}-{max_val} 之间的数!")
continue
return value

核心思想:把”可能会错”的代码放进 try,错了就优雅提示并循环重试,绝不崩溃。

五、综合项目:命令行计算器

把函数、模块、异常处理全部整合成一个完整程序:

5.1 架构设计

1
2
calculator_ops.py     # 运算层:add/subtract/multiply/divide
exercise_calculator.py # 应用层:菜单 + 历史 + 主循环

5.2 亮点一:*args 支持任意多个数字

1
2
3
4
5
6
7
8
9
10
11
12
13
def add(*numbers: float) -> float:
return sum(numbers)

def divide(*numbers: float) -> float:
result = numbers[0]
for num in numbers[1:]:
if num == 0:
raise ValueError("不能除以0")
result /= num
return result

add(1, 2, 3, 4) # 10
divide(100, 2, 5) # 10.0

5.3 亮点二:字典映射代替 if/elif

1
2
3
4
5
6
7
8
op_map = {
"1": ("加法", calc.add),
"2": ("减法", calc.subtract),
"3": ("乘法", calc.multiply),
"4": ("除法", calc.divide),
}
op_name, op_func = op_map[choice] # 一行拿到名字和函数
result = op_func(*numbers) # 直接调用

比一长串 if choice == "1": ... elif choice == "2": ... 简洁得多,也容易扩展。

5.4 踩过的两个坑

坑1:循环里漏了 return

1
2
3
4
5
6
7
8
def divide(*numbers):
result = numbers[0]
if len(numbers) > 1:
for num in numbers[1:]:
result /= num
else:
return result
# ← if 分支循环完没有 return,返回 None!

坑2:split(' ') vs split()

1
2
"1  2  3".split(' ')   # ['1', '', '2', '', '3']  ← 多个空格产生空字符串!
"1 2 3".split() # ['1', '2', '3'] ← split() 自动处理任意空白

split() 不带参数会同时处理空格、制表符、换行,且连续空白只算一个,永远比 split(' ') 更安全。

学习心得

1. 从”写脚本”到”写程序”

阶段1的 BMI 计算器是自上而下一把梭,能跑就行。这阶段学会了拆函数、拆模块、处理异常——同样的功能,代码从”能跑”变成了”能维护”。

2. 默认参数陷阱是最值得记的一个坑

def f(items=[]) 看起来人畜无害,实际上所有调用共享同一个列表。这不像 JS 每次调用都新建。养成”默认参数用 None”的习惯。

3. 异常处理不是”出问题才写”

get_valid_int 这个函数改变了我的思维:输入验证不是”出了问题再补”,而是设计时就考虑用户会乱输。把”可能会错”的代码放进 try,是健壮程序的标配。

4. 模块化让代码有了结构

game_utils.py 和主程序分开后,工具函数可以被多个程序复用,主程序的逻辑也更清晰。这种”纯函数 + 流程编排”的分层,和前端项目里 utils/ + views/ 的思路一脉相承。


项目地址:github.com/halely/python-study

下一篇预告:Python 面向对象——类、继承、魔术方法。

学习时间:2026-06-17