前言
「开始真正编程」这一步,跨的不是语法,而是心态:从「照着示例敲、跑出同样结果」变成「有个需求,自己想清楚结构,把它变成能运行的代码」。很多人卡在这里——教程都看得懂,一合上就写不出来。
需要先说明一点前提:早期的《跟老齐学Python》系列是按Python 2.7写的,而Python 2.7 已于 2020 年 1 月 1 日停止维护。作者后来出了面向 Python 3 的新版(《跟老齐学Python:轻松入门》),并明确说「以前的版本是 Python 2,本书则为 Python 3」。所以本文一律按 Python 3 讲;如果你手上是旧版材料,凡是出现print "x"、raw_input()、xrange()的地方,都要按本文的写法改写。
本文要解决的是:拿到一个真实需求之后,一个 Python 程序应该长什么样——怎么拆函数、怎么组织入口、怎么处理出错、怎么自己排查。代码以 Python 3.8 及以上为基准,只用标准库。
一、先想结构,再动手
拿到需求别急着敲第一行。先回答三个问题:
- 输入是什么、输出是什么?(例如:输入一个目录和一个关键字,输出匹配到的文件清单)
- 中间有哪几步?(扫描 → 筛选 → 排序 → 输出)
- 每一步失败会怎样?(目录不存在?没有权限?没有匹配?)
这三个问题想清楚,代码的主干就出来了。反之,边想边写通常写出一堆嵌在for里的if。
二、把每一步写成一个函数
「一个函数只做一件事」是让代码可测、可改的最有效纪律。仍然以「在目录里按关键字找文件」为例:
# 适用于 Python 3.8+
from pathlib import Path
def iter_files(root):
"""递归产出 root 下所有普通文件的路径。"""
root = Path(root)
for path in root.rglob("*"):
if path.is_file():
yield path
def match_by_name(paths, keyword):
"""筛出文件名中包含 keyword 的路径。"""
keyword = keyword.lower()
for path in paths:
if keyword in path.name.lower():
yield path
def summarize(paths):
"""把路径按大小升序排列后返回列表。"""
return sorted(paths, key=lambda p: p.stat().st_size)注意每个函数都只负责一步,并且用「生成器」把「产出」与「消费」解耦:iter_files和match_by_name用yield惰性产出,summarize才真正把它们收集起来。这样即使目录里有几十万文件,也不需要在内存里存两份列表。
三、组织入口
脚本的最外层只做「串联 + 处理失败」,主体逻辑都放进函数:
# 适用于 Python 3.8+
def find(root, keyword):
paths = match_by_name(iter_files(root), keyword)
return summarize(paths)
def main(argv):
if len(argv) != 3:
print("用法:python find.py 目录 关键字")
return 2
root, keyword = argv[1], argv[2]
if not Path(root).is_dir():
print(f"目录不存在:{root}")
return 1
results = find(root, keyword)
if not results:
print("没有匹配的文件")
return 0
for path in results:
print(f"{path.stat().st_size:>10} {path}")
return 0
if __name__ == "__main__":
import sys
raise SystemExit(main(sys.argv))三个关键点:
main(argv)接收参数、返回退出码。这样 main 本身是可测的,不用真的启动进程。if __name__ == "__main__":是入口守卫。有了它,这个文件被别的模块import时不会自动跑一遍,只有直接运行时才执行。raise SystemExit(main(...))把返回码交给进程。返回0表示成功,非零表示失败,这是命令行工具的通用约定。
四、失败要说得清楚
初学者常把异常直接抛给用户,看到一大段 Traceback 完全不知道哪错了。正确做法是在边界处(最外层)把底层异常翻译成人能看懂的话:
# 适用于 Python 3.8+
def safe_find(root, keyword):
try:
return find(root, keyword)
except PermissionError as err:
print(f"没有权限访问:{err.filename}")
return []
except OSError as err:
print(f"文件系统错误:{err}")
return []规则是:只捕获你能处理的异常,并且给出可操作的提示。不要写裸except:,那会把 Ctrl-C 触发的KeyboardInterrupt也吞掉,程序变得无法中断。
五、调试:从读懂报错开始
真实编程中大部分时间花在排查上。几个够用的手段:
- 读 Traceback:从最后一行往回读,最后一行是错误类型与原因,往上找
File "...", line N定位到自己写的代码。 print加探针:在怀疑的分支打印变量,注意用repr()看真实内容(能暴露多余空格和换行)。assert做断言:assert len(results) >= 0, "结果集异常",用于表达「这里本应成立」。- 小步验证:一次只改一处,改完立刻运行,不要攒一大坨再一起调。
# 适用于 Python 3.8+
def inspect_keyword(raw):
"""排查用户输入里肉眼看不见的字符。"""
print("原始值:", repr(raw))
print("长度:", len(raw))
print("去空格后:", repr(raw.strip()))
return raw.strip()实战:一个能直接用的文件查找工具
把上面各段拼起来,构成一个完整脚本:
# 适用于 Python 3.8+
"""按关键字查找文件:python find.py 目录 关键字"""
import sys
from pathlib import Path
def iter_files(root):
for path in Path(root).rglob("*"):
if path.is_file():
yield path
def match_by_name(paths, keyword):
keyword = keyword.lower()
for path in paths:
if keyword in path.name.lower():
yield path
def summarize(paths):
return sorted(paths, key=lambda p: p.stat().st_size)
def find(root, keyword):
return summarize(match_by_name(iter_files(root), keyword))
def main(argv):
if len(argv) != 3:
print("用法:python find.py 目录 关键字")
return 2
root, keyword = argv[1], argv[2]
if not Path(root).is_dir():
print(f"目录不存在:{root}")
return 1
try:
results = find(root, keyword)
except PermissionError as err:
print(f"没有权限访问:{err.filename}")
return 1
if not results:
print("没有匹配的文件")
return 0
for path in results:
print(f"{path.stat().st_size:>10} {path}")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))运行方式:python find.py . py(在当前目录找名字含py的文件)。它只用标准库,跨平台可用。
常见坑点
- 所有代码堆在文件顶层
❌ 几十行代码平铺在模块里,没有函数 ✅ 按步骤拆函数,逻辑集中,便于单独验证
- 入口守卫写成别的花样
❌if __name__ == "__main__"漏了冒号,或写成单下划线_name_✅ 固定写if __name__ == "__main__":;两边各两个下划线,且必须有冒号
- 把库函数名当变量名用
❌list = [1, 2]、sum = 5、id = "x"覆盖内置名字 ✅ 换名,如numbers = [1, 2];内置名字留作函数调用
- 不处理异常,脚本直接崩
❌ 目录不存在时抛FileNotFoundError一大段栈 ✅ 在边界处捕获,打印可操作的提示并返回非零退出码
- 用裸
except:吞掉一切
❌try: ... except: pass连 Ctrl-C 都拦下 ✅ 明确捕获ValueError/OSError等具体类型
print调试后忘了删
❌ 交付的脚本里到处是调试输出 ✅ 调试完清理,或改用logging模块按级别控制
- 照旧教程写 Python 2 语法
❌print "结果"、raw_input()、xrange(n)✅ Python 2.7 已于 2020-01-01 停止维护;用print(...)、input()、range(n)
- 硬编码路径与魔法数字
❌ 把/Users/xxx/data和if n > 17:直接写死 ✅ 路径用参数或pathlib拼接,常量提取成有名字的变量
总结
| 环节 | 做法 |
|---|
| 需求 | 先定输入、输出、步骤、失败情况 |
| 拆函数 | 一个函数一件事,用生成器解耦产出与消费 |
| 入口 | main(argv)返回退出码,配if __name__ == "__main__": |
| 错误 | 只捕具体异常,在边界翻译成人话 |
| 调试 | 读 Traceback 最后一行;repr()看真实值;assert表达不变量 |
| 版本 | 一律 Python 3;旧教程的 2.7 写法要改写 |
「真正编程」的标志不是写出多复杂的代码,而是写出的代码别人(包括三个月后的自己)能改。做到这一点的路径很朴素:把逻辑拆成职责单一的函数、把失败处理放在边界、把入口收拾干净。从今天起,每写一个小工具都按这个骨架来,很快就成了习惯。