命令行不是黑框,是一个用打字代替点击的界面。
同一个 .py 文件,命令行加文件名、-c 给一行、-m 按模块名,三条路都能跑;
再加一条交互模式,什么都不存。
程序跑完会留下一个整数,叫退出码。0 是成了,非 0 是没成。
上一篇装好了 Python,这一篇回答「在哪儿写、怎么跑」。
教材第 1 章另外三节是【认识文字命令操作界面】和【开始用 Python 解决问题】。
还有一节【安装程序集成开发环境】,讲的是 IDE,也就是编辑器加运行按钮。
本篇的顺序是:命令行是什么 → 三条路跑同一个文件 → IDE 替你省了哪几步 → 退出码怎么用。
这一篇的每个结论在演示里都真跑过,包括那个 -m 和直接跑文件的差别。
一、命令行是打字的界面
图形界面里你点图标、点菜单,命令行里你打字。换的是输入方式,不是难度。
一行命令拆开来只有两段:程序名,后面跟一串参数。
python hello.py 里,python 是程序名,hello.py 是参数。
命令行还有一个东西叫当前目录。你敲的所有相对路径,都是相对它算的。
百分之八十的「文件找不到」,都是当前目录不对。
为什么非学不可?因为第 10 章要部署网站、第 11 章要跑定时任务,那些机器上没有桌面,只有一行 shell。 服务器不认识鼠标。 Windows 上有 cmd 和 PowerShell 两个,macOS 上是终端(Terminal)。 本书的命令在三个系统上都能用,差别只在提示符长什么样。
二、三条路,跑的是同一个文件
第一路最常见:python 文件名.py。整份文件从头跑到尾,跑完退出。
第二路是 python -c "一行代码"。不建文件,临时验算一句。
这一路有个坑:它没有 __file__,因为压根没有文件。
第三路是 python -m 包名.模块名。按模块名找文件,不是按路径。看着差不多,差别在下一篇会说。
还有一路不是「跑文件」,是交互模式:直接敲 python 回车,出现 >>> 提示符,你敲一句它答一句。
这是验算最快的地方,也是什么都不留下的地方——关掉就没了。
| 入口 | 写法 | 适合干什么 | 会留下什么 |
|---|---|---|---|
| 跑文件 | python a.py |
正经程序 | .pyc 缓存 |
| 给一行 | python -c "…" |
临时验算 | 什么都不留 |
| 按模块 | python -m pkg.a |
包内互相 import | .pyc 缓存 |
| 交互 | python |
试语法、试函数 | 什么都不留 |
演示里第一条就露出了一个细节:跑文件时,sys.argv[0] 是脚本自己的路径,
你传的第一个参数在 sys.argv[1]。
而且传进来的参数一律是字符串。你敲 42,程序里拿到的是 '42',要算数得自己 int() 一下。
三、-m 和直接跑文件,差在哪
差在 sys.path[0]——Python 找模块时第一个翻的目录。
python -m pkg.probe 把当前目录放进去;python pkg/probe.py 放的是脚本所在的那个目录。
演示里两条路的 path[0] 一个显示 m03(当前目录),一个显示 pkg(脚本目录)。
看着只差一层,后果不小。
包里 probe.py 想 import pkg.util,用 -m 跑,当前目录在搜索路径上,能找到 pkg 这个包;
直接跑文件,搜索路径是 pkg/ 里面,它看不见 pkg 自己,于是报「找不到模块」。
所以后面凡是包里带 import 的,一律用 -m 启动。
第 9 章起 Flask、第 12 章跑留言板,用的都是这种方式。
四、IDE 替你省了哪几步
IDE 就是「编辑器 + 运行按钮 + 一堆辅助」。它本身不是新东西,底下调的还是 python 文件名.py。
省下的第一件事是补全。敲三个字母,它列出可能的函数名,省得你背 API。
第二件是断点。点一下行号,程序跑到那儿停住,你能看每个变量当时的值。
这是 print 调试法唯一真正的替代品。
第三件是报错定位。抛异常时直接跳到出错那一行,不用自己数行号。
第四件是工作目录。
这一件最容易被忽略,也最容易埋坑:IDE 会替你设好当前目录,
所以同样的代码,IDE 里能跑,命令行里报「文件找不到」。
碰到这种情况别怀疑代码,先看当前目录。在程序开头加一句 print(os.getcwd()),两边一比就知道。
教材推荐的是 VS Code 和 Thonny。两个都行:VS Code 通用、插件多; Thonny 极简,自带变量面板,初学更直观。 本书既不依赖也不排斥,你用什么写都可以,命令行的三条路必须会。
五、退出码是程序留给外面的一张字条
程序跑完会还给系统一个整数,叫退出码(exit code)。0 表示成功,非 0 表示失败。
三种情况在演示里都跑了:正常结束是 0;主动 sys.exit(3) 是 3;抛出没被接住的异常是 1。
注意最后那个 1:程序崩了,Python 替你填了 1。
所以「退出码非 0」既可能是你主动报的错,也可能是程序崩了,外面分不出来。
谁在看这个数?三路人马。一是 shell 的 &&——a && b 表示 a 成功了才跑 b。
二是定时任务,失败了要发告警。三是部署脚本和容器,退出码非 0 就当这次启动失败,重启。
自己写的脚本想告诉外面「数据没准备好,别往下走」,就 sys.exit(1)。
想区分不同原因,用不同的非零数:1 是文件没了、2 是格式不对。约定俗成,不是硬规定。
命令行里看上一次的退出码:Linux 和 macOS 敲 echo $?,Windows 的 cmd 敲 echo %ERRORLEVEL%。
六、动手 Demo:同一个文件,三条路都能跑
写两个小文件,用三种方式启动它们,每次都把 argv 和 __name__ 打出来。
再对比 -m 和直接跑文件的搜索路径差异。
最后看退出码:正常、主动退出、抛异常,各是多少。
import os, subprocess, sys, tempfile
P = sys.executable
def run(args, **kw):
p = subprocess.run([P] + args, capture_output=True, text=True, **kw)
return p.returncode, (p.stdout + p.stderr).strip()
d = tempfile.mkdtemp()
os.chdir(d)
# ① 一路:跑文件
open('t.py', 'w').write(
'import sys, os\n'
'print(" argv =", sys.argv)\n'
'print(" name =", __name__)\n')
print('① 路一:命令行跑文件')
print(run(['t.py'])[1])
# ② 带参数
open('hello.py', 'w').write(
'import sys\n'
'print(" argv =", sys.argv)\n'
'print(" name =", __name__)\n')
print('② 路二:命令行加参数')
print(run(['hello.py', 'apple', '42', '--flag'])[1])
# ③ -c 直接给一行
print('③ 路三:-c 直接给一行')
print(' 输出', run(['-c', 'print(int("1024"), "hello".upper())'])[1])
# ④ -m 与直接跑文件的差别
os.makedirs('pkg', exist_ok=True)
open('pkg/__init__.py', 'w').write('')
open('pkg/probe.py', 'w').write(
'import sys\n'
'print(" path[0] =", sys.path[0])\n'
'print(" name =", __name__)\n'
'print(" package =", __package__)\n')
print('④ -m 按模块名跑')
print('python -m pkg.probe')
print(run(['-m', 'pkg.probe'])[1])
print('python pkg/probe.py')
print(run(['pkg/probe.py'])[1])
# ⑤ input 从管道拿输入
print('⑤ input 从管道拿输入')
rc, out = run(['-c', 'print("提示语与回答:" + input("请输入名字:"))'],
input='小明\n')
print(' ' + out)
# ⑥ 退出码
print('⑥ 退出码是给外面看的')
for tag, code in (('正常结束', 'pass'),
('主动 sys.exit(3)', 'import sys; sys.exit(3)'),
('抛异常', '1/0')):
print(' %-18s 退出码 %d' % (tag, run(['-c', code])[0]))
实跑输出:
--------------------------------------------------------------------
同一个文件,三条路都能跑
--------------------------------------------------------------------
① 路一:命令行跑文件
--------------------------------------------------------------------
argv = ['t.py']
name = __main__
② 路二:命令行加参数,参数进 sys.argv
--------------------------------------------------------------------
argv = ['hello.py', 'apple', '42', '--flag']
name = __main__
argv[0] 永远是脚本自己的路径,你传的参数从 argv[1] 开始。
所以想拿第一个参数,要写 sys.argv[1],不是 sys.argv[0]。
注意 42 进来是字符串 42,带了引号的样子。命令行参数一律是字符串,要算数得自己 int()。
③ 路三:-c 直接给一行
--------------------------------------------------------------------
输出 1024 HELLO
这条路没有文件,也就没有 __file__。临时验算一行代码时最快。
④ -m 按模块名跑
--------------------------------------------------------------------
python -m pkg.probe
path[0] = m03
name = __main__
package = pkg
python pkg/probe.py
path[0] = pkg
name = __main__
package = None
两条路的 path[0] 不同:-m 把当前目录放进去,直接跑文件放的是脚本所在目录。
这就是为什么包里的模块互相 import 时,用 -m 跑不容易报「找不到模块」。
第 9 章跑 Flask、第 12 章跑留言板,用的都是这种按模块名启动的方式。
⑤ input 从管道拿输入
--------------------------------------------------------------------
提示语与回答:请输入名字:你好,小明
input() 拿到的永远是字符串。要数字,自己转。
⑥ 退出码是给外面看的
--------------------------------------------------------------------
正常结束 退出码 0
主动 sys.exit(3) 退出码 3
抛异常 退出码 1
退出码 0 表示成功,非 0 表示失败。定时任务、部署脚本判断「这次跑没跑成」,看的就是这个数字。
第一段两行就够看了:argv 里只有脚本自己的名字,name 是 main。 这说明「这份文件是被直接跑起来的」,不是被别人 import 进去的。 第二段是新手第一个坑:传了三个参数,argv 里有四项。argv[0] 永远是脚本自己,参数从 argv[1] 开始。 另外 42 进来是字符串 42,带引号的样子——命令行参数一律是字符串,要算数得自己转。 第四段是本篇最值得记的一处。 两条路的 path[0] 不同:-m 把当前目录放进去,直接跑文件放的是脚本所在目录。 这就是为什么包里的模块互相 import 时,用 -m 跑不容易报「找不到模块」。 第五段 input 是从管道喂进去的,说明它不挑来源:键盘、管道、文件重定向都行,拿到的永远是字符串。 第六段三个数字连起来看:0 是成了,3 是你自己报的,1 是程序崩了。 外面只知道「非 0」,不知道是哪一种,所以想区分原因就得自己定数字。
30 秒实验
- 把演示第二段的参数去掉一个,重跑,看
argv少了几项,验证「参数从 argv[1] 开始」。 - 在命令行敲
python t.py; echo $?(Windows 用echo %ERRORLEVEL%), 把t.py最后加一句sys.exit(7)再跑,看那个数变没变。 - 把
python -m pkg.probe改成在pkg目录里敲python probe.py, 对比path[0]打印出来的东西。
常见误区
- 「命令行是给高手用的。」它就是另一种输入方式,而且是唯一能在服务器上用的那种。 早晚要用,早学早省事。
- 「
sys.argv[0]是第一个参数。」它是脚本自己的路径。 你的第一个参数在argv[1]。 数组下标从 0 数这件事,在这里最容易咬人。 - 「IDE 里能跑,代码就没问题。」IDE 替你设了工作目录。 同一个文件换个地方启动就报错,八成是相对路径的问题,不是代码的问题。
自测三问(点开对答案)
1. `python -m pkg.probe` 和 `python pkg/probe.py`,跑的是同一个文件, 为什么前者不容易报「找不到模块」? —— 因为 `sys.path[0]` 不一样。 `-m` 把当前目录放进搜索路径,所以 `import pkg.util` 能找到 `pkg` 这个包; 直接跑文件放的是 `pkg/` 目录本身,搜索路径里没有 `pkg` 的上一层,于是看不见 `pkg`。 2. 程序里写 `sys.exit(2)` 和「抛出一个没接住的异常」,对调用方来说有什么不一样? —— 调用方看到的都是「非 0」,但数字不同:前者是 2,后者是 1。 所以你可以用不同的数字表示不同的失败原因,让外面的脚本分得清。 如果外面只看「成没成」,那两者没区别。 3. 交互模式(`>>>`)里敲的代码,为什么关掉就没了? —— 因为它不写文件。交互模式是把每一句读进来、编译、执行、打印结果,循环一遍就丢掉。 它适合试语法、试一个函数怎么用,不适合写超过十行的东西——超过十行就该建文件了。下一篇正式开始写代码:从一道问答题测验入手,看 if 怎么改变程序的走向。
- ☐ (本篇完,下一篇 2.1 从问答题测验看 if 判断)