第二课:自动化与编程——让电脑为你工作
核心问题
电脑是工具。但如果你只会用它做别人设计好的事情——打开微信、打开浏览器、打开游戏——你就只是工具的消费者,不是工具的主人。
这一课想让你变成主人。
不是说你要成为职业程序员。而是说:当你遇到一个重复劳动——比如要把一百个文件改个名字、要从网页上抓一些数据下来整理、想让电脑在你睡觉的时候自动下载然后转换格式——你能意识到「这件事可以让电脑帮我做」,而且你知道从哪开始。
编程不是写代码。编程是设计让电脑为你工作的流程。
第一章:你已经会编程了
上节课你敲过的命令
回想一下第一课,你在终端里敲过这些:
ping 8.8.8.8
ip addr
curl -v https://example.com
你一条一条地敲,计算机一条一条地执行。你脑子里的意图,变成了计算机的实际动作。
现在请你做一件事:把这些命令写到一个文件里。
Windows:新建一个文本文件,改名为 test.bat,里面写:
ping 8.8.8.8
ipconfig
pause
双击。发生了什么?计算机把你刚才手动敲的命令,按文件里的顺序,一条一条执行了。
Linux / macOS:新建一个文件 test.sh,里面写:
#!/bin/bash
ping -c 4 8.8.8.8
ip addr
然后在终端里 chmod +x test.sh 赋予执行权限,再 ./test.sh。同样的效果。
这就是编程
代码说白了就是一套你已经记录下来的逻辑和步骤。你把你想让计算机做的事写成一份清单,计算机按照顺序去做。
顺序执行——这是编程的第一个、也是最基础的概念。就像你游戏里搭建的流水线:矿石进去 → 熔炉烧 → 铁锭出来 → 做成铁镐。每一步按顺序走,前一步的输出是后一步的输入。
你刚才写的那个 .bat 或 .sh 文件,就是一份最原始的清单。它没有判断、没有循环、没有变量——但它已经是程序了。你用不着先学语法学一个月才能说「我写过程序」——你刚才就写了。
从终端脚本到编程语言
终端脚本能做很多事,但它有局限:
你很难在中间保存一个值回头再用(变量虽然有,但是一团乱麻)
你想根据某个条件决定接下来干什么——比如「如果下载成功就转换格式,否则发邮件告诉我失败了」——终端脚本能写,但写起来像在解一道脑筋急转弯
你想处理复杂的数据结构——比如一个学生的所有成绩,按科目分类,算出平均分——终端脚本不是不能做,但它会让你想砸电脑
所以我们需要一个更好的工具。这就是编程语言——专门为「让人清楚地表达逻辑」而设计的语言。
这一课我们用 Python。不是因为它是最好的语言(没有最好的语言),而是因为它最像日常语言,能让你专注在想做的事情上,而不是跟语言的怪脾气搏斗。
后面我们会看到,语言的怪脾气不是坏东西——它们只是别人在另一条路上做的不同选择。但现在,我们先跟脾气最好的那个玩。
第二章:Turtle——你的第一个编程游戏
打开 Python 解释器
在你的终端里敲:
python3
你会看到三个大于号:
>>>
这就是 Python 解释器。它等着你下命令。
它不是写着玩的——你在解释器里敲的每一条命令,Python 都会当场执行,然后立刻告诉你结果。这叫交互模式(REPL——Read 读你的命令,Evaluate 计算,Print 打印结果,Loop 等着你再敲)。它像个随时待命的助手,你说一句,它做一句。
召唤小海龟
>>> from turtle import *
你什么都没看到?因为海龟还没出场。继续:
>>> forward(100)
一个窗口弹出来,一只小海龟从屏幕中央向前画了一条 100 像素的线。
>>> right(90)
>>> forward(100)
它右转 90 度,又向前画了一条线。你画了一个直角。
>>> left(90)
>>> forward(50)
>>> right(90)
>>> forward(80)
你在画什么?——你在指挥一个东西在屏幕上按照你的意愿行动。这就是编程的开始:你下发命令,计算机执行。
这和你游戏里的流水线一模一样
如果你玩过 Minecraft 的红石、Factorio 的传送带、或者缺氧里的自动化管道——你已经理解编程了。你把「挖矿→筛选→冶炼→存储」串成一条线,每个步骤各司其职。编程就是把你脑子里的流水线用文字描述出来,然后计算机照着跑。
不同的是,游戏里流水线每个节点是一个方块或一个机器,编程里每个节点是一行指令。
保存下来:从交互到脚本
在解释器里一句句敲很有意思,但你关掉窗口,一切就没了。就像你在游戏里搭了一个红石电路,退出存档就蒸发了——不行。
把你刚才在解释器里敲的东西复制出来,保存为一个文件,比如 my_turtle.py:
from turtle import *
forward(100)
right(90)
forward(100)
left(90)
forward(50)
right(90)
forward(80)
done() # 让窗口保持打开,别画完就关了
然后在终端里:
python3 my_turtle.py
同样的窗口弹出来,同样的海龟,同样的轨迹——但这次不是一句句敲的,是你把整个流程写下来,让计算机一口气跑完。
.py文件和.bat/.sh没有本质区别——都是「把你要做的事写下来然后让计算机去跑」。Python 只是给了你更丰富的词汇来写这份清单。
等等,有点无聊?
你现在指挥海龟做的事情是固定的。每次运行,它都画一样的形状。这不叫编程——这叫复读。
编程真正的力量在于:程序根据不同的情况,做不同的事。如果海龟能听你的键盘指令实时移动?如果它自己能在碰到边界时自动转弯?如果它能记住之前画过的图案然后重复?
我们一步一步来。第一步:让程序接收外界的输入。
第三章:变量——给数据起名字
你已经在用变量了
想象你在解一道物理题:
一个物体初速度 5 m/s,加速度 2 m/s²,求 3 秒后的位移。
你的草稿纸上写着:
v0 = 5
a = 2
t = 3
s = v0*t + 1/2*a*t²
你给 5 起了个名字 v0,给 2 起了个名字 a,给 3 起了个名字 t。为什么不起名字直接用数字?因为:
名字让你知道这个数字代表什么——三个月后回来看草稿,你不会对着
5*3 + 1/2*2*3²发愣如果初速度从 5 变成 6,你只需要改
v0 = 6一个地方,不用把所有公式里的 5 都翻出来改一遍你可以复用这个名字——
s = v0*t + 1/2*a*t²之后你还能用v0、a、t去算别的东西
编程里的变量,跟你物理草稿纸上的符号是一模一样的东西:给一个值起个名字,然后你可以用这个名字去引用它、操作它、反复使用它。
等号不是「等于」
先看一行代码:
x = 5
你心里在想什么?如果你觉得这是「x 等于 5」,那接下来的代码会让你困惑:
x = 5
x = x + 1
如果 = 是「等于」,那 x = x + 1 在数学上是荒谬的——没有哪个数等于自己加一。
所以 = 不是「等于」。它是赋值:把右边的东西,放到左边的名字里。
x = 5:把 5 放进名为 x 的容器。x = x + 1:把 x 容器里当前的值(5)拿出来,加 1,得到 6,然后把 6 放回 x 容器里。原来里面的 5 被覆盖了——没了。
这个过程很像你玩游戏时:
你的金币数是一个变量
你打怪掉了 100 金币 →
gold = gold + 100你买了一把剑花 50 金币 →
gold = gold - 50任何时候你想知道你有多少钱,看一眼
gold就行
从 input 开始:让程序跟你对话
回到海龟。与其每次都改代码让它画不同的形状,不如让它在跑的时候问你:
from turtle import *
steps = input("小海龟要走多少步?")
forward(int(steps))
done()
运行的时候,终端里会出现:
小海龟要走多少步?
你输入 150,回车——海龟走了 150 步。再跑一次,你输入 50,它走 50 步。
input() 函数做的事情很简单:暂停程序,等你在键盘上敲点什么,然后把你敲的东西作为字符串返回。
你刚才在代码里写的是 int(steps) 而不是直接 forward(steps)。为什么?
字符串 vs 数字:类型的第一次碰面
input() 返回的东西永远是字符串(str)。就算你输入的是 150——对 Python 来说,这是三个字符 "1"、"5"、"0",不是数字 150。
你可以试一下:
>>> a = input("输入一个数字:")
输入一个数字:5
>>> a * 10
'5555555555'
"5" * 10 不是 50——是 "5555555555"。因为 Python 对字符串做乘法,就是把字符串重复十遍。就像 "哈" * 3 等于 "哈哈哈"——挺自然对吧?但对数字来说,同样的 * 做的是算术乘法。
所以 input() 拿到的是字符串,但 forward() 要的是数字。我们需要转换:int("150") → 150(integer,整数)。
变量的命名规则
Python 里给变量起名字很自由:
用字母、数字、下划线,但不能以数字开头
不能用 Python 的关键字(
if、while、for、def、class等等——这些是 Python 自己占了的词)Python 支持 UTF-8,所以你可以用中文——
步数 = 100是完全合法的
步数 = int(input("走多少步?"))
forward(步数)
能跑。但不建议在正式代码里用中文变量名——不是因为技术问题,是因为你的代码可能在别人的电脑上跑,而别人不一定用中文系统。不过在你自己玩的时候,随便。
变量让你储存状态
回头看刚才那个金币的例子:
gold = 0 # 初始金币
gold = gold + 100 # 打怪
gold = gold - 50 # 买剑
print(gold) # 看看有多少
变量给了你储存中间状态的能力。你不用每次都从头算——你可以把算到一半的结果存下来,然后继续往下算。
就像玩 RPG 的时候,你的 HP、MP、经验值、身上哪件装备——这些全都是变量。游戏程序在后台不断读写这些变量,然后反映到屏幕上。你看到的血条,本质上就是 hp / max_hp * 100%。
变量也让你不用反复问用户输入。比如,你想让小海龟以 3 倍速行动:
speed_multiplier = int(input("倍速?(只问一次)"))
forward(100 * speed_multiplier)
right(90)
forward(50 * speed_multiplier)
left(90)
forward(80 * speed_multiplier)
用户只输入了一次 3,后面每一步都自动乘以 3。如果不用变量,你就得每一步之前都 input 一次——用户得烦死。
变量必须先赋值再使用
Python 有一个规矩:你不能用一个还没赋值过的变量。
>>> print(y)
NameError: name 'y' is not defined
这跟你做物理题一样——你不能在草稿纸上写 s = v*t 然后跟老师说「v 和 t 我回头再定义」。Python 是顺序执行的,从上到下一行一行来。你用到一个变量的时候,它必须是已经存在过的。
这跟数学的习惯有点不一样。数学里你可以先写 f(x) = x² + 2x + 1,然后后面再解释 x 是什么。Python 不允许——它是一步一步做事的,你必须先把东西准备好,再用它。
如果你好奇「有没有语言可以先写公式后给变量」,有——Haskell 就是这样。但 Haskell 先生有点太严苛了,我们等跟 Python 这些比较随和的小伙伴玩熟了再去找他吧。别急,你培养的编程思维几乎在所有语言里都能用。
第四章:类型——物以类聚,人以群分
为什么要有类型
你可能会想:变量不就是个容器吗?什么东西都能往里装不就行了?
试试这个:
>>> a = 5
>>> b = "你好"
>>> a + b
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Python 不让你把 5 和 "你好" 加在一起。为什么?因为「加」对数字和对字符串的意义完全不一样:
数字的「加」:算术运算,5 + 3 = 8
字符串的「加」:拼接,"你好" + "世界" = "你好世界"
混在一起算什么?Python 说:我不知道,你给个说法。
但这不仅仅是一个技术限制。类型的本质是:物以类聚,人以群分。世界万物都有它自己的特性和它能做的事情。只有知道你是谁、你能做什么,操作才有意义。
类比:你是一个 human,不是一个 object
假设你写了一个游戏,里面有一个「人类」和一块「石头」。人类可以 说话()、走路()、吃饭()。石头呢?石头不能说话也不能走路。如果你对石头说 石头.说话(),游戏引擎要么报错,要么假装没听见——因为石头这个类型没有「说话」这种操作。
编程语言的类型系统就是干这件事的:定义每一种数据是什么样的东西,以及它能做什么。
你当然可以说「所有类型都是 any 啊,我只想要一个东西」。但 any 做不了任何事——你不知道它是什么,你就不知道能拿它来干什么。就像你跟一个人说「给我一个东西」,他可能给你一块砖头,也可能给你一个气球——你用砖头能砸钉子,用气球不能。你必须知道它是什么,才能决定怎么用它。
Python 的基本类型
为什么 input() 总是返回字符串
回到刚才那个问题——为什么 input() 不聪明点,你输入 5 它就返回整数 5?
因为 input() 做的事情是从键盘读入一串字符。键盘只能输入字符——就算你按的是数字键 5,它产生的也是一个字符 "5",不是整数 5。input() 不知道你打算拿这个 "5" 当数字用还是当字符串用——它统一返回字符串,把决定权交给你。
这其实是好的设计:统一的返回类型对大家都更方便。如果 input() 有时候返回字符串,有时候返回整数,你的程序就得每次猜它这次给你的是什么——猜错了就崩。统一的返回类型意味着你可以预期它的行为,然后你决定要不要转换。
C++ 在这方面走了另一条路——你可以提前声明变量类型,然后
cin >> x会根据 x 的类型自动解释输入。两种选择各有利弊,没有谁是对的。编程语言中充满了这种「在几条性能相差无几的路中选了一条」的情况。就像我们后面会看到的——这不是 bug,这是工程积累的结果。
一个有趣的事实:生物也是这么干的
地球上所有生命诞生之初,蛋白质有两种可能的手性方向——左旋和右旋。今天包括人类在内的绝大部分生物都是左旋氨基酸为主。右旋氨基酸我们几乎不能消化、无法处理。
为什么会这样?不是左旋比右旋更好——纯粹是早期地球上某个偶然事件让左旋占了优势,然后一路固化下来。今天如果给你吃右旋蛋白,你的身体会把它们当成废塑料。
编程语言的设计也一样。= 是赋值(而不是等于)?input() 返回字符串?字符串用单引号还是双引号?这些选择很多没有绝对的「好」和「坏」——只是有人在某个时刻做了决定,然后生态在这个决定上长了起来。我们管这种不得不接受的历史选择叫偶然复杂度。
理解偶然复杂度很重要:你在编程时遇到的很多「为什么要这样?」不是因为你笨——是因为你在撞一堵别人几十年前砌的墙。你要做的就是理解这堵墙为什么在那里,然后翻过去,继续做你的事。
第五章:控制流——让程序做选择
到目前为止,我们的程序是顺序执行的:从第一行到最后一行,一条不落地跑完。但真正的程序需要做判断:「如果密码对了就登录,否则提示错误」「把所有未读邮件标记成已读」「一直尝试连接服务器,连上为止」。
这就是控制流——控制程序执行的流向。
if:让程序做决策
基本的 if 结构:
age = int(input("你几岁?"))
if age >= 18:
print("你可以进网吧了。")
else:
print("回家写作业。")
Python 的 if 读起来几乎就是英语:如果年龄大于等于 18,就打印那句话;否则,打印另一句。
你可以加更多分支:
score = int(input("考试成绩:"))
if score >= 90:
print("优秀")
elif score >= 80:
print("良好")
elif score >= 60:
print("及格")
else:
print("不及格")
elif 就是 else if 的缩写——「否则,如果……」。条件从上往下依次检查,第一个为真的被执行,后面的全部跳过。
缩进就是语法
你注意到没有,print 前面有四个空格。这不是装饰——在 Python 里,缩进就是语法。缩进告诉 Python 哪些代码属于这个 if 里面:
if age >= 18:
print("你可以进网吧了。") # 属于 if
print("但别玩太晚。") # 也属于 if
print("这句话不管年不年龄都会说。") # 不属于 if,缩进回去了
其他很多语言用大括号 {} 来表示代码块。Python 选择了缩进——这让代码看起来干净,但也意味着你必须保持缩进一致。混用空格和 Tab 会炸——选一个,坚持用。(推荐:四个空格。)
比较运算符
你刚才见到了 >=。Python 的比较运算符跟数学差不多:
注意:判断相等是 ==(两个等号)。因为 = 已经被赋值占用了。
这是一个典型的偶然复杂度——假如当年的语言设计者选了 := 做赋值,那 = 就可以用来做判断相等了。但他们选了 = 做赋值,所以判断相等就得用 ==。习惯就好。
逻辑运算符:组合条件
if age >= 12 and age <= 18:
print("你是青少年。")
if weekday == "周六" or weekday == "周日":
print("周末快乐!")
if not is_banned:
print("欢迎回来。")
and:两边都真才真or:有一边真就真not:取反
while:重复做,直到某个条件不满足
count = 1
while count <= 5:
print(f"这是第 {count} 次")
count = count + 1
输出:
这是第 1 次
这是第 2 次
这是第 3 次
这是第 4 次
这是第 5 次
while 的逻辑:先检查条件——条件为真就执行循环体——回到开头再检查——直到条件为假。
无限循环:如果你忘了让条件最终变成假,程序就会永远跑下去:
while True:
print("我停不下来了!")
按 Ctrl+C 可以强行终止。
# 一个更实用的无限循环:一直问,直到用户给有效输入
while True:
password = input("请输入密码:")
if password == "open_sesame":
print("密码正确,进入系统。")
break # 跳出循环
else:
print("密码错误,再试一次。")
break 就是「立刻跳出当前循环」。另一个关键词 continue 是「跳过本次循环的剩余部分,直接回到循环开头检查条件」。
for:遍历一个范围或一个集合
for i in range(5):
print(i)
输出:
0
1
2
3
4
注意:range(5) 从 0 开始,到 4 结束(不包括 5)。这是编程的常见习惯——从 0 开始计数。为什么?跟底层内存地址的计算有关,我们后面聊到 C 的时候再说。现在先接受:range(5) = 0, 1, 2, 3, 4。
for i in range(1, 6): # 从 1 到 5
print(i)
for i in range(0, 10, 2): # 0 到 9,步长 2
print(i) # 0, 2, 4, 6, 8
for 也可以遍历字符串里的每个字符:
for char in "Hello":
print(char)
# H
# e
# l
# l
# o
我们后面学了列表和字典之后,for 会变得更有用——它可以遍历任何「可迭代」的东西。
第六章:Python 的语法风格
在继续深入之前,暂停一下,聊聊你目前看到的 Python 代码有什么共同特征。不是要你背语法——是让你感受一下这门语言的设计哲学。
一切皆表达式(大部分)
Python 里,大部分东西都是表达式——它们计算出一个值,返回出来,你可以把这个值赋给变量、传给函数、嵌入 if 条件里:
name = input("你叫什么?") # input() 返回一个值
upper_name = name.upper() # upper() 返回一个新值
is_long = len(name) > 10 # len() 和 > 组合成一个布尔值
这种一致性让 Python 写起来很流畅。你学了一个东西(比如函数调用的语法),这个知识几乎在所有地方都适用。
少数不是表达式的
if、while、for 在 Python 里不是表达式——你不能写 x = if a > 0: 1 else: -1。你需要写成:
if a > 0:
x = 1
else:
x = -1
(Python 确实后来加了 x = 1 if a > 0 else -1——但这是专门加的语法糖,if 语句本身仍然不是表达式。)
这是 Python 的设计选择:控制流语句是语句,它们做事情,但不返回一个值。其他语言(比如 Rust、Haskell、JavaScript)在这件事上做了不同的选择。没有谁对谁错——只是不同的哲学。
注释
# 这是一个注释。Python 会忽略这行。
x = 5 # 行内注释也可以,但建议放在代码上面
# 注释用来解释「为什么这么做」
# 而不是「做了什么」(代码本身已经说了做了什么)
第七章:内置容器——不止一个值
到目前为止,每个变量只装了一个值。但现实中的数据往往是一组东西:一个班的学生名单、一个购物清单、一个配置项到配置值的映射。
Python 内置了四种容器类型,让你装多个值。
列表(list):有序、可变的集合
fruits = ["苹果", "香蕉", "橘子", "葡萄"]
print(fruits[0]) # 苹果(索引从 0 开始)
print(fruits[2]) # 橘子
print(fruits[-1]) # 葡萄(负数索引从末尾往前数)
print(len(fruits)) # 4
增删改查:
fruits.append("西瓜") # 加到末尾
fruits.insert(1, "草莓") # 在索引 1 处插入
fruits.remove("香蕉") # 删除第一个匹配的值
popped = fruits.pop() # 弹出最后一个元素,返回它
popped2 = fruits.pop(0) # 弹出索引 0 的元素
fruits[2] = "菠萝" # 直接修改索引 2
切片:取列表的一部分。
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[2:5]) # [2, 3, 4] 索引 2 到 4(不含 5)
print(numbers[:3]) # [0, 1, 2] 从开头到索引 2
print(numbers[7:]) # [7, 8, 9] 从索引 7 到末尾
print(numbers[::2]) # [0, 2, 4, 6, 8] 每隔一个取
遍历列表:
for fruit in fruits:
print(f"我喜欢吃{fruit}")
列表推导式:一种简洁的构建新列表的方式。
squares = [x**2 for x in range(10)] # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
evens = [x for x in range(20) if x % 2 == 0] # 筛出偶数
元组(tuple):不可变的列表
point = (3, 5)
rgb = (255, 128, 0)
元组和列表几乎一样,除了一个关键区别:元组创建后不能修改。不能 append、不能改元素。
point[0] = 10 # TypeError! 元组不能改
为什么需要不能改的列表?因为有时候你需要确保数据不被意外修改:「这个坐标就是 (3, 5),谁也别动它」。函数返回多个值的时候,其实也是在返回元组。
字典(dict):键值对的映射
student = {
"姓名": "张三",
"年龄": 18,
"成绩": 92
}
print(student["姓名"]) # 张三
student["成绩"] = 95 # 修改
student["班级"] = "高三1班" # 新增键值对
字典是效率极高的查找结构——你给它一个键(key),它立刻返回对应的值(value)。不管字典里有多少条数据,查找速度几乎不变。就像你去图书馆查一本书:你不需要从第一个书架翻到最后一个——你查目录,定位,直奔书的位置。
遍历字典:
for key, value in student.items():
print(f"{key}: {value}")
集合(set):不重复、无顺序的集合
tags = {"python", "编程", "自动化", "python"} # 重复的 "python" 被自动去掉
print(tags) # {'编程', '自动化', 'python'}(顺序不保证)
tags.add("AI")
tags.remove("自动化")
集合适合做「去重」和「判断成员是否存在」:
students = {"张三", "李四", "王五"}
print("张三" in students) # True
print("赵六" in students) # False
选哪个容器?
这四种容器你会越来越熟悉。现在不用硬背所有操作——知道有这些工具,用到的时候查就行。
第八章:函数——封装与复用
你一直在用函数
print("hello") 是一个函数调用。len(fruits) 是一个函数调用。int("150") 是一个函数调用。forward(100) 是一个函数调用。
函数就是一个有名字的代码块。你给它一些输入(可以没有),它做一些事情,可能返回一个值。
你已经用习惯了——现在学怎么写自己的函数。
定义函数
def greet(name):
"""向某人问好。"""
message = f"你好,{name}!欢迎回来。"
return message
def:定义函数的关键词greet:函数名(你自己起的)name:参数(调用函数时传进来的值)"""...""":文档字符串(可选但推荐——告诉别人这个函数是干什么的)return:返回的值
调用:
result = greet("小明")
print(result) # 你好,小明!欢迎回来。
为什么需要函数
1. 避免重复。 如果你在三处地方都要算圆的面积:
# 没有函数的写法
area1 = 3.14159 * r1**2
area2 = 3.14159 * r2**2
area3 = 3.14159 * r3**2
# 有函数的写法
def circle_area(radius):
return 3.14159 * radius**2
area1 = circle_area(r1)
area2 = circle_area(r2)
area3 = circle_area(r3)
如果哪天要改精度(3.14159 → 3.1415926535),没有函数你得改三个地方;有函数你只改一处。
2. 隐藏复杂度。 调用 circle_area(5) 的人不需要知道面积公式——他只需要知道「给我半径,我还你面积」。这就是封装:把复杂的内部实现包起来,对外只暴露简单的接口。
3. 让代码有意义。 circle_area(r) 比 3.14159 * r**2 更清楚地表达了你在做什么。代码是写给人看的,顺便给机器执行。
参数与返回值
函数可以没有参数,也可以有很多参数:
def say_hello(): # 无参数
print("Hello!")
def add(a, b): # 两个参数
return a + b
def make_sandwich(bread, *fillings): # 可变数量参数
print(f"面包: {bread}")
print(f"馅料: {', '.join(fillings)}")
make_sandwich("全麦", "火腿", "生菜", "番茄")
# 面包: 全麦
# 馅料: 火腿, 生菜, 番茄
函数可以返回多个值(其实返回的是一个元组):
def min_max(numbers):
return min(numbers), max(numbers)
lo, hi = min_max([3, 1, 7, 2, 9])
print(lo, hi) # 1 9
作用域:谁看得见谁
def my_function():
local_var = "我在函数里面"
print(local_var)
my_function()
# print(local_var) # 报错!函数外面看不见 local_var
函数内部定义的变量是局部的——函数外面看不见。函数就像一个房间,房间里的东西不会溢到走廊上。
反过来,函数可以读取外部变量,但不能轻易修改:
total = 0
def add_to_total(x):
# total = total + x # 这会报错!Python 以为你想创建一个新的局部变量 total
pass
# 正确的做法:
def add_to_total_v2(x):
global total # 告诉 Python:我要用的是外面的那个 total
total = total + x
一般来说,尽量避免用 global。函数应该通过参数接收输入,通过 return 返回输出。这样的函数是「无副作用的」——同样的输入永远产生同样的输出,不依赖也不修改外部状态。这样的函数更好理解、更好测试。
你的整个 Python 程序本身,不就是一个没有注入值的巨型函数吗?
第九章:模块与包——不重复发明轮子
你已经用过 import 了
from turtle import *
这一行干了什么?它把另一个模块(turtle)里所有的功能导入到你的程序里,让你可以直接用 forward(100) 而不是每次都写 turtle.forward(100)。
模块就是一个 .py 文件。包就是一个装有多个模块的文件夹。你写 import os,就是把 Python 自带的 os 模块(操作系统的接口)拿来用。
操作系统接口:import os
import os
# 看看当前目录里有什么
print(os.listdir("."))
# 创建一个文件夹
os.mkdir("test_folder")
# 执行一条系统命令
os.system("echo 你好,我是 Python 调用的")
# 获取环境变量
home = os.environ.get("HOME")
print(f"你的主目录是:{home}")
os 给你的程序打开了操纵操作系统的能力。你可以创建文件、删除文件、遍历目录、执行终端命令——就像你之前手动在终端里敲的,现在可以让 Python 程序自动敲。
更高级的文件操作用 shutil(shell utilities)和 pathlib(Python 3 引入的更方便的路径处理库)。现在知道有这些就行。
标准库:Python 自带的家当
Python 装好的时候,附带了一大堆模块——这叫标准库。你不需要安装任何东西就能用:
你不需要背这些——你需要知道的是:当你碰到一个需求的时候,先问自己「Python 标准库可能已经支持这个了吧?」然后查一下。大多数常见任务,标准库都能解决。
第三方库:别人写好的轮子
标准库解决不了的事情,社区帮你解决了。Python 的包索引叫 PyPI(Python Package Index,发音「派派爱」),上面有几百万个第三方包。
装一个包:
pip install requests
然后就可以用了:
import requests
response = requests.get("https://api.github.com")
print(response.status_code) # 200
print(response.json()) # 整个 JSON 数据
找什么库?怎么找?
你可能会问:PyPI 几百万个包,我怎么知道哪个适合我?
答案是:你不用掌握所有。你只需要一个地图,告诉你离你最近的包是什么。
先看标准库:Python 自带的功能已经覆盖了绝大多数基本需求
Google 你的需求 + "python":比如「python 操作 excel」→ 你会发现
openpyxl或pandas看 GitHub stars 和更新频率:被很多人用的、最近还在更新的包,大概率是靠谱的
看文档:好的包一定有好的文档。一个没有文档的包就像一家没有菜单的餐厅——你不知道能吃什么
看看是否有默认参数开箱可用:好用的包通常你只要传最少的信息就能跑起来,默认参数都是合理的。你不会每装一个新软件就翻遍它全部选项对吧?
虚拟环境:绿色的才是好文明
你装了一个包 cool_lib,然后你的另一个项目也要用 cool_lib,但是需要的是旧版本。怎么办?
虚拟环境解决的就是这个问题。每个项目有自己独立的 Python 环境和包集合,互不干扰。
# 创建一个叫 myenv 的虚拟环境
python3 -m venv myenv
# 激活它 (Linux/macOS)
source myenv/bin/activate
# 激活它 (Windows)
myenv\Scripts\activate
# 现在 pip install 只会影响这个虚拟环境
pip install requests
# 用完了退出
deactivate
虚拟环境是伟大的发明。它让你的项目各玩各的,不会互相污染。永远不要在系统 Python 里直接 pip install——给每个项目都建一个虚拟环境。
Windows 上有个经典死法:装了太多全局包,互相冲突,最后 Python 环境坏掉,重装系统。不用走到那一步——用虚拟环境从一开始就干净。
Native 库:Python 里没有 Python 代码的包
你如果去看一些包的源码(比如 numpy),你会发现里面根本没多少 Python 代码——大部分是 C 或 Fortran 写的,编译成 .so(Linux)或 .dll(Windows)文件。
这是 Python 的超能力之一:它可以调用其他语言写的代码。Python 本身可能不够快(做大规模数值运算时),但你可以用 C 写最耗时的部分,然后从 Python 调用它。这就是为什么 Python 在数据科学里那么流行——numpy 的核心是 C,pandas 也是,你享受的是 Python 的易用性 + C 的性能。
这也意味着:你在 Python 上学到的编程思维,就算以后学别的语言,也不会浪费——Python 可以和几乎所有主流语言互调。
模块的语法和约定:import 的几种写法
在学怎么拆文件之前,先把 import 本身搞明白。Python 的 import 有好几种写法,它们做的事情有细微但重要的区别。你不需要全背——看一眼,知道有这些选择,用的时候回来查。
# 1. import 整个模块——使用时带模块名前缀
import os
os.listdir(".")
os.mkdir("new_folder")
# 好处:清楚知道 listdir 来自 os,不会和别的同名函数冲突
# 2. import 模块并起别名
import numpy as np
import matplotlib.pyplot as plt
# 好处:短,约定俗成的别名让懂行的人一眼认出你在用什么库
# 3. from 模块 import 特定名字——直接使用,不带前缀
from math import sqrt, sin, cos
print(sqrt(16)) # 4.0,不用写 math.sqrt
# 好处:简洁。坏处:如果多个模块有同名函数,后 import 的会把先 import 的盖掉。
# 4. from 模块 import * ——导入所有公开的名字
from turtle import *
# ⚠️ 危险:你不知道导入了什么,可能覆盖你已有的变量。
# 除了在解释器里临时玩,永远不要在正式代码里用 * 导入。
# 5. import 包下的子模块
import urllib.request # import 了 urllib 包里的 request 子模块
urllib.request.urlopen("http://example.com")
约定:
import放在文件最顶部,标准库在前,第三方库在中间,你自己的模块在最后,每组之间空一行不要
import *import语句不写在函数里面(极少数特殊情况下可以)别名一般用社区流行的缩写(
np是 numpy,pd是 pandas,plt是 matplotlib.pyplot)——不要自己乱造
__all__:控制 from xxx import * 导出什么
如果你真的写了一个模块给别人用,你可以在模块里定义一个 __all__ 列表,控制星号导入时暴露哪些东西:
# 文件:mymodule.py
__all__ = ["public_func", "PublicClass"] # from mymodule import * 只导出这两个
def public_func():
pass
def _internal_helper(): # 下划线开头 = 约定私有,但拦不住硬要访问的人
pass
class PublicClass:
pass
没定义 __all__ 时,from mymodule import * 会导出所有不以 _ 开头的名字。但正如前面说的——别用 import *。
模块化:当你的文件越来越大
你一开始写程序,一个 my_turtle.py 就够了。但如果你写了一个项目,有几十个功能,全塞在一个文件里——几千行代码在一个文件里滚动——你会疯的。
这时候你需要模块化:把相关的函数和类拆分到不同的 .py 文件中。
my_project/
├── main.py # 入口
├── utils.py # 工具函数
├── models.py # 数据模型
└── config.py # 配置
然后在 main.py 里:
from utils import circle_area, format_date
from models import User, Post
相对导入:当你的包内部互相引用时,用 . 表示当前包,.. 表示上级包:
my_package/
├── __init__.py
├── core.py
└── utils/
├── __init__.py
├── helpers.py
└── math_tools.py
在 helpers.py 里引用同包下的 math_tools:
from . import math_tools # . 代表 helpers.py 所在的 utils/ 包
from .math_tools import gcd # 从同包的 math_tools 模块导入 gcd
在 core.py 里引用 utils 子包:
from .utils import helpers # . 代表 core.py 所在的 my_package/
from .utils.helpers import some_func
相对导入只能在包内部使用——你不能在直接运行的脚本(python my_script.py)里用相对导入。这是个常见踩坑点。
包就是文件夹:__init__.py 的完整约定
一个文件夹要被 Python 认作「包」,里面必须有个 __init__.py(Python 3.3+ 可以省略,但不要省略——显式写出来让意图明确)。
__init__.py 可以做三件事,从简单到复杂:
级别一:空文件。 绝大多数情况这就够了。
# __init__.py
# 空文件——只是告诉 Python「这个文件夹是一个包」
级别二:预先导入子模块,简化使用者的 import。
如果你的包结构是 mypackage/submodule/deep_stuff.py,使用者想用里面的功能得写 from mypackage.submodule.deep_stuff import useful_func——太长了。你可以在 __init__.py 里提前导入:
# mypackage/__init__.py
from .submodule.deep_stuff import useful_func, AnotherClass
这样使用者只需要 from mypackage import useful_func——包的结构复杂度被 __init__.py 消化了。这也是为什么你 pip install 了很多库之后,直接 import 库名 就能用——库的作者在 __init__.py 里做了聚合。
级别三:包级别的初始化代码。 比如连接数据库、读取配置文件、注册插件。不推荐——包被 import 时就会执行这些代码,可能导致意外的副作用。如果你需要初始化,提供一个显式的 init() 函数让使用者自己调用。
site-packages:打开看看,pip install 的东西到底在哪
你 pip install requests 之后,requests 的代码去哪了?就在 site-packages 目录里。
找找看:
# 查看 site-packages 路径
python3 -c "import site; print(site.getsitepackages())"
# 或者更简单地
python3 -m site
典型输出(Linux):
['/usr/lib/python3.12/site-packages']
或(用虚拟环境时):
['/home/你的名字/myenv/lib/python3.12/site-packages']
打开这个目录,看看里面有什么:
ls /home/你的名字/myenv/lib/python3.12/site-packages
你会看到两类东西:
单文件模块:requests.py、six.py——就是一个 .py 文件,直接放在 site-packages 下。你 import requests 时 Python 找到的就是这个文件。
包目录:requests/、numpy/——一整个文件夹,里面有 __init__.py 和一堆 .py 文件。numpy/ 里面还有 .so 文件(编译好的 C 代码)。
还有两个特殊后缀:
.dist-info/或.egg-info/目录:pip 安装时自动生成的元数据文件夹。里面记录了这个包的名字、版本、依赖了哪些其他包、作者、许可证。pip list就是靠遍历这些元数据目录来告诉你装了哪些包。
requests-2.31.0.dist-info/
├── INSTALLER # 谁装的(通常是 pip)
├── METADATA # 包名、版本、作者、依赖列表
├── RECORD # 这个包的所有文件清单
├── LICENSE # 许可证文本(如果有的话)
└── WHEEL # 打包格式信息
你甚至可以自己 ls 进去翻翻看。这不是黑魔法——就是一堆文件放在系统约定的路径里。理解这一点很重要:pip install 的本质是把文件下载下来放到 Python 能找到的目录里。 卸载就是删掉这些文件。没有注册表,没有神秘配置——就是文件系统操作。
建议你:现在就打开终端,找到你的 site-packages,翻一翻看几个你装过的包是怎么组织的。看到一个包就是一堆
.py文件放在一个文件夹里——你对「包」的理解就落地了。
Python 怎么找到你 import 的东西:sys.path 与导入路径
当你写 import xxx,Python 去哪里找 xxx?它按顺序在 sys.path 这个列表里的每一个目录中搜索。
看一下你的 sys.path:
import sys
for p in sys.path:
print(p)
典型输出(用虚拟环境时):
# 第一项是空字符串——代表「当前目录」
/home/你/myenv/lib/python3.12.zip # 压缩的标准库(通常不存在,只是路径在)
/home/你/myenv/lib/python3.12 # 标准库
/home/你/myenv/lib/python3.12/lib-dynload # C 扩展模块
/home/你/myenv/lib/python3.12/site-packages # pip install 的第三方库
搜索顺序就是列表顺序——从前往后,找到就停。空字符串(当前目录)在最前面意味着:当前目录下跟标准库同名的模块会覆盖标准库。 比如你在当前目录下建了个 math.py,那你 import math 导入的是你自己的 math.py,不是 Python 的数学模块。这是一个经典踩坑:给你的模块起名字时,别跟标准库重名。
sys.path 的来源(优先级从高到低):
当前脚本所在目录(或交互模式时的当前工作目录)——自动加入
PYTHONPATH环境变量——你手动设置的,多个路径用:分隔(Windows 用;)export PYTHONPATH=/home/你/my_libs:/home/你/another_lib python3 my_script.py.pth文件(路径配置文件)——放在 site-packages 下的特殊文件,每行一个路径。Python 启动时自动读取,把里面的路径加到sys.path。这是虚拟环境和一些大型框架用来注入路径的机制。你可以打开一个看看:cat /home/你/myenv/lib/python3.12/site-packages/easy-install.pth # 或者 cat /home/你/myenv/lib/python3.12/site-packages/distutils-precedence.pth标准库路径和 site-packages——Python 安装时写死的
你可能需要手动加路径的场景:
import sys
sys.path.insert(0, "/path/to/my/custom/libraries")
import my_custom_module # 现在能找到上面路径里的东西了
这是在脚本里临时修改搜索路径的写法。如果你有长期需求,应该用 PYTHONPATH 环境变量或 .pth 文件,而不是在每个脚本里 sys.path.insert。
文件和模块的命名规矩
文件名就是模块名。 utils.py → import utils。文件名必须是合法的 Python 标识符(不能以数字开头、不能含连字符 -、不能是 Python 关键字)。
utils.py ✅ import utils
my_utils.py ✅ import my_utils
2utils.py ❌ 数字开头,import 语法报错
my-utils.py ❌ 含连字符,但可以用 importlib 强制导入(不推荐,改名字就好)
约定俗成:
模块名全小写,用下划线分隔(
my_module.py,不是MyModule.py或my-module.py)包名也是全小写,不加下划线(
mypackage/,不过my_package/也可以——一致性比规则重要)类名用大写驼峰(
MyClass),函数和变量用小写蛇形(my_function,my_variable)
第十章:面向对象——组织复杂系统
命令式编程够用吗?
到目前为止,你写代码的方式是命令式的:列出要做的事,按顺序执行。这就像你给自己写的操作手册:
1. 打开文件
2. 读取数据
3. 计算平均值
4. 打印结果
5. 关闭文件
这种方式在处理简单任务时完全没问题。但想象你在做一个游戏:有玩家、敌人、道具、地图、AI 行为……如果你用一堆函数和全局变量去管理所有这些东西,很快你的代码结构会变成意大利面条——每个人都在操作同一堆全局数据,你改一行代码,不知道哪里会坏。
现实中的大工程怎么组织?
假设你在造一栋楼。你不会把所有工人叫到一起发一本操作手册然后让他们自己去——你会:
定义角色:电工、水管工、砌砖工、设计师
定义接口:电工知道哪面墙里埋了电线,水管工知道管道走向
各司其职:电工不需要知道管道怎么走的,水管工不用懂电路
面向对象编程(OOP,Object-Oriented Programming)就是把这种组织方式带进代码里:定义好每个对象的职责,然后让它们通过接口交互,而不是所有人直接操作同一堆数据。
一个游戏例子
用命令式写法管理一个角色:
player_name = "勇者"
player_hp = 100
player_mp = 50
player_items = ["药水", "木剑"]
def player_attack(target_hp, damage):
return target_hp - damage
def player_heal(player_hp, amount):
return player_hp + amount
# 但如果有两个角色呢?
p1_name = "勇者"
p1_hp = 100
p2_name = "法师"
p2_hp = 60
# 变量名开始爆炸……
用面向对象:
class Player:
def __init__(self, name, hp, mp):
self.name = name
self.hp = hp
self.mp = mp
self.items = []
def attack(self, other, damage):
print(f"{self.name} 攻击 {other.name},造成 {damage} 点伤害!")
other.hp -= damage
def heal(self, amount):
self.hp += amount
print(f"{self.name} 恢复 {amount} HP,当前 {self.hp}")
def add_item(self, item):
self.items.append(item)
print(f"{self.name} 获得了 {item}!")
# 使用
hero = Player("勇者", 100, 50)
mage = Player("法师", 60, 150)
hero.add_item("长剑")
hero.attack(mage, 15)
mage.heal(10)
类和对象
类(class):模板。
Player是一个类——定义了「玩家」这个概念的共同特征(有名字、有 HP、能攻击、能治疗)对象(object):根据模板造出来的实例。
hero和mage是两个不同的 Player 对象——名字不同、HP 不同,但它们有相同的结构
类比:类 = 建筑图纸。对象 = 根据图纸造出来的实际建筑。同一张图纸可以造很多栋楼,每栋楼有自己的位置、颜色、住户。
self 是什么意思
def attack(self, other, damage):
self.hp # 这个对象的 HP
other.hp # 那个对象的 HP
self 代表「当前这个对象自己」。当你写 hero.attack(mage, 15) 时,self 就是 hero,other 是 mage。
面向对象不是替代命令式——是补充
你不需要在所有地方都搞面向对象。很多任务——特别是短小的脚本、数据处理、一次性分析——命令式足够了。
面向对象的意义在于:当系统变复杂、多人协作时,它能帮你把复杂度装进盒子里。每个盒子管理好自己的数据,通过明确的接口跟别的盒子对话。你不用知道盒子里面怎么工作的——这跟你在第一课学的网络分层是一模一样的道理。
对模块和库的重新理解
回过头看——import os、import turtle——这些本质上也是面向对象。os 是一个模块(盒子),它封装了操作系统的细节,给你提供 listdir、mkdir 这些接口。你不知道也不需要知道 listdir 内部怎么遍历的文件系统——它替你封装好了。
你自己写的类和包,跟 Python 标准库在三观上是一致的:每个模块/类管好自己的一亩三分地,对外提供干净的接口。
第十一章:你在哪里跑——Python 与计算机底层
Python 不是一个独立王国
你写的 Python 程序,在哪跑?在 Python 解释器里。
Python 解释器是一个程序(大部分用 C 写的,叫 CPython)。它读你的 .py 文件,理解你的意图,然后替你跟操作系统说话。你从来没直接跟操作系统说过话——你通过 Python 解释器这个中间人。
你的 Python 代码
↓
Python 解释器(CPython)
↓
操作系统(Linux / Windows / macOS)
↓
硬件(CPU、内存、硬盘、外设)
这是好事:你不需要知道操作系统怎么管理内存,Python 帮你做了。这也是限制:Python 只能做它被设计来做的事——如果 Python 没有封装某个操作系统的功能,你就做不了。
比如,你想直接控制一个 USB 外设。如果 PyPI 上没有对应的库,纯 Python 很难做到。你需要一个能直接跟硬件对话的语言。
Python 的对外接口
对外(跟世界):PyPI +
import。你通过标准库或第三方库来获得能力。对内(算东西):变量、函数、类、if、while、for。你用这些 Python 语言本身的构件来组织逻辑。
你在 Python 里写的所有东西,都逃不出这个圈:解释器给你提供了什么接口,你就能做什么;没提供的,你就做不了。
Native 库的魔法
为什么 numpy 能在 Python 里跑这么快?因为它大部分代码不是 Python——是 C 写的。Python 只负责调用 C 写好的函数。C 代码直接操纵内存、用 CPU 的向量指令做批量计算——这些是 Python 解释器本身做不到的。
所以当你用 numpy 的时候,你实际上在:
Python 代码 → numpy Python 壳 → numpy C 内核 → 操作系统 → CPU
其他语言写的库(C、C++、Rust、Fortran)都可以用类似的方式接入 Python。这就是为什么 Python 在科学计算和 AI 领域占据统治地位——Python 提供易用的接口,底层用最快的语言计算。
第十二章:其他语言一瞥——Python 不是唯一的选择
学完 Python 的基础,你会发现大部分编程语言跟你已经理解的东西在结构上非常相似。变量、类型、控制流、函数、模块——这些概念几乎在所有语言里都有,只是写法不同、侧重点不同。
我们用很小的篇幅看一眼几个语言,不是要你现在学会它们,而是让你知道:你学的不是 Python,你学的是编程。换一门语言,核心思想都在。
Bash:你已经会了
for i in {1..5}; do
echo "第 $i 次"
done
if [ -f "config.txt" ]; then
echo "配置文件存在"
else
echo "没有配置文件"
fi
Bash 是第一课里你已经见过的终端脚本语言。它的目的很窄:操纵文件、执行命令、把几个命令行工具串在一起。它不是通用编程语言——你不会拿 Bash 写一个网站。但在它的领域内,它是最好的工具。
一句话:当你想自动化终端命令时,用 Bash。其他时候,用 Python。
C:掀开引擎盖
#include <stdio.h>
int main() {
int x = 5;
int* ptr = &x; // ptr 指向 x 的内存地址
*ptr = 10; // 通过指针修改 x 的值
printf("x = %d\n", x); // x = 10
return 0;
}
C 是最接近硬件的「高级」语言。你不再有 Python 解释器帮你管理一切——你直接操作内存。
在 C 里,变量就是内存中的一格纸。你可以拿到这格纸的地址(&x),可以用这个地址间接修改它里面的值(*ptr)。你甚至可以拿到一个数组之后直接访问它后面的内存——C 不会拦你,你读到的可能是别的变量的值,可能是垃圾,也可能直接崩掉。
这就是 C 的哲学:相信程序员知道自己在干什么,不给额外的护栏。 给你骚操作的自由,也给你搞崩一切的权力。
Python 帮你管内存(自动垃圾回收)、检查数组边界(索引越界直接报错)、保证字符串是合法的。C 几乎什么都不管——你的程序崩了,是你写错了,不是语言的错。
冯诺依曼架构:现代计算机的基本结构——内存存数据和指令,CPU 从内存取指令执行。C 是对这个结构的直接映射。你在 C 里做的所有事,本质上就是:
在内存里读写数据
对数据做运算
根据条件跳转到不同的指令
Python 帮你隐藏了这些细节。C 让你直接看到它们。理解了 C,你就理解了计算机怎么跑。
UB(Undefined Behavior,未定义行为):C 标准说「如果你做了某件事,C 不规定会发生什么——可能正常、可能崩溃、可能安全漏洞」。这是 C 里最危险也最迷人的东西。它给了编译器极大的优化空间,但前提是你必须遵守规则。比如数组越界——标准不管,不同系统可能表现不同。你写的代码在一个机器上是好的,换个机器就可能炸。Python 没有 UB——同样的代码在同样的 Python 版本下行为是确定的。护栏的存在是有道理的。
C++:C 加上了高级抽象——以及找库的另一种活法
C++ 在 C 的基础上引入了面向对象、模板、异常处理等等高级抽象。但它的核心理念依旧:你不需要抽象的时候不用为它付出开销(zero-overhead principle)。如果你只用 C++ 写 C 风格代码——它就是 C。如果你想用高级抽象(类、智能指针、泛型),它们会被编译器「编译掉」,变成跟手写 C 几乎一样快的机器码。
C++ 也是 Python 很多包的底层语言——当你 pip install 一个包时,pip 可能正在你电脑上编译 C++ 代码。
回到 C 的底层视角:为什么 C/C++ 能看见更多东西
Python 浮在解释器这个「海面」上。海面以下是什么?回过头看冯诺依曼架构:内存就是一大排编了号的格子,CPU 从格子里取指令、取数据、算完放回去。C 语言就是对着这排格子直接说话。
在 C 里,没有什么「变量名」——变量名只是一个给人看的标签,编译器把它翻译成内存地址。也没有什么「列表」——列表是你自己用指针和 malloc 在内存里拼出来的。当你写 int arr[10],编译器在内存里连续划出 40 个字节(10 个 int,每个 4 字节),arr 就是这 40 个字节起点的地址。
Python 让你觉得内存是无限的、自动管理的。C 告诉你内存是有限的一排格子,你可以精确控制每一格放什么、什么时候回收。这就是为什么高性能场景必须用 C/C++:你离硬件的距离越近,你就能榨出越多的性能。Python 的易用性是用一层层的抽象换来的,而每一层抽象都有性能开销。
你不需要现在就能写 C——但你知道了海面下面是什么,你就理解了 Python 的边界在哪里。
C++ 的面向对象:更精细的控制
你前面学过 Python 的面向对象。C++ 的 OOP 底层思想相同,但它给你更多控制权——当然也意味着你需要更小心。
访问控制:public、private、protected
Python 里没有真正的「私有」——你在变量名前面加个下划线 _secret,意思是「别碰」,但别人硬要碰你拦不住。C++ 不一样——它是编译期强制执行:
class Player {
public: // 谁都能访问
string name;
void attack(Player& other, int damage);
private: // 只有这个类自己的方法能访问
int hp;
int mp;
protected: // 这个类及其子类能访问
int level;
};
public:外部代码可以直接p.name、p.attack(...)private:外部代码想访问p.hp→ 编译器直接报错,编译不过。数据被锁在类里面,只有类自己的方法能碰protected:跟 private 差不多,但子类(继承者)也能访问
为什么需要这个?回到第十章讲的「大工程组织」:一个类就像一个黑盒子,对外只暴露能用的接口(public),把内部实现细节锁起来(private)。外部代码只依赖接口,不依赖实现——这样你修改内部实现时,外部代码不需要改。没有访问控制,封装就只能靠自觉。有了访问控制,封装是编译期强制执行的。
重载运算符:让自定义类型像内置类型一样自然
Python 也可以用 __add__、__lt__ 这些魔术方法重载运算符。C++ 做得更直接:
class Vector3 {
public:
float x, y, z;
Vector3(float x, float y, float z) : x(x), y(y), z(z) {}
// 重载 +:两个向量相加
Vector3 operator+(const Vector3& other) const {
return Vector3(x + other.x, y + other.y, z + other.z);
}
// 重载 *:向量乘以标量
Vector3 operator*(float scalar) const {
return Vector3(x * scalar, y * scalar, z * scalar);
}
// 重载 ==:判断两个向量是否相等
bool operator==(const Vector3& other) const {
return x == other.x && y == other.y && z == other.z;
}
};
// 使用——跟内置类型一样自然
Vector3 a(1, 2, 3);
Vector3 b(4, 5, 6);
Vector3 c = a + b; // operator+ 被调用
Vector3 d = a * 2.0f; // operator* 被调用
if (a == b) { ... } // operator== 被调用
这有什么好处?你的自定义类型(向量、矩阵、复数、字符串)可以使用 +、-、*、==、[] 这些你从小在数学课上用的符号,而不需要调用 a.add(b) 或 a.multiply(b)。这让数学密集型代码(游戏物理、图形渲染、科学计算)读起来就像数学公式。代价是:你如果滥用了——比如把 + 重载成减法——你的同事会想杀你。权力越大,责任越大。
继承:复用接口和实现
Python 的继承你已经见过了(class Dog(Animal):)。C++ 的继承支持多重继承(一个子类可以同时继承多个父类),但更关键的是它区分三种继承方式:
class Animal {
public:
void breathe() { ... }
protected:
int age;
};
class Dog : public Animal { // public 继承:Animal 的 public 在 Dog 里还是 public
public:
void bark() { ... }
};
class Cat : public Animal {
public:
void meow() { ... }
};
Dog 继承了 Animal 的所有接口和行为——breathe() 可以直接用,age 可以通过自己的方法访问。继承让你「定义一个通用概念(动物),然后让具体类型(狗、猫)自动拥有通用概念的全部能力,只写自己特殊的部分」。
C++ 的深刻之处在于:所有这些东西——访问控制、重载运算符、虚函数、继承——最终编译完之后,产生的机器码跟你手写 C 几乎一样快。 你在编译期享受了高层的抽象便利,但运行时不为抽象付出额外开销。这就是 zero-overhead principle 的含义:你为使用的抽象付费,不为你没用的抽象付费。如果一段代码不需要虚函数,它就没有虚函数调用的开销。Python 永远在为你付这些开销——不管你的类有没有子类,所有的属性查找都要走一遍动态分发的流程。这就是为什么 Python 的「默认便利」和 C++ 的「默认高性能」是两条不同路。
找库、装库:没有「应用商店」的世界
这是 C++ 和 Python 最大的体验差异之一。Python 有 PyPI + pip,几百万个包,pip install 一键搞定,虚拟环境隔离开——你可以叫它「统一应用商店」。
C++ 没有这种东西。或者说,有过很多尝试,但没有一个成为统一标准。你在 C++ 里找一个库、装一个库的过程大致是这样:
第一步:明确需求。 跟 Python 一样——先搞清楚你要做什么,然后搜「C++ library for [你的需求]」。比如「C++ library for JSON parsing」「C++ library for HTTP requests」。
第二步:找到库。 发现有好几个选择——nlohmann/json、RapidJSON、simdjson、Boost.JSON……哪个好?看 GitHub stars、看文档质量、看最近有没有更新、看有没有人还在维护。不像 Python 里 pip 告诉你下载量——你得自己去判断。
第三步:决定怎么装。 这里就有好几种路:
header-only 库(比如
nlohmann/json):最好的情况——只有一个.hpp头文件,下载下来放到项目文件夹里,#include就能用。没有编译步骤,没有依赖,跟拖一个 Python 脚本进项目一样简单。系统包管理器:Linux 上
sudo apt install libcurl-dev、macOS 上brew install boost。系统帮你编译好,放在系统路径里,你的项目链接就能用。好处是方便,坏处是不同系统命令不一样,而且系统仓库里的版本可能很旧。vcpkg / Conan:C++ 社区试图建立的「应用商店」——跨平台的包管理器。
vcpkg install nlohmann-json,然后 CMake 里配置一下。比系统包管理器先进,但远没有 pip 成熟——很多包没有,或者版本不对,或者编译选项不兼容。自己编译:下载源码,
cmake .. && make -j8 && sudo make install。最原始的做法,也最灵活。你能精确控制编译选项、优化级别、安装路径。但你要处理依赖——库 A 依赖库 B 的 2.1 版本,但你的系统上只有 1.8……
第四步:集成到你的项目。 Python 里 import requests 就完了。C++ 里你需要:
告诉编译器头文件在哪(
-I/path/to/include)告诉链接器库文件在哪(
-L/path/to/lib)告诉链接器链接哪个库(
-lcurl)如果库是动态链接的(
.so/.dll),运行时还要确保库在系统能找到的路径里
现代 CMake 可以自动化大部分这些步骤,但学习 CMake 本身又是一条学习曲线。
为什么 C++ 不像 Python 那样有个统一的 pip? 因为 C++ 的编译模型本质上跟 Python 不一样。Python 代码是解释执行的——你下载的 .py 文件可以原样在任何平台上跑。C++ 代码是编译执行的——需要针对目标平台(Windows/Linux/macOS)、CPU 架构(x86/ARM)、编译器(GCC/Clang/MSVC)、优化级别分别编译。一个预编译的 .so 在 Linux x86_64 上能用,到了 ARM Mac 上就废了。统一的「应用商店」需要为每一种组合预编译一份——工程量巨大。这就是为什么 vcpkg 和 Conan 大部分时候在你机器上现场编译而不是下载预编译包——它要适配你的具体环境。
但这不意味着 C++ 比 Python 差——它只是意味着:Python 选择了「方便」作为默认,C++ 选择了「高性能和精确控制」作为默认。 你付出更多配置的心力,换来的是:你确切地知道你的程序依赖了什么版本的什么库、用哪个编译器哪个优化级别编译的、最终二进制里包含了什么。在需要极致性能和控制权的场景下,这个代价是值得的。
对初学者来说,这个对比最大的意义是:你享受了 pip 的便利,但要意识到这是 Python 生态花了十几年构建的工程成就——不是所有语言都有的。 如果有一天你需要在 C++ 里用别人写的库,别被吓到——只是多了几个步骤,核心逻辑没变:找到东西、下载、配置、用。
JavaScript:所有需要界面的地方
// 浏览器里的 JavaScript
document.getElementById("btn").addEventListener("click", () => {
alert("你点了我!");
});
// 服务器端的 JavaScript (Node.js)
const fs = require("fs");
fs.readFile("data.txt", "utf8", (err, data) => {
if (err) throw err;
console.log(data);
});
JavaScript 是浏览器里唯一能用的编程语言(WebAssembly 越来越成熟,但 JS 仍然是入口)。如果你想让网页有互动——点按钮弹窗、拖拽元素、实时更新内容——你必须用到 JavaScript。
Node.js 让 JS 在服务器端也能跑。今天的前后端可能都是 JavaScript——这叫全栈。对只想写脚本的人来说 JS 不是必须的,但如果你想做网站,它是绕不开的。
Java:庞大的工程生态
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Java 是「企业级」的代名词。它要求所有东西都放在类里面、类型必须显式声明、编译检查非常严格。写一个 Hello World 都要五层结构——看起来多余,但在几百万行代码的大项目里,这种强制纪律是有价值的:它让搭砖块的人不容易砸坏别人的砖块。
Java 运行在 JVM(Java 虚拟机)上——跟 Python 解释器一样的思路:写的代码不是直接给操作系统跑,是给一个中间平台跑。好处是写一次,到处跑。坏处是启动慢、吃内存。
Haskell:另一条路
-- 定义一个无限列表(对,无限)
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- 取前 20 个斐波那契数
take 20 fibs -- [0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181]
Haskell 跟你目前学的 Python 几乎完全相反:
Python 是命令式——你告诉计算机做什么
Haskell 是声明式——你告诉计算机是什么
Python 变量随时可以改——Haskell 变量一旦定义就不能改
Python 按顺序执行——Haskell 不关心顺序,只关心依赖关系
Haskell 先生很严苛(你前面听过他的名号),但他教会你一种全新的思维方式:把计算当作数学函数的组合,而不是修改内存的步骤。
Haskell 的背后有两座数学靠山:类型论(Type Theory)和范畴论(Category Theory)。
类型论听起来很学术,但直觉很简单:每一个东西都有一个类型,类型定义了你能拿它做什么。你在 Python 里已经见过——int 能加减乘除,str 能拼接切片。类型论把这个直觉推到极致:不仅数据有类型,函数本身也是类型——一个「从 A 到 B 的函数」本身就是一个类型(写成 A -> B)。甚至类型的类型(我们叫它 kind)也有自己的规律。当你写 Haskell 代码时,类型系统在编译期就能证明你的程序不会出错——比如,它保证你永远不会把 Int 和 String 加在一起。这不是运行时检查(像 Python 的 TypeError),是编译时就发现了。你的程序还没跑,类型系统已经帮你排除了大量可能的错误。
范畴论更抽象,但核心思想你能懂:不看东西本身,看东西之间的关系。在编程里,「东西」是类型(Int、String、List),「关系」是函数(Int -> String 做数字转文本,String -> Int 做解析)。范畴论研究的就是这些关系和它们的组合方式。比如,map 这个操作——你给一个函数 a -> b 和一个 List a,得到 List b。map 在范畴论里叫「函子(Functor)」——一种保持结构的映射。你不需要学完范畴论才能编程,但当你在 Python 里用 map、在 Promise 里用 .then()、在 Rust 里用 Option::map——你已经在用范畴论的概念了,只是这些概念被编程语言翻译成了你能直接用的语法。
这些概念影响越来越大:Rust 的 trait 系统从 Haskell 的 typeclass 借鉴(而 typeclass 受范畴论启发)、TypeScript 的类型体操在往类型论的方向走、Swift 的协议和扩展、Kotlin 的扩展函数。函数式编程的基因正在渗透到几乎所有主流语言里。
现在不用学 Haskell,更不用学范畴论。但知道它们的存在、知道「编程语言的设计不是随意为之——它们背后有严肃的数学在指导」——这本身就是重要的认知。你学的不是某个语言,你学的是人类表达计算的各种方式。
第十三章:三种复杂度——理解你遇到的困难
写了这么多代码,也碰了不少壁。让我们退一步,总结一下你碰到的困难属于哪一类。这会帮助你更好地理解你正在学的东西。
编程中的困难可以分为三种。这个框架会伴随你整个编程生涯。
第一种:本质复杂度
你解决问题的本质难度。不管你用什么语言、什么工具,这个复杂度都在。
你要设计一个推荐算法。算法本身有多难——这就是本质复杂度。跟语言无关。
你想做一个游戏里 AI 的寻路逻辑。A* 算法本身的理解和设计——本质复杂度。跟语言无关。
你能减少本质复杂度吗? 通常不能——除非你简化问题本身。比如「我不做全图寻路了,只做房间内寻路」。但如果你要解决的是领域核心问题,你就得直面它。
第二种:语言设计复杂度
你选择的编程语言给你多少表达能力,以及它的设计哲学给你带来多少便利(或麻烦)。
Python 的 for i in range(5) 比 C 的 for (int i = 0; i < 5; i++) 好读——语言设计的便利。
Python 的 GIL(全局解释器锁)让多线程跑不快——语言设计的局限。
Haskell 的强类型系统能在编译期就帮你发现大量错误——语言设计的力量。
JavaScript 的 == 坑([] == ![] 是 true)——语言设计的屎山。
你能改变吗? 选了语言之后通常不能——但你可以选另一门语言。这就是为什么有那么多语言存在:它们在本质复杂度面前做了不同的取舍。
第三种:运行时/平台复杂度
你的程序跑在什么上面。Python 解释器、JVM、浏览器、操作系统——这些都给你的程序设定了边界和接口。
Python 解释器帮你管理内存——你不需要 malloc/free。
但 Python 解释器也挡住你直接操作 USB 设备——你需要 native 库。
浏览器只给你 JS 和有限的 API——你不能在浏览器里直接读写本地文件。
操作系统的文件权限可能阻止你的程序访问某个目录。
你能改变吗? 可以——换平台、用 native 库、换运行环境。但每一个平台都有它的代价。
三种复杂度怎么指导你学习
当你遇到困难时,先判断它属于哪一种:
「我不懂怎么设计这个算法」→ 本质复杂度。你需要的是算法知识,不是换语言。
「Python 为什么不能直接做这件事?」→ 语言/平台复杂度。查官方文档、找库、或者考虑换工具。
「为什么同样的代码在我的电脑上跑的跟服务器上不一样?」→ 平台复杂度。检查环境差异、版本差异、系统差异。
「为什么 Python 的
=是赋值不是等于?」→ 偶然复杂度(语言设计复杂度的一个子类)。认了,习惯,继续往前走。
理解这三种复杂度能帮你免于一种常见的挫败感:把偶然复杂度当成了本质复杂度。你学不会某个语言怪异的语法不等于你学不会编程。把两者分开,你就能清醒地知道:我需要克服的是语言本身的怪癖,而不是我能力不够。
复杂度的数学根源:类型论、范畴论与控制论
退远一步看——你刚才读完了变量、类型、控制流、函数、面向对象、模块化。这些东西为什么会被设计成这样?为什么全世界的编程语言在设计上最终都殊途同归?背后有三股数学力量。
类型论告诉你:怎么保证程序不胡说
你在第四章学过,类型定义了「什么东西能做什么事」。类型论把这个直觉变成了严肃的数学:每一个值都有一个类型,类型之间的关系由函数定义,函数可以组合,组合必须满足某些定律。
这听起来很抽象,但你在 Python 里每时每刻都在用它的成果:"hello" + 5 报 TypeError——因为类型论告诉我们,+ 的类型是 Int -> Int -> Int 或 String -> String -> String,没有 String -> Int -> ??? 这条路。Python 的类型检查是运行时的;Haskell 的类型检查是编译期的——但从数学上看,它们在描述同一件事:程序是一组类型的组合,组合必须是合法的。
类型论的意义不仅在「防止出错」。它还告诉你:类型就是规范。你写一个函数 def sort(lst: List[Int]) -> List[Int],类型签名本身就是一份微型文档——不用读实现,你知道输入是什么、输出是什么。在大项目里,类型系统是团队协作的合约——它强制每个人遵守接口约定,否则编译不过。你在面向对象那章学的「封装」和「接口」,其数学本质就是类型论。
范畴论告诉你:抽象是怎么被「抽」出来的
范畴论的核心只有三个东西:对象、箭头、组合。对象是类型(Int, String, List a),箭头是函数(Int → String),组合是把两个箭头接成一条新路(f: A→B 和 g: B→C 组合成 g∘f: A→C)。
为什么这个框架厉害?因为它不看东西是什么,只看东西怎么连接。List、Optional、Future、IO——它们在范畴论的视角下是同一个模式的不同实例:都是函子(Functor),都支持 map 操作——给你一个容器和一个函数,把函数应用到容器里的值,返回一个同类型的容器。List.map(f) 把 [a, b, c] 变成 [f(a), f(b), f(c)]。Optional.map(f) 把 Some(x) 变成 Some(f(x)),把 None 保持为 None。Future.map(f) 把「将来会得到的值」变成「将来会得到的转换后的值」。不同概念,同一个数学结构。
范畴论给编程语言设计提供了一套「抽象应该长什么样」的标尺。当你设计一个新类型时,你应该问:它是不是函子?它支持 map 吗?它的 map 满足函子定律吗?如果满足,任何懂范畴论的程序员看到你的类型,不需要读文档就知道它大致怎么用——因为他们认识这个数学模式。这就是 Rust 的 Iterator trait、Haskell 的 Functor typeclass、JavaScript 的 Promise.then()、Python 的 map() 的共通设计灵感。
控制论告诉你:程序和外部世界怎么对话
你前面学的所有东西——变量、控制流、函数——都假设程序是一个自足的计算过程:输入进去,输出出来。但真正的程序不是这样的。真正的程序要跟外部世界持续交互:等用户按键(你在 Turtle 里用 input() 做的)、收网络包(第一课的 TCP)、响应中断(Ctrl+C 终止程序)。
控制论研究的正是这个问题:系统怎么通过「感知→决策→执行→再感知」的循环来和世界互动。这个循环在编程里无处不在:
游戏主循环:每帧读取输入→更新状态→渲染画面→等待下一帧
服务器:接收请求→处理→返回响应→等待下一个请求
操作系统:轮询设备→调度进程→处理中断→再轮询
你在写 while True: cmd = input("> "); execute(cmd) 的时候,就在实现一个最简单的控制论循环。你写的每一个事件监听器(button.on_click(handler))本质上是在声明:当外界发生某个事件时,执行这段代码。这就是控制论在编程中的落地——事件驱动编程。
控制论还回答了一个更深层的问题:错误的本质是什么? 在三类复杂度里,很多错误不是程序「算错了」——是程序关于外部世界的假设错了。你假设文件存在,但用户删了(FileNotFoundError)。你假设网络通畅,但网线被猫踢了(ConnectionError)。控制论的视角告诉你:程序不能只做最理想的假设——它必须持续感知外部状态、验证假设、出错时有预案。这就是 try/except 和错误处理的设计动机。
三股力量汇聚:类型论管程序内部的一致性——你的逻辑自洽吗?范畴论管抽象的通用性——你的设计模式能跨语言复用吗?控制论管程序和世界的接口——你的假设和现实对得上吗?
你不用现在就去啃这三门数学。但每一次你在 Python 里碰到类型错误、每一次你写出一个可以复用的函数、每一次你处理了 try/except——你都在和这三股力量打交道。它们不是书本上的死知识,是你每一次编程都在无意识中使用的思维工具。认识它们、理解它们为什么存在——这会让你从「会用 Python」变成「理解计算这件事本身」。
第十四章:课后测试
回到第一课你 ping 过的那些命令:写一个
.sh或.bat脚本,让计算机依次执行三个你学过的网络诊断命令,并输出结果到文件中。这跟你手动敲有什么区别?用 Turtle 写一个程序,画出一个等边三角形。需要输入边长,海龟根据输入画相应大小的三角形。(提示:内角 60°,外角 120°。
right转的是外角。)接上题,把画三角形的代码封装成一个函数
draw_triangle(side_length)。然后用一个循环,画五个大小递增的三角形。你会体会到函数复用的价值。Python 里
input()返回的是什么类型?为什么?如果你想让用户输入一个年份然后算出距今多久,你需要做什么转换?写出代码。设计一个猜数字游戏:程序随机生成 1~100 之间的一个数,用户猜,程序提示「大了」或「小了」,直到猜对为止。统计猜了多少次。你需要用到哪些控制流结构?(
random.randint可以生成随机整数)重新阐释为什么
=是「赋值」而不是「等于」。解释x = x + 1在程序里的含义。如果 Python 使用:=表示赋值,=表示判断相等,会有什么不同?(试着说说语言设计中的偶然复杂度)面向对象和命令式编程的根本区别是什么?在什么情况下你会选择用面向对象来组织代码?试着用第一课学到的网络分层概念来类比——分层设计的好处和面向对象封装的好处有什么相似之处?
你有一个班的成绩单,存储为一个字典列表:
[{"姓名": "张三", "成绩": 85}, ...]。写出 Python 代码:计算平均分、找出最高分和最低分、列出不及格的名单。解释你每一步用了什么容器类型、为什么选它。解释虚拟环境的作用。为什么不应该在系统全局 Python 里直接
pip install?作为类比:为什么你不会把所有游戏都装在 C 盘 Windows 目录下?开放题:第一课教你「从底层开始排查网络故障」。类似地,如果你写了一个程序但没按预期工作,你会从哪里开始查?试着用三种复杂度的框架分析:这个问题是本质复杂度(算法设计错了)?还是偶然复杂度(语法不对、类型不匹配)?还是平台复杂度(环境变量没配、依赖没装)?
第十五章:扩展提高
以下内容供感兴趣的同学继续深入。每个小专题几分钟到几十分钟就能建立基本概念。
报错应该怎么看
Python 报错是帮你,不是骂你。学会读报错是编程最重要的技能之一。
Traceback (most recent call last):
File "test.py", line 3, in <module>
print(x / y)
NameError: name 'x' is not defined
最下面一行是错误类型和描述:
NameError,x没定义往上看是调用链:
test.py的第三行出的事出错的位置不一定就是错误的原因——有时候你把一个错误的值传了三层函数才炸
常见错误速查:
核心原则:不要猜,读报错。Python 非常诚实——它告诉你哪一行、什么问题。你读懂了就解决了一半。
range 究竟是什么
range(5) 不是列表——它是一个「承诺」:当你遍历它的时候,它会按需生成 0, 1, 2, 3, 4。如果你写 range(1000000000),它不会真的创建十亿个数字——它只记住「从 0 开始,每次加 1,到十亿为止」。
这叫惰性求值:不提前算,用到的时候再算。这让 Python 能处理理论上无限大的序列而不撑爆内存。
enumerate, zip, lambda——语法糖里的设计智慧
Python 有不少让你写起代码来行云流水的「小」功能。它们不是必需的——你总可以用更啰嗦的方式做到一样的事。但它们让代码读起来像你脑子里的意图,而不是机器执行的步骤。我们管这种不增加新能力、但让表达更自然的语法叫语法糖。
enumerate:遍历的时候自动带上序号
fruits = ["苹果", "香蕉", "橘子"]
# 不用 enumerate(啰嗦版)
i = 0
for fruit in fruits:
print(f"{i}: {fruit}")
i += 1
# 用 enumerate(清爽版)
for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")
# 输出:
# 0: 苹果
# 1: 香蕉
# 2: 橘子
你每次需要「第几个」的时候都要手动维护一个计数器——enumerate 把这个模式封装成了一个函数。它返回的每一个元素是一个 (序号, 值) 的元组,你直接拆包用。
zip:把多个序列拉链一样拉在一起
names = ["小明", "小红", "小刚"]
scores = [85, 92, 78]
# 不用 zip(啰嗦版)
for i in range(len(names)):
print(f"{names[i]} 考了 {scores[i]} 分")
# 用 zip(清爽版)
for name, score in zip(names, scores):
print(f"{name} 考了 {score} 分")
zip 把两个(或多个)序列按位置配对,像拉链一样咬合在一起。长度不一样时,以最短的那个为准停下来。这在数据对应处理时极其常用——你有一个名字列表和一个成绩列表,它们是对应的,但你不想用索引去访问。
# zip 也可以反过来——把配对好的拆回两个列表
pairs = [("小明", 85), ("小红", 92), ("小刚", 78)]
names, scores = zip(*pairs)
# names = ("小明", "小红", "小刚")
# scores = (85, 92, 78)
lambda:一句写完的函数
有些函数你只需要用一次,不想给它起名字占地方:
# 不用 lambda
def square(x):
return x**2
numbers = [1, 2, 3, 4, 5]
squared = map(square, numbers)
# 用 lambda
squared = map(lambda x: x**2, numbers)
lambda 就是「一句话函数」——lambda 参数: 返回值。它特别适合作为 map、filter、sorted 的回调:
students = [
{"name": "小明", "score": 85},
{"name": "小红", "score": 92},
{"name": "小刚", "score": 78},
]
# 按成绩排序
ranked = sorted(students, key=lambda s: s["score"], reverse=True)
# 筛出及格的
passed = list(filter(lambda s: s["score"] >= 60, students))
lambda 的哲学是:如果一个函数简单到一句话就能说清楚,你就不应该给它起名字、定义、缩进——直接写在使用它的地方,读代码的人一眼就懂了。
这些语法糖为什么好:它们让代码更短,但不是为了省打字——是为了减少你需要同时记住的东西。你看 for i, fruit in enumerate(fruits),脑子里只需要想「我要遍历,同时知道序号」。你看 for name, score in zip(names, scores),脑子里只需要想「我要把这两列配对」。你不用分出注意力去管理计数器和索引变量——那些是偶然复杂度,语法糖帮你消掉了。
一个判断语法糖好坏的标准:好的语法糖消除模式——把反复出现的代码套路变成一个词,让你从「怎么写」的细节中抽身,专注于「想干什么」。坏的语法糖增加模式——让你需要记住一个额外的规则,而它带来的便利不值这个记忆成本。Python 的
enumerate、zip、lambda都是前一种。
函数的更多形态
默认参数:
def greet(name, greeting="你好"):
print(f"{greeting},{name}!")
greet("小明") # 你好,小明!
greet("小明", "早上好") # 早上好,小明!
关键字参数:
def create_user(name, age, email=None, phone=None):
pass
create_user("小明", 18, email="xiaoming@example.com")
# 不需要按位置传,可以用名字指定,顺序无所谓
包的高级用法
__init__.py:放在文件夹里,把这个文件夹标记为 Python 包。可以为空,也可以写一些初始化代码或控制 from package import * 导出什么。
导入路径:当你写 import my_package,Python 去哪里找?
当前目录
PYTHONPATH环境变量里的目录标准库目录
pip install安装的第三方库目录(site-packages)
这就是为什么你 pip install 之后就能直接 import——包被放到了 site-packages 里,Python 扫描的时候能找到。
安全警告:pip install 等同于安装软件
pip install 下载的包可以执行任意代码——安装过程中就可以。你跟从网上下了一个来路不明的 exe 跑起来是一样的。
只安装你明确需要的包
看包的下载量、GitHub stars、最近更新时间
用虚拟环境隔离——一个包出问题不会污染别的项目
对特别关键的环境(比如你的主系统),考虑用 Docker 等更强的隔离方式
JSON:程序之间沟通的语言
import json
# Python 字典 → JSON 字符串
data = {"name": "小明", "age": 18, "scores": [85, 92, 78]}
json_str = json.dumps(data, ensure_ascii=False)
print(json_str) # {"name": "小明", "age": 18, "scores": [85, 92, 78]}
# JSON 字符串 → Python 字典
received = '{"name": "小红", "age": 17}'
parsed = json.loads(received)
print(parsed["name"]) # 小红
JSON 是现代互联网最通用的数据交换格式。几乎所有的 API 都返回 JSON——前端到后端、服务到服务、程序到程序。学会读写 JSON,你就能跟互联网上的大多数服务对话。
一些可能对你有用的库
rich:让终端输出变漂亮——彩色文字、进度条、表格streamlit:把 Python 脚本变成网页应用——不需要学前端,十几行代码就有一个可交互的界面pandas:数据处理神器——读 Excel/CSV、清洗、分析、画图,几行搞定matplotlib:画图——折线图、柱状图、散点图playwright/selenium:浏览器自动化——让程序自动打开网页、点击、填表、截图flask/fastapi:写后端 API——让你写一个能接收 HTTP 请求并返回数据的服务器
安装第三方库的正确方式(含 C++ 编译注意事项)
有些 Python 包在安装时需要编译 C/C++ 代码(比如 numpy、pillow)。在 Linux 上你一般需要装编译工具:
# Debian/Ubuntu
sudo apt install build-essential python3-dev
# 然后正常 pip install
pip install numpy
在 Windows 上,pip 通常直接下载预编译好的 .whl 文件,不需要你自己编译。但如果你碰到报错说「找不到 Visual C++ Build Tools」——那就去装一个。
怎么继续学
练:编程是手艺,不是知识。你看了这节课的所有内容,不等于你会了。打开 Python,每一段示例都自己敲一遍,改了再跑。
做一个实际的东西:别只练语法。想一个你真的需要的工具——比如自动整理下载文件夹、每天发天气提醒到自己邮箱、统计你某个聊天群的发言——然后去实现它。不是为了炫技,是因为你自己需要。
读官方文档:docs.python.org。中文翻译很全面。标准库文档是最好的学习材料之一——每一个模块都有清晰的使用示例。
查 StackOverflow:你碰到的 99% 的问题,别人都碰到过,而且已经有了答案。学会用英文关键词搜索(
python how to sort list of dict),这是程序员最基本的技能——比记住所有语法重要得多。GitHub 上逛:找一些星标多的 Python 项目,读它们的代码。不用全读懂——感受一下真正的项目是怎么组织的、代码风格是什么样的。
第十六章:终极项目——从零到可分发
前面十五章你学了变量、控制流、函数、类、模块、包。现在把它们全用上,从头到尾走一遍真正的项目。
这一章有两个项目,它们合在一起构成一个完整系统:一台你可以在任何地方遥控的电脑。项目一用 socket 写服务器和终端客户端(你第一课学的 TCP 终于要自己写了),项目二用 tkinter 给同一个服务器写个图形界面。
每个项目都按真实的研究流程走:明确目标 → 找库 → 看文档 → 设计组件和拆分文件 → 开发 → 测试。最后讲怎么打包分发给别人。
研究流程:拿到一个需求,怎么开始
在动手敲代码之前,先讲讲方法论。你以后接到任何项目——无论大小——按这个流程走,不会慌:
1. 明确目标
我想要什么?输入是什么?输出是什么?用户怎么用?
写下来,哪怕只有三句话。目标不清就开始写代码 = 闭眼走路。
2. 找合适的库
这个需求 Python 标准库能解决吗?标准库没有的话 PyPI 上有什么?
搜「python + 关键词」,看两三个候选,对比 GitHub stars、文档质量、最近更新时间。
3. 看文档
不是从头读到尾——先看 Quickstart / Getting Started,把最简例子跑通。
然后看 API Reference 找你需要的具体功能。
4. 设计组件关系和拆分文件
这个程序有几大块?每块的职责是什么?块之间怎么通信?
画个简单的方框箭头图(纸上画就行),然后决定哪些东西放同一个文件。
5. 开发
从最简单的能跑的版本开始(一个文件、最核心功能),跑通了再加功能。
不要一口气写完所有代码再跑——写一点跑一点。
6. 测试
正常情况跑一遍,边界情况跑一遍(用户输入了奇怪的东西怎么办?网络断了怎么办?)。
不用写自动化测试——手动跑一遍关键路径就行。
下面两个项目,我们就按这个流程走一遍。
项目一:Socket 远程命令服务器 + 终端客户端
第一步:明确目标
我想做什么? 一台电脑(服务器)运行一个程序,在后台监听。我从另一台电脑(或同一台电脑的另一终端窗口)发命令过去,服务器执行后返回结果。
类比:这就是一个微缩版 SSH。你在第一课学过 TCP——客户端连服务器、发数据、收响应。现在你亲手写出来。
命令列表(为了安全,不让它执行任意 shell 命令——只支持白名单里的操作):
输入:用户在客户端敲一行命令。
输出:服务器执行结果,显示在客户端终端。
第二步:找库
socket:Python 标准库,不需要装。TCP 通信靠它。
threading:Python 标准库。服务器要同时处理多个客户端。
ast.literal_eval:Python 标准库。安全地计算数学表达式(
eval()危险——用户输入__import__('os').system('rm -rf /')你就完了;ast.literal_eval只能算字面量表达式,不会执行任意代码)。os / platform / subprocess:标准库。系统信息和目录浏览。
全部标准库——零依赖。
第三步:看文档(关键 API 速查)
你需要用到的 socket API 不多,就是第一课讲的 TCP 流程:
import socket
# 服务器端
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # IPv4, TCP
server.bind(("0.0.0.0", 8888)) # 监听所有网卡的 8888 端口
server.listen(5) # 最多 5 个排队等待
client, addr = server.accept() # 阻塞等待客户端连接
data = client.recv(4096) # 收最多 4096 字节
client.send(b"response") # 发送字节
client.close() # 关连接
# 客户端
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8888)) # 连服务器
client.send(b"command") # 发命令
response = client.recv(4096) # 收结果
client.close()
AF_INET = IPv4,SOCK_STREAM = TCP。端口号 8888 只要不跟系统服务冲突就行(大于 1024 的一般安全)。"0.0.0.0" 表示监听本机所有网络接口——你可以从局域网其他电脑连过来。
第四步:设计组件关系和拆分文件
remote_control/
├── server.py # 服务器:监听、处理命令、返回结果
└── client.py # 终端客户端:连接、发送命令、显示结果
server.py:一个CommandHandler类负责解析和执行命令;一个主函数负责 socket 监听和多线程client.py:一个交互循环——读用户输入、发到服务器、打印返回结果
第五步:开发
"""
远程命令服务器
监听 8888 端口,接收客户端命令,执行后返回结果。
支持多客户端同时连接。
"""
import socket
import threading
import ast
import os
import platform
import sys
class CommandHandler:
"""解析和执行命令。每个命令是一个方法,方便扩展。"""
def handle(self, command_str):
"""入口:接收原始字符串,返回结果字符串。"""
parts = command_str.strip().split(maxsplit=1)
cmd = parts[0].lower() if parts else ""
args = parts[1] if len(parts) > 1 else ""
handlers = {
"calc": self._calc,
"ls": self._ls,
"sysinfo": self._sysinfo,
"echo": self._echo,
"help": self._help,
}
if cmd in handlers:
try:
return handlers[cmd](args)
except Exception as e:
return f"[错误] {cmd} 执行失败:{e}"
else:
return f"[错误] 未知命令:{cmd}。输入 help 查看可用命令。"
def _calc(self, expr):
"""安全计算数学表达式。用 ast.literal_eval 而不是 eval。"""
# ast.literal_eval 只能处理字面量(数字、字符串、元组等),
# 不能处理运算符——所以我们用简单的替换把表达式包装成可求值的形式。
# 为了安全,只允许数字、空格和 + - * / // % ** ( ) .
allowed = set("0123456789+-*/%(). e")
if not all(c in allowed for c in expr):
return "[错误] 表达式包含不允许的字符。只支持数字和 + - * / // % ** ( )"
try:
result = eval(expr, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"[错误] 计算失败:{e}"
def _ls(self, path):
"""列出目录内容。默认当前目录。"""
target = path.strip() or "."
if not os.path.exists(target):
return f"[错误] 路径不存在:{target}"
if not os.path.isdir(target):
return f"[错误] 不是目录:{target}"
try:
entries = os.listdir(target)
entries.sort()
lines = []
for name in entries:
full = os.path.join(target, name)
tag = "/" if os.path.isdir(full) else ""
lines.append(f" {name}{tag}")
return "\n".join(lines) if lines else " (空目录)"
except PermissionError:
return f"[错误] 没有权限访问:{target}"
def _sysinfo(self, _args):
"""返回系统信息。"""
info = [
f"操作系统:{platform.system()} {platform.release()}",
f"主机名:{platform.node()}",
f"Python 版本:{sys.version.split()[0]}",
f"CPU 核心数(逻辑):{os.cpu_count()}",
]
return "\n".join(info)
def _echo(self, text):
"""原样返回文本。"""
return text if text else "(空)"
def _help(self, _args):
"""列出可用命令。"""
return (
"可用命令:\n"
" calc <表达式> - 计算数学表达式(例:calc 3 + 5 * 2)\n"
" ls [路径] - 列出目录内容(例:ls .)\n"
" sysinfo - 显示系统信息\n"
" echo <文本> - 原样返回文本\n"
" help - 显示此帮助\n"
" quit - 断开连接"
)
def handle_client(client_socket, address):
"""处理一个客户端连接。运行在独立线程中。"""
handler = CommandHandler()
print(f"[连接] {address[0]}:{address[1]} 已连接")
# 发送欢迎信息
welcome = (
f"=== 远程命令服务器 ===\n"
f"已连接到 {address[0]}:{address[1]}\n"
f"输入 help 查看可用命令,quit 断开连接。"
)
client_socket.send(welcome.encode("utf-8"))
try:
while True:
# 接收命令
data = client_socket.recv(4096)
if not data:
break # 客户端断开
command = data.decode("utf-8").strip()
if not command:
continue
print(f"[命令] {address[0]}:{address[1]} → {command}")
if command.lower() == "quit":
client_socket.send("再见!".encode("utf-8"))
break
# 执行命令并返回结果
result = handler.handle(command)
client_socket.send(result.encode("utf-8"))
except (ConnectionResetError, BrokenPipeError):
print(f"[断开] {address[0]}:{address[1]} 连接中断")
except Exception as e:
print(f"[异常] {address[0]}:{address[1]} 出错:{e}")
finally:
client_socket.close()
print(f"[关闭] {address[0]}:{address[1]} 连接已关闭")
def start_server(host="0.0.0.0", port=8888):
"""启动服务器,持续监听。"""
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# SO_REUSEADDR 让服务器重启时能立即绑定同一端口,不等 TIME_WAIT
try:
server.bind((host, port))
except PermissionError:
print(f"错误:没有权限绑定端口 {port}。试试大于 1024 的端口。")
return
except OSError as e:
print(f"错误:无法绑定 {host}:{port} - {e}")
return
server.listen(5)
print(f"服务器已启动 → {host}:{port}")
print("等待客户端连接...(Ctrl+C 停止)")
try:
while True:
client_socket, address = server.accept()
# 每个客户端一个线程,互不阻塞
thread = threading.Thread(
target=handle_client,
args=(client_socket, address),
daemon=True # 主程序退出时自动清理
)
thread.start()
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
server.close()
print("服务器已停止。")
if __name__ == "__main__":
start_server()
"""
远程命令客户端(终端版)
连接到服务器,在交互式提示符下输入命令,显示返回结果。
"""
import socket
def start_client(host="127.0.0.1", port=8888):
"""连接到服务器并开始交互循环。"""
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
client.connect((host, port))
except ConnectionRefusedError:
print(f"错误:无法连接到 {host}:{port}。服务器启动了吗?")
return
except socket.gaierror:
print(f"错误:无法解析地址 {host}。")
return
# 接收欢迎信息
welcome = client.recv(4096).decode("utf-8")
print(welcome)
print()
try:
while True:
# 读用户输入
try:
command = input(">> ").strip()
except (EOFError, KeyboardInterrupt):
print("\n断开连接。")
client.send(b"quit")
break
if not command:
continue
# 发送命令
client.send(command.encode("utf-8"))
if command.lower() == "quit":
response = client.recv(4096).decode("utf-8")
print(response)
break
# 接收响应
response = client.recv(4096).decode("utf-8")
print(response)
except (ConnectionResetError, BrokenPipeError):
print("\n与服务器的连接中断。")
finally:
client.close()
if __name__ == "__main__":
start_client()
第六步:测试
打开两个终端窗口:
# 终端 1:启动服务器
python3 server.py
# 输出:服务器已启动 → 0.0.0.0:8888
# 终端 2:启动客户端
python3 client.py
# 输出:=== 远程命令服务器 ===
# >>
测试清单:
>> help
(显示命令列表)
>> calc 3 + 5 * 2
13
>> calc 2**10
1024
>> ls .
(列出当前目录文件)
>> sysinfo
操作系统:Linux 6.x.x
主机名:my-computer
...
>> echo 你好世界
你好世界
>> quit
再见!
局域网测试:如果你有两台电脑在同一个 WiFi 下,把 client.py 里的 host 改成服务器的局域网 IP(ip addr 查看,通常是 192.168.x.x),客户端就能从另一台电脑连过来。这就是你第一课学的 TCP 在实战。
扩展思路
加文件上传下载(
upload/download命令,发送端读文件分块发送,接收端写文件)加认证(密码或密钥对——回忆第一课 TLS 的公私钥)
把通信加密(用
ssl标准库包裹 socket——这就是 HTTPS 的底层)把命令处理改成插件式(每个命令一个
.py文件,放到commands/文件夹,服务器自动发现加载)
项目二:Tkinter 图形界面客户端
第一步:明确目标
项目一的客户端是命令行的——在终端里敲 >> calc 3+5。但普通人看到黑窗口会怕。我们要给它穿一件衣服:一个窗口,有输入框、按钮、结果显示区、连接状态。
目标:一个图形界面程序,连上项目一的服务器,用户在输入框里敲命令,点「发送」按钮或按回车,结果显示在下方文本框。顶部显示连接状态。
第二步:找库
tkinter:Python 标准库,不需要装。Python 装好就能用
import tkinter。它是 Python 自带的 GUI 工具包,简单够用。PyQt 更强大更漂亮,但要另外装。FastAPI 是做网页界面的(也需要装)。对第一个 GUI 项目,tkinter 最合适——零依赖,立刻能跑。
第三步:看文档(关键 API 速查)
tkinter 的核心是「控件树」——窗口是根,上面放各种控件:
import tkinter as tk
from tkinter import scrolledtext
# 创建窗口
root = tk.Tk()
root.title("我的程序")
root.geometry("600x400") # 宽 x 高
# 标签
label = tk.Label(root, text="状态:未连接")
# 输入框
entry = tk.Entry(root, width=50)
entry.bind("<Return>", on_enter) # 绑定回车键
# 按钮
btn = tk.Button(root, text="发送", command=on_click)
# 滚动文本框
output = scrolledtext.ScrolledText(root, width=70, height=20)
# 布局(三种方式选一种):
label.pack() # 从上到下堆叠
entry.pack()
btn.pack()
output.pack()
# 启动主循环
root.mainloop()
第四步:设计组件
┌─────────────────────────────────────┐
│ [标签] 状态:已连接 / 未连接 │
├─────────────────────────────────────┤
│ [输入框] 在此输入命令 [发送] │
├─────────────────────────────────────┤
│ │
│ [滚动文本框] │
│ 显示命令历史和执行结果 │
│ │
│ │
├─────────────────────────────────────┤
│ [按钮] 连接 [按钮] 断开 [按钮] 清屏│
└─────────────────────────────────────┘
连接状态标签:绿色/红色文字
输入框 + 发送按钮:发命令
滚动文本框:显示所有交互历史
底部三个按钮:连接、断开、清屏
第五步:开发
gui_client.py:
"""
远程命令客户端(图形界面版)
使用 tkinter 连接项目一的 socket 服务器,提供窗口化的命令交互。
"""
import tkinter as tk
from tkinter import scrolledtext, messagebox
import socket
import threading
class RemoteClientGUI:
"""图形界面客户端。连接远程命令服务器,发送命令并显示结果。"""
def __init__(self):
# ── 网络状态 ──
self.socket = None
self.connected = False
# ── 构建界面 ──
self.root = tk.Tk()
self.root.title("远程命令客户端")
self.root.geometry("700x500")
self.root.resizable(True, True)
# 状态标签
self.status_label = tk.Label(
self.root,
text="● 未连接",
fg="red",
font=("Microsoft YaHei", 11)
)
self.status_label.pack(pady=(8, 4))
# 输入区(输入框 + 发送按钮放同一行)
input_frame = tk.Frame(self.root)
input_frame.pack(fill="x", padx=8, pady=4)
self.command_entry = tk.Entry(input_frame, font=("Consolas", 12))
self.command_entry.pack(side="left", fill="x", expand=True, padx=(0, 4))
self.command_entry.bind("<Return>", lambda e: self.send_command())
self.command_entry.config(state="disabled") # 未连接时禁用
self.send_btn = tk.Button(
input_frame, text="发送", width=8,
command=self.send_command, state="disabled"
)
self.send_btn.pack(side="right")
# 输出区
self.output = scrolledtext.ScrolledText(
self.root,
font=("Consolas", 11),
wrap="word", # 自动换行
state="disabled" # 只读
)
self.output.pack(fill="both", expand=True, padx=8, pady=4)
# 底部按钮
btn_frame = tk.Frame(self.root)
btn_frame.pack(fill="x", padx=8, pady=(4, 8))
self.connect_btn = tk.Button(
btn_frame, text="连接", width=10,
command=self.connect_to_server
)
self.connect_btn.pack(side="left", padx=2)
self.disconnect_btn = tk.Button(
btn_frame, text="断开", width=10,
command=self.disconnect, state="disabled"
)
self.disconnect_btn.pack(side="left", padx=2)
tk.Button(
btn_frame, text="清屏", width=10,
command=self.clear_output
).pack(side="left", padx=2)
# 连接信息(默认连本机)
self.host_var = tk.StringVar(value="127.0.0.1")
self.port_var = tk.IntVar(value=8888)
tk.Label(btn_frame, text=" 主机:").pack(side="left")
host_entry = tk.Entry(btn_frame, textvariable=self.host_var, width=12)
host_entry.pack(side="left", padx=2)
tk.Label(btn_frame, text=" 端口:").pack(side="left")
port_entry = tk.Entry(btn_frame, textvariable=self.port_var, width=6)
port_entry.pack(side="left", padx=2)
# ── 网络操作 ──
def connect_to_server(self):
"""连接到服务器。"""
if self.connected:
return
host = self.host_var.get().strip()
port = self.port_var.get()
self.append_output(f"正在连接 {host}:{port}...\n")
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(5) # 5 秒超时
self.socket.connect((host, port))
# 接收欢迎信息
welcome = self.socket.recv(4096).decode("utf-8")
self.append_output(welcome + "\n\n")
self.socket.settimeout(None) # 连接建立后取消超时,保持长连接
self.set_connected(True)
except ConnectionRefusedError:
self.append_output("[错误] 连接被拒绝。服务器启动了吗?\n")
self.cleanup_socket()
except socket.timeout:
self.append_output("[错误] 连接超时。检查主机地址和端口。\n")
self.cleanup_socket()
except Exception as e:
self.append_output(f"[错误] 连接失败:{e}\n")
self.cleanup_socket()
def send_command(self):
"""发送当前输入框的命令到服务器。"""
if not self.connected:
return
command = self.command_entry.get().strip()
if not command:
return
self.append_output(f">> {command}\n")
self.command_entry.delete(0, "end")
try:
self.socket.send(command.encode("utf-8"))
if command.lower() == "quit":
response = self.socket.recv(4096).decode("utf-8")
self.append_output(response + "\n")
self.disconnect()
return
response = self.socket.recv(4096).decode("utf-8")
self.append_output(response + "\n")
except (ConnectionResetError, BrokenPipeError, OSError):
self.append_output("[错误] 连接中断。\n")
self.disconnect()
def disconnect(self):
"""断开与服务器的连接。"""
if self.socket:
try:
self.socket.send(b"quit")
except Exception:
pass
self.cleanup_socket()
self.set_connected(False)
self.append_output("已断开连接。\n")
def cleanup_socket(self):
"""安全关闭 socket。"""
if self.socket:
try:
self.socket.close()
except Exception:
pass
self.socket = None
# ── UI 更新 ──
def set_connected(self, state):
"""更新连接状态 UI。"""
self.connected = state
if state:
self.status_label.config(text="● 已连接", fg="green")
self.command_entry.config(state="normal")
self.send_btn.config(state="normal")
self.connect_btn.config(state="disabled")
self.disconnect_btn.config(state="normal")
else:
self.status_label.config(text="● 未连接", fg="red")
self.command_entry.config(state="disabled")
self.send_btn.config(state="disabled")
self.connect_btn.config(state="normal")
self.disconnect_btn.config(state="disabled")
def append_output(self, text):
"""追加文本到输出区域。"""
self.output.config(state="normal")
self.output.insert("end", text)
self.output.see("end") # 自动滚动到底部
self.output.config(state="disabled")
def clear_output(self):
"""清空输出区域。"""
self.output.config(state="normal")
self.output.delete("1.0", "end")
self.output.config(state="disabled")
def run(self):
"""启动 GUI 主循环。关闭窗口时自动断开连接。"""
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
self.root.mainloop()
def _on_close(self):
"""窗口关闭时的清理。"""
if self.connected:
self.disconnect()
self.root.destroy()
if __name__ == "__main__":
app = RemoteClientGUI()
app.run()
第六步:测试
# 终端 1:启动服务器
python3 server.py
# 终端 2:启动图形界面客户端
python3 gui_client.py
点「连接」→ 状态变绿,收到欢迎信息
输入
help,点发送或按回车 → 看到命令列表输入
calc 2**20→ 返回1048576输入
sysinfo→ 看到系统信息输入
quit或点「断开」→ 状态变红服务器没启动时点连接 → 看到超时/拒绝错误提示,不崩溃
练手:试试把 host 改成你手机的热点 IP,用手机开热点连电脑——感受一下 TCP 跨设备通信。
打包分发:让别人能跑你的程序
代码写好了,但别人没有 Python 环境怎么办?或者你觉得每次让人家 pip install 一坨东西太麻烦?打包就是把你的 Python 程序变成双击就能跑的独立文件。
最简方案:uv(现代 Python 包管理器)
uv 是 Rust 写的 Python 包管理器,比 pip 快 10-100 倍,而且它把虚拟环境、依赖锁定、运行打包成一件事。
# 1. 安装 uv(只需要做一次)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows:powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
# 2. 在项目根目录初始化
cd remote_control
uv init # 创建 pyproject.toml
# 3. 我们的项目没有第三方依赖(全部用标准库),
# 但如果有,就这样加:
# uv add requests
# 4. 同步环境(创建虚拟环境 + 安装依赖)
uv sync
# 5. 运行!(uv 自动用虚拟环境跑)
uv run server.py
uv run client.py
uv run gui_client.py
uv run 的好处:你不需要手动 source venv/bin/activate——uv 自动找到虚拟环境。别人拿到你的项目,只需要装好 uv,然后 uv sync && uv run xxx.py 就完事了。
进阶:PyInstaller 打包成独立可执行文件
如果对方连 Python 和 uv 都不想装——那就把程序打成 .exe(Windows)或独立的可执行文件。
# 安装 pyinstaller
pip install pyinstaller
# 打包服务器(单文件,不弹终端窗口)
pyinstaller --onefile --name "rcmd-server" server.py
# 打包 GUI 客户端(Windows 上 --noconsole 不显示黑窗口)
pyinstaller --onefile --name "rcmd-client" --noconsole gui_client.py
# 输出在 dist/ 目录
ls dist/
# rcmd-server rcmd-client (Linux/macOS)
# rcmd-server.exe rcmd-client.exe (Windows)
--onefile 把所有依赖(包括 Python 解释器本身)打成一个文件。拿到这个文件的人双击就能跑,不需要装 Python。
注意:
打包后的文件很大(几十 MB)——因为包含了整个 Python 解释器
跨平台不通用:在 Windows 上打包的
.exe不能在 Linux 上跑;在 Linux 上打包的不能在 macOS 上跑。需要什么平台就在什么平台上打包。杀毒软件可能误报——因为打包后的结构跟恶意软件有相似之处(自解压 + 内嵌可执行代码)。这不是你的问题,是杀软的启发式检测太敏感。
PyInstaller 自动化脚本(写成 build.py,一键打包):
"""
自动化打包脚本。运行 python build.py 即可打包所有组件。
"""
import subprocess
import sys
import os
targets = [
{"script": "server.py", "name": "rcmd-server", "console": True},
{"script": "client.py", "name": "rcmd-client", "console": True},
{"script": "gui_client.py", "name": "rcmd-gui-client", "console": False},
]
for target in targets:
script = target["script"]
name = target["name"]
console_flag = [] if target["console"] else ["--noconsole"]
if not os.path.exists(script):
print(f"跳过:{script} 不存在")
continue
print(f"正在打包 {script} → {name}...")
cmd = [
sys.executable, "-m", "PyInstaller",
"--onefile",
"--name", name,
*console_flag,
"--clean", # 清理临时文件
"--noconfirm", # 不询问覆盖
script,
]
result = subprocess.run(cmd)
if result.returncode != 0:
print(f" ✗ {script} 打包失败")
else:
print(f" ✓ {name} 打包完成 → dist/{name}")
print("\n全部打包完成。可执行文件在 dist/ 目录。")
想要传递的感觉
你不是电脑的消费者,你是电脑的主人。 编程不是「写代码」,是「设计让电脑为你工作的流程」。从 Bash 脚本到 Python 程序,核心思想不变:你把意图变成可执行的指令,机器为你干活。
抽象和分层是计算机科学最伟大的发明。 你在第一课学了网络分层——你不需要知道链路层的细节就能用 HTTP。在这一课你学了函数和类——你不需要知道内部实现就能用别人写好的函数。分层让你每次只需要关注一个层次,把复杂性隔离开。这是通用的思维方式,不只适用于计算机。
编程语言只是工具。 Python 适合你入门,但世界上有几十种不同的语言,它们在不同场景下各有所长。你学的是编程思维——变量、控制流、抽象、复用——这些概念几乎在所有语言里都适用。不要跟语言结婚——跟解决问题结婚。
犯了错就去读报错。 Python 跟你很诚实——每一行报错都是信息,不是审判。学会读它们,你就解决了编程中一半以上的困难。
自动化思维比具体语法更重要。 语言会变(Python 流行之前是 Perl,再之前是 C),框架会变(Django 之前有 Ruby on Rails),但「这件事能不能让机器帮我做」的意识,是终身的。培养这种感觉比背语法重要一百倍。
与后续课的钩子
→ 第三课:当你的自动化和抽象能力足够,你就可以和别人一起建更大的东西——这就是开源。自由软件运动不只是一群程序员免费分享代码——它是一种劳动观念:被人看到、收到反馈、和志同道合的人一起改造世界。我们将从 Minecraft 社区讲起,因为你已经在那里体验过分享与协作的快乐。
→ 数学先导课:抽象的力量在数学里达到了极致。函数可以是一种向量,向量空间可以用基底变换重新理解——这和编程里的「换个数据结构,同样的逻辑变得简单」是同一件东西。你会看到数学和编程在底层思维方式上的共振。
→ 语文哲学课:「人是万物的尺度」。技术是给人用的,不是反过来。你学了编程不是为了被算法操控——是为了让技术服务于你的目标,而不是其他人的。