🐍 第1章 认识 Python 语言
📄 一句话概括

命令行不是黑框,是一个用打字代替点击的界面。 同一个 .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 秒实验

  1. 把演示第二段的参数去掉一个,重跑,看 argv 少了几项,验证「参数从 argv[1] 开始」。
  2. 在命令行敲 python t.py; echo $?(Windows 用 echo %ERRORLEVEL%), 把 t.py 最后加一句 sys.exit(7) 再跑,看那个数变没变。
  3. 把 python -m pkg.probe 改成在 pkg 目录里敲 python probe.py, 对比 path[0] 打印出来的东西。

常见误区

  1. 「命令行是给高手用的。」它就是另一种输入方式,而且是唯一能在服务器上用的那种。 早晚要用,早学早省事。
  2. 「sys.argv[0] 是第一个参数。」它是脚本自己的路径。 你的第一个参数在 argv[1]。 数组下标从 0 数这件事,在这里最容易咬人。
  3. 「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 怎么改变程序的走向。

相关笔记