🐍 第4章 操作文件夹与文件:同步备份文件
📄 一句话概括

argparse 有三类参数:必填的位置参数、带值的选项、不给值就是真的开关。 if __name__ == '__main__' 防的是「别人 import 你的文件时顺手把整个程序跑起来」。 可变与不可变是全书最重要的一条分界线:int、str、tuple 改了就是换新对象; list、dict、set 原地改,id 不变。

📝 本章和前面几章的关系

上一篇写完了备份脚本,但参数那块只露了个头。这一篇补完。 教材第 4 章最后三节是【设置命令行指令的选择性参数和参数动作】【直接运行 Python 程序文件】 最后一节是【可变与不可变的数据类型和 Tuple】。 三件事看着不搭界,其实是同一层:都是「程序怎么跟外面打交道」——参数从外面来, __name__ 决定它被谁调用,可变不可变决定它传出去之后会不会被改。 最后那条是第 3 章那个坑的正式答案。学完这篇,前面很多「为什么这里改了那里也变」都能解释。

一、三种参数,三行声明

演示第一段跑了三次同一个脚本,差别只在参数。

类型 声明写法 拿到什么
位置参数 add_argument('path') 字符串,必须给
带值选项 add_argument('-n', type=int, default=10) 整数,不给就是 10
开关 add_argument('-v', action='store_true') 不给是 False,给了是 True
限定取值 add_argument('--mode', choices=['fast','safe']) 只能是这两个之一

给了 -v 就是 True,不给就是 False,这叫开关参数。action='store_true' 就是这个意思。 带值选项写了 type=int,拿到就是整数。不写的话是字符串,后面算数还得自己转。 声明类型等于让 argparse 替你校验。 choices 更狠:只允许 fast 或 safe。 演示里写 --mode turbo,argparse 直接拦下来,打印「invalid choice」,退出码 2。

退出码 2 这个细节值得记。 参数错误不是 1,是 2。 原因是 1 留给了「程序运行中出错」,2 专门表示「你命令行打错了」。 脚本外面想区分「是我打错了还是程序坏了」,看这个数。

二、if __name__ 到底防了什么

每个脚本结尾都有这么一段。 它防的事情很具体:别人 import 你的文件时,不该顺手把整个程序跑起来。 演示第二段造了一个 mymod.py,里面有一句打印、一个 area 函数、 还有一段在 if __name__ == '__main__': 里面。 直接运行它,三句都出现,__name__ 是 __main__。 被别的文件 import 时,「这句话只有直接运行时才出现」这句不见了,__name__ 是 mymod。

原因是:__name__ 的值取决于这个文件是怎么被用起来的。 自己当主程序跑,它是 __main__; 被别人 import,它是模块名。 所以那个 if 翻译成人话是:「如果我是主程序,就执行下面这些; 如果我是被借用的工具,就只提供函数,别乱跑。」 为什么非要写? 因为你写的每个文件都可能被别人 import——包括你自己。 第 8 章写类、第 9 章写 Flask 应用,都会遇到「这个文件既想能直接跑,又想让别人 import 它的函数」。

三、让 .py 在 Unix 上直接跑

Linux 和 macOS 上,./hello.py 直接跑需要两件事:文件第一行写 #!/usr/bin/env python3, 然后 chmod +x hello.py 给它执行权限。 第一行那个东西叫 shebang。#! 后面跟解释器的路径,告诉系统「用谁来跑这个文件」。 写 env python3 而不是写死 /usr/bin/python3,是为了跟着环境变量找。 Windows 不看这一行。 它靠 .py 的文件关联,双击就跑。 所以同一份代码在 Windows 上这一行纯属注释,在 Linux 上少了它就跑不起来。

Windows 想双击不弹黑框,把后缀改成 .pyw。想做成一个能发给别人的 exe,用 pyinstaller。 后者是另一个话题,本书不展开。 这一段在演示里标了「平台原样 · 本沙箱不执行」——沙箱是 Windows,chmod 没有意义。 想看效果得在 Linux 或 macOS 上试。

四、不可变:改了就是换了个新对象

a = 10,a = a + 1。看着像把 10 改成了 11,实际上不是。 演示第四段把 id() 打出来了:加一之前一个值,加一之后另一个值。 说明新造了一个 11,让 a 指过去,原来那个 10 还在内存里(只是没人指了)。 字符串一样。s.replace('a', 'x') 不给 s 本身做任何改动,它返回一个新串。 想留住结果必须 s = s.replace(...)。

这就是为什么字符串的方法全都返回新值。不是设计啰嗦,是因为原来那个改不了。 id() 这个函数给的是对象在内存里的地址。 用它判断「是不是同一个东西」最直接——id(a) == id(b) 为真,就是同一个对象, 不是两个长得一样的东西。

五、可变:原地改,id 不变

L = [1, 2, 3],L.append(4)。id 前后完全一样。列表是原地改的,没有造新对象。

类型 可变吗 改了 id 变吗
int、float、str 不可变 变
tuple 不可变 变
list、dict、set 可变 不变

这张表要背下来。 它解释了三件事:第 3 章那个「传列表进函数会被改掉」、第 5 章 set 为什么能快速判断「在不在」、 以及下面这一条。 字典的键只能用不可变类型。 因为字典靠哈希值定位,而哈希值要求「这个东西不会变」。 列表可变,哈希值随时会变,所以不能当键;元组不可变,能当键。 试着拿一个列表当字典的键,直接报 TypeError: unhashable type: 'list'。 这条报错信息里的 unhashable,就是「不可哈希」的意思。

六、元组:不给改的列表

t = (3, 4),t[0] = 9 报 TypeError: tuple object does not support item assignment。 元组的用处不是「省内存」,是「表示一组固定搭配」:一个坐标、一条数据库记录、一个函数返回的多个值。 它的价值在「保证不会被改」。 传给函数不怕被改,这是最实际的好处。 第 3 章那个 add_item(box, ...) 的坑,换成元组就不存在了——它压根不给你改。

解包是元组最常见的用法:x, y = t。函数返回多个值时,本质上就是返回一个元组,左边解包拆开。 最后一个坑:单元素元组必须带逗号。 (5) 是带括号的 5,类型是 int;(5,) 才是元组。 那个逗号不能省。 空元组写 (),没歧义。所以「一个元素的元组」是唯一需要特别小心的情况。

七、动手 Demo:可选参数、直接运行,还有可变与不可变

先跑三次同一个脚本,看三类参数各自怎么取值、怎么被拦。 再让一个模块被直接跑一次、被 import 一次,看 __name__ 的两种值。 最后用 id() 把可变与不可变摆出来——这是本篇的重点。

import subprocess, sys, tempfile, os

d = tempfile.mkdtemp()
ARGS = os.path.join(d, 'args.py')
open(ARGS, 'w').write('''import argparse
ap = argparse.ArgumentParser()
ap.add_argument('path')
ap.add_argument('-n', '--num', type=int, default=10)
ap.add_argument('-v', '--verbose', action='store_true')
ap.add_argument('--mode', choices=['fast', 'safe'], default='safe')
a = ap.parse_args()
print('    path    =', a.path)
print('    num     =', a.num, type(a.num).__name__)
print('    verbose =', a.verbose)
print('    mode    =', a.mode)
''')
P = sys.executable
print('① argparse 的三种参数')
for args in (['data.csv'], ['data.csv', '-n', '3', '-v'], ['data.csv', '--mode', 'turbo']):
    print('  python args.py ' + ' '.join(args))
    r = subprocess.run([P, ARGS] + args, capture_output=True, text=True)
    out = (r.stdout + r.stderr).strip().splitlines()
    for ln in out[-2:]:
        print('    ' + ln)
    print('    退出码 %d' % r.returncode)

print('② if __name__ == \'__main__\' 到底防了什么')
MOD = os.path.join(d, 'mymod.py')
open(MOD, 'w').write('''def area(w, h):
    return w * h
print('模块被加载了,名字是', __name__)
if __name__ == '__main__':
    print('这句话只有直接运行时才出现')
    print('面积', area(3, 4))
''')
print('  直接运行 mymod.py:')
for ln in subprocess.run([P, MOD], capture_output=True, text=True).stdout.strip().splitlines():
    print('    ' + ln)
print('  被别的文件 import:')
USE = os.path.join(d, 'use.py')
open(USE, 'w').write('import mymod\nprint("    area =", mymod.area(5, 6))\n')
for ln in subprocess.run([P, USE], capture_output=True, text=True, cwd=d).stdout.strip().splitlines():
    print('    ' + ln)

print('③ 让 .py 在 Unix 上直接跑')
print('  [平台原样 · 本沙箱不执行]')
print('  | #!/usr/bin/env python3')
print("  | print('hello')")
print('  存成 hello.py 后:chmod +x hello.py,就能 ./hello.py 直接跑')

print('④ 不可变:改了就是换了个新对象')
a = 10
print('  a = 10        id =', id(a))
a = a + 1
print('  a = a + 1     id =', id(a), ' ← 换了')
s = 'abc'
print('  s = %r     id = %d' % (s, id(s)))
s2 = s.replace('a', 'x')
print('  s.replace 之后 s 还是 %r,s2 = %r' % (s, s2))

print('⑤ 可变:原地改,id 不变')
L = [1, 2, 3]
print('  L = [1,2,3]   id =', id(L))
L.append(4)
print('  L.append(4)   id =', id(L), ' ← 没变')
print('  L =', L)
print('| 类型 | 可变吗 | 改了 id 变吗 |')
print('|---|---|---|')
for t, mut in (("int、float、str", "不可变"), ("tuple", "不可变"), ("list、dict、set", "可变")):
    print('| %s | %s | %s |' % (t, mut, '变' if mut == '不可变' else '不变'))

print('⑥ 元组:不给改的列表')
t = (3, 4)
print('  t = (3, 4)      t[0] =', t[0])
try:
    t[0] = 9
except TypeError as e:
    print('  t[0] = 9        → TypeError:', e)
x, y = t
print('  解包 x, y = t   →', x, y)
print('  单元素元组 (5,) →', repr((5,)), '而 (5) 是', type((5)).__name__)

实跑输出:

--------------------------------------------------------------------
可选参数、直接运行,还有可变与不可变
--------------------------------------------------------------------

  ① argparse 的三种参数
  --------------------------------------------------------------------
    python args.py data.csv
      path    = data.csv
      num     = 10 int
      verbose = False
      mode    = safe
    python args.py data.csv -n 3 -v
      path    = data.csv
      num     = 3 int
      verbose = True
      mode    = safe
    python args.py data.csv --mode turbo
      usage: args.py [-h] [-n NUM] [-v] [--mode {fast,safe}] path
      args.py: error: argument --mode: invalid choice: 'turbo' (choose from 'fast', 'safe')
      退出码 2
  给了 -v 就是 True,不给就是 False,这叫开关参数。
  --mode 只允许 fast 或 safe,写 turbo 直接被拦下来,退出码 2。
  注意 --num 写了 type=int,拿到就是整数;不写的话是字符串,后面算数还得自己转。
  声明类型等于让 argparse 替你校验。

  ② if __name__ == '__main__' 到底防了什么
  --------------------------------------------------------------------
    直接运行 mymod.py:
      模块被加载了,名字是 __main__
      这句话只有直接运行时才出现
      面积 12
    被别的文件 import:
      模块被加载了,名字是 mymod
      area = 30
  被 import 时 __name__ 是模块名 mymod,所以 if 里面那两句不执行。
  直接运行时 __name__ 是 __main__,所以执行。
  这就是为什么每个脚本结尾都要写这一段:别人 import 你的文件拿函数用,不该顺手跑起整个程序。

  ③ 让 .py 在 Unix 上直接跑
  --------------------------------------------------------------------

  [平台原样 · 本沙箱不执行]
  | #!/usr/bin/env python3
  | print('hello')
  存成 hello.py 后:chmod +x hello.py,就能 ./hello.py 直接跑

  第一行那个 #! 叫 shebang,告诉系统用哪个解释器。Windows 上不看这一行,靠 .py 的文件关联;
  Linux 和 macOS 上它是必须的。
  Windows 想双击就跑,更省事的办法是把后缀改成 .pyw(不弹黑框),或者用 pyinstaller 打包成 exe。

  ④ 不可变:改了就是换了个新对象
  --------------------------------------------------------------------
    a = 10        id = 140705459938504
    a = a + 1     id = 140705459938536  ← 换了
    s = 'abc'     id = 2203611948608
    s.replace     s 还是 'abc',s2 = 'xbc'
  整数和字符串不可变。a = a + 1 不是把 10 改成 11,是新造了一个 11,再让 a 指过去。
  所以字符串的方法都返回新串,不原地改。

  ⑤ 可变:原地改,id 不变
  --------------------------------------------------------------------
    L = [1,2,3]   id = 2203621458688
    L.append(4)   id = 2203621458688  ← 没变
    L = [1, 2, 3, 4]

| 类型 | 可变吗 | 改了 id 变吗 |
|---|---|---|
| int、float、str | 不可变 | 变 |
| tuple | 不可变 | 变 |
| list、dict、set | 可变 | 不变 |

  这一条决定了很多事:dict 的键只能用不可变类型(因为要能算哈希),元组能当键、列表不能。

  ⑥ 元组:不给改的列表
  --------------------------------------------------------------------
    t = (3, 4)      t[0] = 3
    t[0] = 9        → TypeError: 'tuple' object does not support item assignment
    解包 x, y = t   → 3 4
    单元素元组要写逗号 (5,) → (5,)
  元组用来表示「一组固定搭配」,比如一个坐标、一条数据库记录。
  不可变的好处是安全:传给函数不怕被改,还能当字典的键。
  注意 (5) 不是元组,是带括号的 5;(5,) 才是。这个逗号不能省。

第一段三次调用,前两次正常,第三次被拦。 注意最后一次的退出码是 2,不是 1——argparse 用 2 表示「命令行打错了」。 第二段是本篇最实用的一处:同一个文件,被直接跑时 name 是 main,被 import 时是 mymod。 所以 if 里面那两句只在直接跑时执行。 第四段那个 id 变化是关键证据:a = a + 1 不是把 10 改成 11,是新造了一个 11。 字符串的方法全都返回新串,原因就在这里。 第五段 id 一模一样,说明 append 是原地改。 这张表解释了三件事:第 3 章传列表被改、第 5 章 set 查得快、字典键为什么只能用不可变类型。 第六段最后那句要看清:(5,) 是元组,(5) 是整数。那个逗号不能省。

30 秒实验

  1. 把演示第一段的 --mode turbo 改成 --mode fast,看退出码变成几。
  2. 在演示第二段的 mymod.py 里把 print('模块被加载了…') 挪进 if 里面,再跑一次「被 import」, 看那行还在不在。
  3. 把演示第五段的 L.append(4) 改成 L = L + [4],打印 id,看这次变不变。

常见误区

  1. 「if __name__ == '__main__' 是固定写法,抄上就行。」它防的是「被 import 时跑起整个程序」。 不写的话,别人拿你的函数用,会连你的主程序一起跑。
  2. 「s.replace('a', 'x') 会把 s 改掉。」不会。字符串不可变,它返回新串。 想留住结果必须 s = s.replace(...)。 字符串所有方法都是这个脾气。
  3. 「元组就是不能改的列表,没什么用。」它的价值恰恰在「不能改」:传给函数不怕被改,能当字典的键。 单元素元组别忘了那个逗号。
自测三问(点开对答案) 1. `argparse` 遇到参数错误时退出码是几?为什么不是 1? —— 是 2。1 留给了「程序运行中出错」,2 专门表示「命令行打错了」。 这样脚本外面就能区分「是我参数没给对」还是「程序自己崩了」。 2. 一个模块里写了 `print('加载了')` 和 `if __name__ == '__main__':` 里的代码, 被 import 时哪句会执行? —— 只有 `print('加载了')` 会执行,因为它在顶层。 `if` 里面的不会执行,因为被 import 时 `__name__` 是模块名,不是 `__main__`。 所以想让某段代码「只在自己跑时执行」,就得放进那个 if 里面。 3. 为什么列表不能当字典的键,元组可以? —— 因为字典靠哈希值定位,哈希值要求「对象不会变」。 列表可变,内容一变哈希值就变,字典就找不到它了,所以 Python 直接禁止(报 unhashable type)。 元组不可变,哈希值固定,所以能当键。

第 4 章结束。下一篇进第 5 章:用 pip 装第三方套件,把程序做成一个能下载视频的命令行工具。

相关笔记