第二课:自动化与编程——让电脑为你工作

核心问题

电脑是工具。但如果你只会用它做别人设计好的事情——打开微信、打开浏览器、打开游戏——你就只是工具的消费者,不是工具的主人。

这一课想让你变成主人。

不是说你要成为职业程序员。而是说:当你遇到一个重复劳动——比如要把一百个文件改个名字、要从网页上抓一些数据下来整理、想让电脑在你睡觉的时候自动下载然后转换格式——你能意识到「这件事可以让电脑帮我做」,而且你知道从哪开始。

编程不是写代码。编程是设计让电脑为你工作的流程


第一章:你已经会编程了

上节课你敲过的命令

回想一下第一课,你在终端里敲过这些:

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。为什么不起名字直接用数字?因为:

  1. 名字让你知道这个数字代表什么——三个月后回来看草稿,你不会对着 5*3 + 1/2*2*3² 发愣

  2. 如果初速度从 5 变成 6,你只需要改 v0 = 6 一个地方,不用把所有公式里的 5 都翻出来改一遍

  3. 你可以复用这个名字——s = v0*t + 1/2*a*t² 之后你还能用 v0at 去算别的东西

编程里的变量,跟你物理草稿纸上的符号是一模一样的东西:给一个值起个名字,然后你可以用这个名字去引用它、操作它、反复使用它

等号不是「等于」

先看一行代码:

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 的关键字(ifwhilefordefclass 等等——这些是 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 的基本类型

类型

关键字

例子

能干什么

整数

int

42-70

加减乘除、取余、比较大小

浮点数

float

3.14-0.51e10

跟整数差不多,但有小数,要注意精度

字符串

str

"hello"'你好'

拼接、切片、查找、替换、大小写转换

布尔值

bool

TrueFalse

判断条件,and/or/not

空值

None

None

表示「啥也没有」,不是 0,不是空字符串,就是没有

为什么 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 的比较运算符跟数学差不多:

运算符

含义

例子

==

等于

x == 5

!=

不等于

x != 5

<

小于

x < 5

>

大于

x > 5

<=

小于等于

x <= 5

>=

大于等于

x >= 5

注意:判断相等是 ==(两个等号)。因为 = 已经被赋值占用了。

这是一个典型的偶然复杂度——假如当年的语言设计者选了 := 做赋值,那 = 就可以用来做判断相等了。但他们选了 = 做赋值,所以判断相等就得用 ==。习惯就好。

逻辑运算符:组合条件

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

选哪个容器?

需求

用哪个

有顺序、可能修改

列表(list)

有顺序、不会修改

元组(tuple)

键查值

字典(dict)

不重复、快速判断成员

集合(set)

这四种容器你会越来越熟悉。现在不用硬背所有操作——知道有这些工具,用到的时候查就行。


第八章:函数——封装与复用

你一直在用函数

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 装好的时候,附带了一大堆模块——这叫标准库。你不需要安装任何东西就能用:

模块

能干什么

os

操作系统交互

sys

Python 解释器本身(命令行参数、退出程序)

math

数学函数(sqrtsincospi

random

随机数(randintchoiceshuffle

datetime

日期和时间处理

json

JSON 格式的读写

csv

CSV 表格文件的读写

re

正则表达式(文本搜索和替换)

urllib / requests

网络请求(requests 需要装,比 urllib 好用)

sqlite3

轻量数据库

你不需要背这些——你需要知道的是:当你碰到一个需求的时候,先问自己「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 几百万个包,我怎么知道哪个适合我?

答案是:你不用掌握所有。你只需要一个地图,告诉你离你最近的包是什么。

  1. 先看标准库:Python 自带的功能已经覆盖了绝大多数基本需求

  2. Google 你的需求 + "python":比如「python 操作 excel」→ 你会发现 openpyxl 或 pandas

  3. 看 GitHub stars 和更新频率:被很多人用的、最近还在更新的包,大概率是靠谱的

  4. 看文档:好的包一定有好的文档。一个没有文档的包就像一家没有菜单的餐厅——你不知道能吃什么

  5. 看看是否有默认参数开箱可用:好用的包通常你只要传最少的信息就能跑起来,默认参数都是合理的。你不会每装一个新软件就翻遍它全部选项对吧?

虚拟环境:绿色的才是好文明

你装了一个包 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.pysix.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 的来源(优先级从高到低)

  1. 当前脚本所在目录(或交互模式时的当前工作目录)——自动加入

  2. PYTHONPATH 环境变量——你手动设置的,多个路径用 : 分隔(Windows 用 ;

    export PYTHONPATH=/home/你/my_libs:/home/你/another_lib
    python3 my_script.py
    
  3. .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
    
  4. 标准库路径和 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_functionmy_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 就是 heroother 是 mage

面向对象不是替代命令式——是补充

你不需要在所有地方都搞面向对象。很多任务——特别是短小的脚本、数据处理、一次性分析——命令式足够了。

面向对象的意义在于:当系统变复杂、多人协作时,它能帮你把复杂度装进盒子里。每个盒子管理好自己的数据,通过明确的接口跟别的盒子对话。你不用知道盒子里面怎么工作的——这跟你在第一课学的网络分层是一模一样的道理。

对模块和库的重新理解

回过头看——import osimport turtle——这些本质上也是面向对象。os 是一个模块(盒子),它封装了操作系统的细节,给你提供 listdirmkdir 这些接口。你不知道也不需要知道 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 里做的所有事,本质上就是:

  1. 在内存里读写数据

  2. 对数据做运算

  3. 根据条件跳转到不同的指令

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.namep.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/jsonRapidJSONsimdjsonBoost.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++ 里你需要:

  1. 告诉编译器头文件在哪(-I/path/to/include

  2. 告诉链接器库文件在哪(-L/path/to/lib

  3. 告诉链接器链接哪个库(-lcurl

  4. 如果库是动态链接的(.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),是编译时就发现了。你的程序还没跑,类型系统已经帮你排除了大量可能的错误。

范畴论更抽象,但核心思想你能懂:不看东西本身,看东西之间的关系。在编程里,「东西」是类型(IntStringList),「关系」是函数(Int -> String 做数字转文本,String -> Int 做解析)。范畴论研究的就是这些关系和它们的组合方式。比如,map 这个操作——你给一个函数 a -> b 和一个 List a,得到 List bmap 在范畴论里叫「函子(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],类型签名本身就是一份微型文档——不用读实现,你知道输入是什么、输出是什么。在大项目里,类型系统是团队协作的合约——它强制每个人遵守接口约定,否则编译不过。你在面向对象那章学的「封装」和「接口」,其数学本质就是类型论。

范畴论告诉你:抽象是怎么被「抽」出来的

范畴论的核心只有三个东西:对象、箭头、组合。对象是类型(IntStringList a),箭头是函数(Int → String),组合是把两个箭头接成一条新路(f: A→B 和 g: B→C 组合成 g∘f: A→C)。

为什么这个框架厉害?因为它不看东西是什么,只看东西怎么连接ListOptionalFutureIO——它们在范畴论的视角下是同一个模式的不同实例:都是函子(Functor),都支持 map 操作——给你一个容器和一个函数,把函数应用到容器里的值,返回一个同类型的容器。List.map(f) 把 [a, b, c] 变成 [f(a), f(b), f(c)]Optional.map(f) 把 Some(x) 变成 Some(f(x)),把 None 保持为 NoneFuture.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」变成「理解计算这件事本身」。


第十四章:课后测试

  1. 回到第一课你 ping 过的那些命令:写一个 .sh 或 .bat 脚本,让计算机依次执行三个你学过的网络诊断命令,并输出结果到文件中。这跟你手动敲有什么区别?

  2. 用 Turtle 写一个程序,画出一个等边三角形。需要输入边长,海龟根据输入画相应大小的三角形。(提示:内角 60°,外角 120°。right 转的是外角。)

  3. 接上题,把画三角形的代码封装成一个函数 draw_triangle(side_length)。然后用一个循环,画五个大小递增的三角形。你会体会到函数复用的价值。

  4. Python 里 input() 返回的是什么类型?为什么?如果你想让用户输入一个年份然后算出距今多久,你需要做什么转换?写出代码。

  5. 设计一个猜数字游戏:程序随机生成 1~100 之间的一个数,用户猜,程序提示「大了」或「小了」,直到猜对为止。统计猜了多少次。你需要用到哪些控制流结构?(random.randint 可以生成随机整数)

  6. 重新阐释为什么 = 是「赋值」而不是「等于」。解释 x = x + 1 在程序里的含义。如果 Python 使用 := 表示赋值,= 表示判断相等,会有什么不同?(试着说说语言设计中的偶然复杂度)

  7. 面向对象和命令式编程的根本区别是什么?在什么情况下你会选择用面向对象来组织代码?试着用第一课学到的网络分层概念来类比——分层设计的好处和面向对象封装的好处有什么相似之处?

  8. 你有一个班的成绩单,存储为一个字典列表:[{"姓名": "张三", "成绩": 85}, ...]。写出 Python 代码:计算平均分、找出最高分和最低分、列出不及格的名单。解释你每一步用了什么容器类型、为什么选它。

  9. 解释虚拟环境的作用。为什么不应该在系统全局 Python 里直接 pip install?作为类比:为什么你不会把所有游戏都装在 C 盘 Windows 目录下?

  10. 开放题:第一课教你「从底层开始排查网络故障」。类似地,如果你写了一个程序但没按预期工作,你会从哪里开始查?试着用三种复杂度的框架分析:这个问题是本质复杂度(算法设计错了)?还是偶然复杂度(语法不对、类型不匹配)?还是平台复杂度(环境变量没配、依赖没装)?


第十五章:扩展提高

以下内容供感兴趣的同学继续深入。每个小专题几分钟到几十分钟就能建立基本概念。

报错应该怎么看

Python 报错是帮你,不是骂你。学会读报错是编程最重要的技能之一。

Traceback (most recent call last):
  File "test.py", line 3, in <module>
    print(x / y)
NameError: name 'x' is not defined
  • 最下面一行是错误类型和描述:NameErrorx 没定义

  • 往上看是调用链:test.py 的第三行出的事

  • 出错的位置不一定就是错误的原因——有时候你把一个错误的值传了三层函数才炸

常见错误速查:

错误

含义

常见原因

SyntaxError

语法错误

漏了冒号、括号不匹配、缩进乱了

NameError

名字不存在

变量没定义就用了、函数名打错了

TypeError

类型不对

把字符串当数字用、传错了参数类型

IndexError

索引越界

列表就 5 个元素你访问了第 10 个

KeyError

键不存在

字典里没这个键

FileNotFoundError

文件不存在

路径写错了、文件被删了

ImportError

导入失败

模块名打错了、没 pip install

核心原则:不要猜,读报错。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 参数: 返回值。它特别适合作为 mapfiltersorted 的回调:

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 的 enumerateziplambda 都是前一种。

函数的更多形态

默认参数

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 去哪里找?

  1. 当前目录

  2. PYTHONPATH 环境变量里的目录

  3. 标准库目录

  4. 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++ 代码(比如 numpypillow)。在 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 命令——只支持白名单里的操作):

命令

功能

例子

calc 表达式

安全计算数学表达式

calc 3 + 5 * 2 → 13

ls 路径

列出目录内容

ls .

sysinfo

返回系统信息

操作系统、主机名、CPU 核心数

echo 文本

原样返回

echo hello world

help

列出所有可用命令

quit

断开连接

输入:用户在客户端敲一行命令。
输出:服务器执行结果,显示在客户端终端。

第二步:找库

  • 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:一个交互循环——读用户输入、发到服务器、打印返回结果

第五步:开发

server.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()

client.py

"""
远程命令客户端(终端版)
连接到服务器,在交互式提示符下输入命令,显示返回结果。
"""
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
  1. 点「连接」→ 状态变绿,收到欢迎信息

  2. 输入 help,点发送或按回车 → 看到命令列表

  3. 输入 calc 2**20 → 返回 1048576

  4. 输入 sysinfo → 看到系统信息

  5. 输入 quit 或点「断开」→ 状态变红

  6. 服务器没启动时点连接 → 看到超时/拒绝错误提示,不崩溃

练手:试试把 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 社区讲起,因为你已经在那里体验过分享与协作的快乐。

  • → 数学先导课:抽象的力量在数学里达到了极致。函数可以是一种向量,向量空间可以用基底变换重新理解——这和编程里的「换个数据结构,同样的逻辑变得简单」是同一件东西。你会看到数学和编程在底层思维方式上的共振。

  • → 语文哲学课:「人是万物的尺度」。技术是给人用的,不是反过来。你学了编程不是为了被算法操控——是为了让技术服务于你的目标,而不是其他人的。