上一节的程序已经能判断、能循环了,但代码一多就变成一大坨。 本节给两样"整理工具":函数(把一段活儿打包,起个名字,随叫随用)和模块(把别人写好的整套函数拿来直接用)。 本节也是全章最容易踩坑的一节——因为"装一个第三方库"这件事,在现在的树莓派上和网上老教程写的完全不一样了。
前面几节写的代码都是"平铺直叙"的:从上往下一条条执行。函数是第一次"给代码分层"——6-4 猜数字游戏里那三段逻辑(出题、判断、主流程),本节会把它们拆成三个函数。
另外本节会正经讲 import——其实你上一节已经偷偷用过一次了(import random),当时说"下一节正式讲",就是现在。
读法建议:本节的"第二节(模块)"里有一段关于装库的大坑,是本节最值钱的内容——中文教程在这个点上有 90% 是过期的。
一、函数:把一段活儿打包
1.1 为什么需要函数:看一段"复制粘贴"的代码
假设要算三次圆面积:
r1 = 5
area1 = 3.14159 * r1 * r1
print(f"半径 {r1} 的圆,面积是 {area1:.2f}")
r2 = 10
area2 = 3.14159 * r2 * r2
print(f"半径 {r2} 的圆,面积是 {area2:.2f}")
r3 = 2.5
area3 = 3.14159 * r3 * r3
print(f"半径 {r3} 的圆,面积是 {area3:.2f}")
问题很明显:3.14159 * r * r 抄了三遍。
- 想改精度(改成 3.14)?三处都得改,漏一处就是隐藏 bug。
- 想加个判断(半径不能是负数)?又得写三遍。
用函数改造:
import math
def circle_area(r):
"""算圆的面积"""
if r < 0:
return None
return math.pi * r * r
for r in [5, 10, 2.5]:
print(f"半径 {r} 的圆,面积是 {circle_area(r):.2f}")
函数 = 给一段代码起个名字。 好处就三条:① 不用抄(改一处就全改);② 逻辑有名字了,一眼看懂在干什么;③ 能反复用。 这一条其实你在 函数的定义 里见过——C++ 和 Python 在这一点的思想完全一样,只是写法不同。
1.2 定义与调用:def 和"打括号"
def say_hello(): # ← 定义:def 函数名(参数): ← 冒号别忘
print("你好!") # ← 函数体:必须缩进 4 格
say_hello() # ← 调用:函数名 + 括号
say_hello() # 想用几次就叫几次
三件事要分清:
| 写法 | 意思 |
|---|---|
def say_hello(): |
定义它(只是"教会"Python 这件事,不执行) |
say_hello() |
调用它(真的执行一次) |
say_hello |
只是提到这个名字(没有括号,不执行) |
say_hello # 没报错,也什么都没发生
不报错、不执行、静默无事——这是最难查的一类问题(3-5 讲过的"安静失败")。 记住:函数名不写括号,就是"提到它",不是"用"它。
1.3 参数:把"变化的部分"留成口子
def greet(name, times):
for _ in range(times): # _ 表示"这个变量我用不上,占个位"
print(f"你好,{name}!")
greet("波仔", 2)
输出:
你好,波仔!
你好,波仔!
为什么参数很重要:因为函数要"能通用"。如果你把 波仔 写死在函数里,那这个函数就只能对一个人说你好。
1.4 返回值:return——这是最需要分清的一点
def add(a, b):
return a + b # 把结果"交出去"
result = add(3, 5) # 用变量接住
print(result) # 8
这是新手最大的困惑点,必须分清:
print(...) |
return ... |
|
|---|---|---|
| 干什么 | 给人看(显示在屏幕上) | 给程序用(把结果交回调用处) |
| 结果能继续算吗 | 不能 | 能 |
| 打比方 | 用广播喊出来 | 把东西递到你手里 |
def bad_add(a, b):
print(a + b) # 只能看,拿不走
def good_add(a, b):
return a + b # 能拿走
x = bad_add(3, 5) # 屏幕上显示 8
print(x) # 但 x 是 None!因为函数没 return
y = good_add(3, 5) # 屏幕上什么都不显示
print(y * 2) # 16 —— 结果能接着算
一个函数没有 return,它返回的就是 None(6-3 学过的"空")。
return 还有第二个作用:立刻结束函数(和循环里的 break 有点像):
def check_age(age):
if age < 0:
return "年龄不能是负数" # 直接结束,后面的代码不执行
if age < 18:
return "未成年"
return "成年"
print(check_age(-5)) # 年龄不能是负数
print(check_age(10)) # 未成年
print(check_age(25)) # 成年
1.5 返回多个值(Python 的特产)
别的语言要返回两个值很麻烦,Python 直接返回:
def divide(a, b):
"""返回 (商, 余数)"""
return a // b, a % b
shang, yu = divide(10, 3) # 直接拆成两个变量接住
print(shang, yu) # 3 1
return a // b, a % b 实际返回的是一个元组(可以理解为"打包在一起的一小组值")。
左边用两个变量接,叫解包——这是 Python 很常用的写法,本节 Demo 会用到它。6-6 会正式讲元组。
1.6 默认参数:可以省略的"标配"
def connect(host, port=22):
print(f"连接 {host},端口 {port}")
connect("192.168.1.10") # 用默认端口 22
connect("192.168.1.10", 5900) # 指定端口 5900
def connect(port=22, host): # ❌ SyntaxError: non-default argument follows default argument
因为调用时 Python 是从左往右对号的,带默认值的参数如果放前面,后面的参数就永远对不上号。
还有个更好用的形式——按名字传参:
connect(host="192.168.1.10", port=5900) # 名字写清楚,顺序无所谓
参数多的时候强烈建议这么写(读代码的人一眼知道哪个是哪个,不容易传错顺序)。
1.7 作用域:函数里的变量是"私有的"
count = 0 # 全局变量(函数外面)
def add_one():
count = 1 # 这是新的局部变量!不是外面那个
print(f"函数里看到的是 {count}")
add_one() # 函数里看到的是 1
print(f"函数外还是 {count}") # 函数外还是 0
关键理解:函数内部新赋值的变量,默认是"局部"的——函数执行完就消失了,外面的同名变量动都没动。
count = 0
def add_one():
global count # 声明:我要改的是外面那个
count += 1
add_one()
add_one()
print(count) # 2
但更推荐的做法是"别用 global"——让函数用参数收数据、用 return 交结果,这样函数是"干净的",好懂也不容易出乱子。
global 是那种"知道有它就行,尽量别用"的东西。
1.8 文档字符串:给函数写一句"说明"
def circle_area(r):
"""算圆的半径 r 对应的面积(r 为负数时返回 None)"""
if r < 0:
return None
return math.pi * r * r
放在 def 下一行的三引号字符串,就是文档字符串(docstring)。 调用 help(circle_area) 就能看到它。
为什么要写:呼应 6-2 那个"报错里 Python 会告诉你 line N"——Python 对新手友好的一面,就是它处处留说明。你写的函数也留一句,过一个月回来还能立刻看懂。
二、模块:用别人写好的函数
2.1 模块是什么
一个 .py 文件就是一个模块。 你自己写的 hello.py 也是模块。
Python 自带一大批模块,叫标准库——装了 Python 就有,不用另外下载。
2.2 import 的三种写法
import random # ① 最常用:用的时候写 random.randint(...)
import random as rd # ② 起个短别名:用的时候写 rd.randint(...)
from random import randint # ③ 只拿一个函数出来:直接用 randint(...)
| 写法 | 调用时的样子 | 什么时候用 |
|---|---|---|
import random |
random.randint(1, 10) |
默认就用它——名字带在前面,一眼知道是谁家的 |
import ... as rd |
rd.randint(1, 10) |
名字太长时(如 import RPi.GPIO as GPIO) |
from ... import ... |
randint(1, 10) |
只用一两个函数时(如 from time import sleep) |
from ... import ... 会把名字直接搬进你的命名空间,可能和你的变量撞名。
最典型的是:from math import *(把所有东西都搬进来)——它里面有个 pow、有个 e,很容易和你自己的变量撞上,撞了之后行为会变得很莫名其妙。
建议:一律写成 import 模块名 或 import 模块名 as 短名,把来源标清楚。
2.3 三个马上能用的标准库
import random
import math
from time import sleep
print(random.randint(1, 100)) # 1~100 的随机整数(含两端)
print(random.choice(["红", "绿", "蓝"])) # 从一串里随机挑一个
print(math.sqrt(16)) # 平方根 → 4.0
print(math.pi) # 圆周率 3.141592653589793
print(round(math.pi, 2)) # 四舍五入到两位 → 3.14
print("倒数 3 秒…")
for i in range(3):
print(i + 1)
sleep(1) # 停 1 秒(这就是 3-3 灯语实验里的那个 sleep)
print("开始!")
你以后写的所有硬件控制代码里都会有它——让 LED 闪、让蜂鸣器响、让传感器隔一段读一次,全靠 sleep() 控制节奏。
单位是秒,可以是小数:sleep(0.5) 就是半秒。
三、装第三方库:本节最大的坑(老教程在这儿全军覆没)
标准库解决不了的事(比如"发一个 HTTP 请求"),就需要第三方库。
网上几乎所有的中文教程都这么教你:
pip3 install requests # ⚠️ 现在的树莓派上,这行会直接失败
你实际会看到:
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to install.
...
这个机制叫 PEP 668,从 Bookworm 起生效,Trixie 依然(对应本课 2-2 讲的系统换代)。
原因:以前 pip install 会往系统 Python 目录里塞包,和 apt 装的包混在一起——经常把系统工具搞坏(树莓派上有的系统程序自己就依赖 Python)。
所以现在系统默认锁死了"直接往全局装包"这条路。
一句话记:pip install 直接装 = 已经不允许了。 看到 externally-managed-environment 别慌,这不是你错了,是玩法变了。
现在正确的两条路:
3.1 路线 A:用 apt 装(最推荐,树莓派上首选)
很多常用库已经被 Debian 打包好了,直接用系统包管理器装:
sudo apt update
sudo apt install python3-requests
apt 的包名带 python3- 前缀,但 Python 里 import 时不带:
| 装的时候 | 用的时候 |
|---|---|
sudo apt install python3-requests |
import requests |
sudo apt install python3-opencv |
import cv2 |
sudo apt install python3-gpiozero |
from gpiozero import LED |
装完 import 报 ModuleNotFoundError?先回头核对包名写对没有。 这是本节第二个高频坑。
验证装好了:
python3 -c "import requests; print(requests.__version__)"
能打印出版本号就成功。
3.2 路线 B:虚拟环境(venv)——项目专用的小隔间
如果某个库 apt 里没有(或者版本太旧),就得用 venv:给这个项目单独开一个"小 Python 环境",装什么都只影响这里,不碰系统。
# ① 进你的项目目录
cd ~/projects
# ② 建一个虚拟环境(名字随便,习惯叫 venv 或 .venv)
python3 -m venv venv
# ③ 激活它(注意前面那个点,还有中间的空格)
source venv/bin/activate
激活成功的标志:命令提示符前面多了一对括号,像这样:
(venv) 你的用户名@树莓派:~/projects$
# ④ 现在可以正常 pip install 了
pip install requests
# ⑤ 用完退出
deactivate
① 每次新开终端都要重新激活(source venv/bin/activate)。不激活就用 pip install,还是会被 PEP 668 拒绝——这也是九成人"明明建了环境还报错"的原因。
② 如果报错说缺 ensurepip,先补一个包:
sudo apt install python3-venv
③ ⚠️ 如果你的项目要用 GPIO(这是本课后面一定会遇到的),建 venv 时必须加一个参数:
python3 -m venv --system-site-packages venv
为什么:gpiozero 那些库是 apt 装在系统里的,普通 venv 默认看不见系统包——不加这个参数,你在 venv 里 import gpiozero 会失败,而且报错信息还看不出原因。
--system-site-packages 的意思是"让这个虚拟环境也能看见系统里的库"。以后做硬件项目,加它就对了。
3.3 三条路怎么选(一句话版)
| 情况 | 用什么 |
|---|---|
| apt 里有这个库(大多数常用库) | sudo apt install python3-xxx ← 树莓派上首选 |
| apt 里没有 / 版本太旧 / 想隔离 | venv + pip install |
| 只是临时试一下(不推荐) | pip install --break-system-packages xxx ← 能绕开保护,但可能搞坏系统,别用 |
四、动手 Demo:把猜数字游戏拆成函数
上一节那个猜数字游戏,现在用函数重写一遍——你会立刻感受到"代码变清楚了"。
cd ~/projects
nano guess2.py
内容:
import random
def make_secret():
"""随机生成一个 1~100 的整数"""
return random.randint(1, 100)
def check(guess, secret):
"""比较猜测与答案,返回 (是否猜对, 提示文字)"""
if guess < secret:
return False, "小了,再大一点。"
elif guess > secret:
return False, "大了,再小一点。"
else:
return True, "猜对了!"
def play():
"""一整局游戏"""
secret = make_secret()
tries = 0
print("我想了一个 1 到 100 之间的整数,你来猜。")
while True:
guess = int(input("请输入你的猜测:"))
tries += 1
hit, msg = check(guess, secret) # 一次接住两个返回值
print(msg)
if hit:
print(f"答案就是 {secret},你一共猜了 {tries} 次。")
break
play()
跑:
python3 guess2.py
看到什么算成功:和 6-4 那个版本几乎一样。唯一的差别是最后两行分开了——新版先打印 猜对了!(这是 check() 返回的提示文字),再打印成绩(这句由 play() 打印)。
我想了一个 1 到 100 之间的整数,你来猜。
请输入你的猜测:50
小了,再大一点。
请输入你的猜测:80
大了,再小一点。
请输入你的猜测:65
猜对了!
答案就是 65,你一共猜了 3 次。
读结果 —— 对比一下两版有什么不同:
| 6-4 的版本 | 本节版本 | |
|---|---|---|
| 结构 | 一大坨,从上往下 | 三个小函数,各司其职 |
| 想看出题逻辑 | 得在中间找 | 看 make_secret() 就行 |
| 想判断逻辑 | 混在主循环里 | check() 独立,还能单独测试 |
| 想再来一局 | 得重启程序 | 再调一次 play() 就行 |
把最后一行 play() 改成:
play()
print("再来一局!")
play()
再跑一次——不需要改任何内部代码,游戏就能玩两轮。 这就是函数最实际的价值:"能反复用"不是理论,是你现在就能感受到的东西。
五、30 秒实验
实验一:print 和 return 的差别,亲手验一遍
>>> def bad(a, b):
... print(a + b)
...
>>> def good(a, b):
... return a + b
...
>>> x = bad(3, 5)
8
>>> print(x)
None # ← 只能看,拿不走
>>> y = good(3, 5) # 屏幕上什么都不显示
>>> print(y * 2)
16 # ← 能接着算
亲自看见那个 None,比读十遍解释都管用。
实验二:感受"局部变量"
>>> x = 100
>>> def change():
... x = 1
... print(f"函数里:{x}")
...
>>> change()
函数里:1
>>> print(f"函数外:{x}")
函数外:100 # ← 外面这个没被改
实验三:import 三种写法跑一遍
>>> import random
>>> random.randint(1, 100)
42
>>> import random as rd
>>> rd.randint(1, 100)
87
>>> from random import randint
>>> randint(1, 100)
13
三种写法都能拿到随机数,只是"叫它"的方式不同。
实验四(本节的重量级实验):亲手撞一次 PEP 668 的墙
先确认自己没在虚拟环境里(提示符前面没有 (venv)),然后:
pip3 install requests
你会看到那个 externally-managed-environment 报错。 恭喜——你亲眼见到了"老教程失效"的现场。
然后走正确路线:
sudo apt update
sudo apt install python3-requests
python3 -c "import requests; print('装上啦,版本', requests.__version__)"
说明一下:requests 在 Debian 仓库里是有的,所以这条路能走通。如果换成 apt 里没有的库,就得上 venv(见 3.2)。
六、常见误区
- "
print就是return,反正都出结果了" —— 完全不同。print给人看(拿不走,函数返回None),return给程序用(能接着算)。 这是新手最常混淆的一对。 - "函数名后面加不加括号都一样" —— 不一样。
func只是"提到它",func()才是"用"它。 少了括号不报错,但什么都不发生——这是最阴的一类 bug。 - "
pip install装库天经地义" —— 在现在的树莓派上会直接被拒(PEP 668)。首选sudo apt install python3-xxx;要 pip 就先进 venv。 - "apt 装了
python3-requests,代码里就该import python3-requests" —— 错。包名带python3-前缀,import时去掉:装python3-requests,写import requests。 - "建了 venv 就一劳永逸了" —— 每次新开终端都要重新
source venv/bin/activate。不激活就pip install,照样被 PEP 668 拒绝。 - "
from math import *挺省事的" —— 隐患大。它会把几十个名字搬进你的代码里(pow、e、pi…),很容易和你自己的变量撞名,撞了之后行为莫名其妙。写全import math,用math.pi更清楚。 - "函数里改全局变量直接改就行" —— 直接赋值只会新建一个局部变量,外面那个纹丝不动(而且不报错,最难查)。真要改就声明
global;更好的做法是用参数和return传。
自测三问(点开对答案)
1. `print(x)` 和 `return x` 最本质的区别是什么?一个没有 `return` 的函数,返回什么? 2. 现在的树莓派上 `pip3 install requests` 为什么会报 `externally-managed-environment`?正确的两条路是什么? 3. 做 GPIO 项目建虚拟环境时,为什么必须加 `--system-site-packages`? **参考答案** 1. **`print` 是"给人看"**——把值显示在屏幕上,但**程序拿不到它**;**`return` 是"给程序用"**——把值交回调用处,**能接着参与运算**。一个函数没有 `return`(或者 `return` 后面啥都没写),**返回的是 `None`**。 2. 因为 **PEP 668**:系统锁死了"往全局 Python 里装包"这条路,防止 pip 装的包和 apt 装的包打架、把系统工具搞坏(**Bookworm 起生效,Trixie 依然**)。两条正路:**① `sudo apt install python3-xxx`**(Debian 已经打包好的库,树莓派上首选);**② 建 venv,`source venv/bin/activate` 激活之后再 `pip install`。** 3. 因为 **`gpiozero` 这些硬件库是 apt 装在系统 Python 里的**,而**虚拟环境默认"看不见"系统包**——不加这个参数,`import gpiozero` 会失败,而且报错信息看不出真正原因。**`--system-site-packages` 就是让虚拟环境也能用系统里的库。**- ☐ 下一节(6-6 Python 列表与字符串)
相关笔记
- Python 流程控制 —— 上一节:本节 Demo 是把它那个猜数字游戏拆成了函数
- 使用命令行安装和卸载应用程序 ——
apt install的完整用法;python3-xxx这种包名规则就是那里学的 - Raspberry Pi OS 应用程序界面 —— 「程序名 ≠ 软件包名」这件事的出处,本节又出现一次(
python3-requestsvsimport requests) - 函数的定义 —— 函数思想的对照;C++ 里还要区分"声明"和"定义",Python 里直接
def一步到位 - 你需要知道的背景知识 —— 那里说过 GPIO 要用 gpiozero,本节讲了"为什么它必须靠 apt 装"
- 🍓 树莓派 · 索引 —— 全课路线图