Skip to content
This page has been auto-translated and may contain errors.View in English

模块与标准库 ​

Python 自带一大批现成可用的工具:随机数、数学运算、日期、文件路径,等等。这些工具存放在**模块(module)**里,你用 import 把它们引入自己的代码。在 文件与异常 一章里,你已经用过 import json。这一章会把 import 讲透,并介绍标准库中最有用的一些部分。

Python 的标准库为常见问题提供了经过测试、有完整文档的解决方案。模块是代码组织的基本单位:每个文件就是一个模块,每个带有 __init__.py 的目录就是一个包(package)。import 系统会查找模块,必要时编译它,并把结果缓存在 sys.modules 里,因此模块只会被加载一次。

import 会真正做一些工作:Python 找到模块、运行它一次,然后把结果缓存到 sys.modules(记录所有已加载模块的字典)里,这样同一个名字的第二次 import 只是一次快速查找,而不会重新运行一遍。import requests 会把这个模块对象绑定到当前命名空间(代码执行到那一点时可见的名字表)中的 requests 这个名字上。from requests import get 做的是同样的加载过程,但只绑定 get 这一个名字。真正影响实际项目的是:Python 在哪里查找模块(sys.path)、目录是如何变成可导入的包的(__init__.py),以及如何避免循环导入——这些内容下面都会讲到。

导入模块 ​

最简单的导入方式是引入整个模块,然后用点号语法使用它里面的内容。你也可以从模块中导入特定的名字,这样使用时就不需要加前缀了。别名可以把长名字缩短。

import module 会把模块对象绑定到当前作用域里的 module 这个名字上。from module import name 则只绑定 name。使用别名(import module as alias)在第三方库中很常见。要避免使用 from module import *:它会把命名空间弄得一团糟,让人搞不清某个名字到底是从哪来的。

from module import name 并不会少做任何工作:Python 仍然会加载并运行整个模块,只是最后只绑定你要的那一个名字。这里要留意的坑是循环导入(circular import):两个模块在各自还没加载完的时候互相导入对方,结果其中一个看到的是一个只加载了一半的模块,缺了某个名字。常见的修复办法是把出问题的那个 import 移到需要它的函数内部(这样它会在两个模块都加载完之后才执行),或者把两边共用的代码抽到第三个模块里,让两边都去导入它。如果你只有在运行时才知道要导入的模块名,可以用 importlib.import_module("name"),它接受一个字符串形式的模块名。

python
import math

math.sqrt(16)     # 4.0
math.pi           # 3.141592653589793
math.floor(3.9)   # 3
math.ceil(3.1)    # 4

从模块中导入特定名字,这样就可以直接使用它们:

python
from math import sqrt, pi

sqrt(16)    # 4.0(不需要 "math." 前缀)
pi          # 3.141592653589793

给模块或名字取个别名来缩短它:

python
import math as m

m.sqrt(16)    # 4.0

from math import sqrt as square_root
square_root(25)    # 5.0

别名在常用第三方库中很常见(比如 import numpy as np、import pandas as pd)。对于标准库模块,建议使用完整名字,这样代码更易读。

Juno导入模块import math 会引入整个模块,之后你用点号语法进去用:math.sqrt(16)。from math import sqrt 只取出一个名字,这样就不用加前缀了。as 关键字可以给一个长的导入改名,但对标准库的东西,用完整名字反而读起来更清楚。
Juno导入模块import module 绑定的是模块对象,from module import name 只绑定一个名字,import module as alias 给它改个名。别用 from module import *:它会把所有名字一股脑塞进你的作用域,还让人看不出每个名字到底是从哪来的。
Juno导入模块from module import name 依然会加载整个模块,只是最后只留下一个名字。你终究会遇到的坑是循环导入:两个模块在加载途中互相导入对方——把那个 import 挪到函数内部,或者把共用代码拆到第三个模块里。当模块名只有运行时才知道时,就用 importlib.import_module()。

random ​

random 模块用来生成随机数、做随机选择。游戏、模拟、随机抽样,以及其他任何需要不可预测性的场景都可以用它。设置一个**种子(seed)**可以让结果可复现:同一个种子每次都会产生相同的序列。

random 使用的是梅森旋转(Mersenne Twister)伪随机数生成器。种子决定了整个序列;同一个种子永远会产生相同的输出。.choice() 挑一项,.choices() 是有放回地挑,.sample() 是无放回地挑。.shuffle() 会原地修改列表,返回值是 None。

random 产生的是伪随机数(pseudo-random):它们看起来是随机的,但实际上是由一个固定公式根据一个初始值(种子)算出来的,所以同一个种子在任何机器上都会得到相同的序列,这正是测试可复现的原因。生产环境中要记住一条规则:绝不要把 random 用在任何可能被人利用的地方,比如密码、会话令牌,或密码重置码。它本质上是可预测的,攻击者只要看到足够多的输出,就能猜出剩下的部分。这类场景应该改用 secrets 模块,它取用的是操作系统提供的安全随机源。如果你不想改用别的 API,random.SystemRandom() 会在熟悉的 random 方法名下给你同样的安全随机源。

python
import random

random.random()              # 0 到 1 之间的浮点数(不含 1)
random.randint(1, 10)        # 1 到 10 之间的整数(两端都包含)
random.uniform(1.0, 10.0)    # 1.0 到 10.0 之间的浮点数

colours = ["red", "green", "blue"]
random.choice(colours)       # 挑一项
random.choices(colours, k=3) # 挑 k 项(有放回)
random.sample(colours, k=2)  # 挑 k 项(无放回)

numbers = [1, 2, 3, 4, 5]
random.shuffle(numbers)      # 原地打乱,返回 None

想要结果可复现(在测试和数据科学中很有用),生成前先设置一个种子:

python
random.seed(42)
random.randint(1, 100)   # 种子为 42 时,结果永远一样

同一个种子在任何机器上每次都会产生相同的序列。

Junorandomrandom.choice() 从列表里挑一项,random.randint(1, 10) 在一个区间内给你一个整数,两端都包含在内。想每次运行结果都一样,比如写测试?先调用一下 random.seed(),序列就固定下来了。
Junorandom.choice() 挑一个,.choices() 有放回地挑,.sample() 无放回地挑。.shuffle() 原地打乱列表,返回 None,所以千万别写 nums = random.shuffle(nums)。种子固定了整个序列,这就是让一个随机测试可重复的办法。
Junorandomrandom 是伪随机的,本质上就是可预测的,所以拿来做游戏、写测试没问题,但用在任何攻击者可能利用的场景上就不合适了。令牌、密码、重置码都该用 secrets。random.seed() 留给那些你确实希望序列能重复的可复现场景。

math ​

math 模块在你于 数字 一章中学到的基本运算符之外,提供了更高级的数学运算。平方根、幂运算、对数、三角函数,以及像 pi 和无穷大这样的特殊值,都在这里。

math 提供了标准数学函数的 C 语言级实现。注意 math.pow() 永远返回 float,而 Python 的 ** 运算符在底数和指数都是整数时会返回 int。math.log(x, base) 计算任意底数的对数;math.log(x) 计算自然对数。

实际代码中最容易踩坑的是 nan("not a number" 的缩写,是像 0/0 这样未定义的运算结果,或解析失败时得到的浮点数)和 inf(无穷大)。nan 永远不等于任何东西,甚至不等于它自己,所以 x == float("nan") 永远是 False。也就是说你不能用 == 来判断它,得用 math.isnan(x),判断无穷大则用 math.isinf(x)。另一个值得保持的习惯是:比较两个浮点数时,用 math.isclose(a, b) 而不是 a == b,因为浮点数的舍入误差会让精确相等变得不可靠。如果要对整个数组的数字做数学运算,而不是逐个处理,标准工具是 numpy,不过这超出了本指南的范围。

python
import math

math.sqrt(25)        # 5.0
math.pow(2, 10)      # 1024.0(和 2 ** 10 结果一样,但永远返回 float)
math.log(100, 10)    # 2.0(以 10 为底的对数)
math.log(math.e)     # 1.0(自然对数)

math.sin(math.pi / 2)   # 1.0
math.cos(0)             # 1.0

math.ceil(3.2)    # 4
math.floor(3.9)   # 3
math.trunc(3.9)   # 3(对正数来说和 int() 一样)

math.inf          # 无穷大
math.isnan(float("nan"))   # True
math.isinf(math.inf)       # True
Junomathmath 涵盖了 + - * / 之外的一切:平方根、幂运算、对数、三角函数,还有像 math.pi 这样的常量。一旦某个计算需要用到基本运算符之外的东西,就该想到它,这样你就不用自己去写公式了。
Junomathmath 里有平方根、对数、三角函数。要注意一点:math.pow(2, 10) 永远返回 float,而 2 ** 10 在整数输入下会保持 int。math.log(x, base) 可以指定任意底数,math.log(x) 是自然对数。
Junomath 坑主要在 nan 和 inf 上:nan 永远不等于自己,所以要用 math.isnan() 而不是 == 来判断。比较浮点数也要用 math.isclose() 而不是 ==,因为舍入误差会让精确相等变得不可靠。要对整个数组做运算,那就是 numpy 的地盘了。

datetime ​

datetime 模块用来处理日期和时间。datetime.now() 给你当前的日期和时间。strftime() 把它格式化成字符串。strptime() 把字符串解析成 datetime。timedelta 表示一段可以加减的时长。

datetime、date 和 timedelta 是主要的几个类。strftime() 用格式代码把 datetime 格式化成字符串。strptime() 按给定的格式模式解析字符串。timedelta 支持算术运算:你可以对日期做加减,也可以用 <、>、- 来比较 datetime。

生产环境中的一个坑是:普通的 datetime.now() 是**naive(无时区)的:它没有绑定时区,所以并不真正知道自己代表的是世界上哪个地方的时间。来自不同机器的两个 naive datetime 看起来可以比较,但实际上并不能,这种模糊性会导致各种"差几个小时"的诡异 bug。解决办法是使用aware(带时区)**的 datetime(也就是携带时区信息的那种),并且规定在系统之间存储或传递的一律统一为 UTC:datetime.now(timezone.utc)。只在最后展示给用户看的那一刻才转换成本地时间。要处理像 "Europe/Oslo" 这样的命名时区,用 zoneinfo 模块,它会自动帮你处理夏令时切换。还有一条规则:绝不要用 datetime.now() 的差值来测量经过的时间,因为系统时钟可能会跳变(比如时间同步或手动修改);测量时长要用 time.perf_counter(),这是一个只会往前走的计数器。

python
from datetime import datetime, date, timedelta

now = datetime.now()             # 当前日期和时间
today = date.today()             # 只有当前日期

print(now.year, now.month, now.day)
print(now.hour, now.minute, now.second)

# 格式化
print(now.strftime("%Y-%m-%d"))           # "2024-01-15"
print(now.strftime("%d %B %Y, %H:%M"))   # "15 January 2024, 09:42"

# 解析
deadline = datetime.strptime("2024-12-31", "%Y-%m-%d")

# 算术运算
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(weeks=1)
diff = deadline - now
print(f"{diff.days} days until deadline")

常用的 strftime 代码:

代码含义示例
%Y4 位数年份2024
%m月份(补零)01
%d日(补零)15
%H小时(24 小时制)09
%M分钟42
%B完整月份名称January
Junodatetimedatetime.now() 给你当前的日期和时间。strftime() 用类似 %Y-%m-%d 这样的代码把它变成字符串,strptime() 则反过来,把字符串解析回去。timedelta 是一段可以加减的时长,所以 today + timedelta(days=1) 就是明天。
Junodatetimedatetime、date 和 timedelta 是核心。strftime() 用代码来格式化,strptime() 用对应的模式来解析。日期之间可以做算术运算:两个日期相减得到一个 timedelta,加上一个 timedelta 就能前移或后移。
Junodatetime 普通的 datetime.now() 是 naive 的,没有时区,一旦涉及两台机器就会悄悄出问题。存储和传递时统一用 UTC,datetime.now(timezone.utc),只在展示时才转成本地时间,命名时区就靠 zoneinfo。测量耗时要用 time.perf_counter(),绝不要用 now() 的差值,因为系统时钟会跳变。

os 与 pathlib ​

pathlib 是处理文件路径的现代方式。Path 对象让你可以用 / 运算符来构建、检查和遍历路径。os 提供了对环境变量以及更底层操作系统操作的访问。新代码建议优先使用 pathlib。

pathlib.Path 把文件系统路径表示成带有查询和遍历方法的对象。/ 运算符可以干净地拼接路径片段,自动处理不同操作系统的分隔符差异。os.environ 是一个类似字典的对象,用来访问环境变量;对于可能不存在的变量,用 os.environ.get("KEY", "default") 是安全的做法。

Path 会自动适配所在的操作系统,所以同一份代码在任何机器上都能用正确的分隔符拼接路径:写 Path("data") / "file.csv",而不要手写 "data/" + name 这种在 Windows 上会出问题的字符串拼接。这里有两个实用的注意点。第一,.glob()、.rglob() 和 .iterdir() 返回的是生成器(generator)(它们逐个产出条目,而不是一次性构建完整列表),所以如果你需要循环两次或者提前知道数量,就用 list() 把它包起来;对于超大目录,惰性遍历本身正是它的意义所在。第二,有些老一点的库仍然要求传入普通字符串而不是 Path 对象;当某个库拒绝接受 Path 时,传 str(path) 就能解决问题。读取环境变量时,永远用 os.environ.get("KEY", "default"),而不要用 os.environ["KEY"],因为下标形式在变量未设置时会抛出 KeyError,而这恰恰是新部署环境中很常见的情况。

python
from pathlib import Path

p = Path("data/reports")

p.exists()           # 路径存在则为 True
p.is_dir()           # 是目录则为 True
p.is_file()          # 是文件则为 True

p.mkdir(parents=True, exist_ok=True)   # 创建目录

for f in p.glob("*.csv"):              # 目录中所有的 CSV 文件
    print(f.name)                      # 文件名

report = p / "report_jan.csv"          # / 运算符拼接路径
report.stem       # "report_jan"(不含扩展名的文件名)
report.suffix     # ".csv"
report.parent     # Path("data/reports")

content = report.read_text()           # 直接读取文件内容
report.write_text("new content\n")    # 直接写入

再来看 os 模块:

python
import os

os.getcwd()                        # 当前工作目录
os.listdir(".")                    # 列出目录内容
os.path.exists("data.txt")        # 路径存在则为 True
os.path.join("data", "file.txt")  # "data/file.txt"(跨平台)
os.environ.get("HOME")            # 读取一个环境变量

新代码建议优先使用 pathlib。需要用到环境变量,或是要对接期望字符串的老 API 时,再用 os。

Junoos 与 pathlibpathlib.Path 把文件路径当成一个可以摆弄的对象:.exists()、.read_text()、.write_text()、.glob()。/ 运算符用来拼接路径片段,所以 p / "report.csv" 读起来就像一条真正的路径。新代码里,用 pathlib 代替老旧的 os.path。
Junoos 与 pathlibPath 用 / 来拼接,并自动帮你处理操作系统的分隔符,路径因此保持可移植。os.environ 是类似字典的环境变量入口,os.environ.get("KEY", "default") 是安全的读取方式,不会因为缺少某个键而报错。
Junoos 与 pathlib 用 Path 和 / 来构建路径,别用在 Windows 上会出问题的字符串拼接。.glob() 之类的方法返回的是生成器,需要循环两次的话就用 list() 包起来。读环境变量用 os.environ.get(key, default):中括号形式一旦碰到变量未设置就会抛出 KeyError,而这恰好是新部署时的常见情况。

timeit ​

timeit 用来测量一段代码运行所需的时间。当你想比较两种写法、挑出更快的那个时,它很有用。要多次运行代码,才能得到稳定的测量结果。

timeit.timeit(stmt, setup, number) 通过把 stmt 运行 number 次来计时,返回总共花费的秒数。setup 字符串会在计时循环开始之前运行一次。把结果除以 number 就能得到单次调用的时间。重复次数越多,来自系统调度的噪音就越小。

timeit 是做**微基准测试(microbenchmark)**的合适工具:单独为一小段代码计时,多次运行让调度噪音被平均掉。它会替你压低这种噪音,但单次运行仍然不可靠,所以要取多次重复中的最好成绩,而不是只看一个数字。要避免的错误用法是拿它去查整个程序中的性能瓶颈:它只会给你手动传进去的那段代码计时,没法告诉你到底哪个函数才是真正慢的地方。要找瓶颈,应该用 cProfile,它会运行你的整个程序,报告时间都花在了哪里。经验法则是:用 timeit 比较两种实现,用 cProfile 先找出瓶颈所在。

python
import timeit

# 给单条语句计时
timeit.timeit("sum(range(1000))", number=10000)

# 给更复杂的一段代码计时
setup = "data = list(range(1000))"
code = "[x * 2 for x in data]"
time = timeit.timeit(code, setup=setup, number=10000)
print(f"{time:.4f} seconds for 10,000 runs")

number 表示要重复运行的次数。重复次数越多,测量结果就越稳定。

Junotimeittimeit.timeit() 通过一遍又一遍地运行一段代码来计时,然后你用总时间除以运行次数,得到一个稳定的单次耗时。它是用来确定"这两种写法哪个更快",而不是靠猜。运行一次太不稳定,让它多跑几遍才靠得住。
Junotimeittimeit.timeit(stmt, setup, number) 把 stmt 运行那么多次,返回总秒数;setup 字符串会在循环开始前运行一次。除以 number 得到单次调用的耗时,把重复次数调高一些能压掉调度带来的噪音。
Junotimeittimeit 用来做微基准测试:比较两小段代码,多次运行取最好成绩,而不是信一次运行的噪音结果。它没法告诉你真实程序里哪个函数慢,因为它只会给你喂进去的那段代码计时。用 cProfile 找瓶颈,再用 timeit 比较修复方案。

string ​

string 模块提供了预先构建好的字符串常量,涵盖字母、数字和标点符号。当你需要检查字符,或者从特定字符集中生成随机字符串时,这个模块很有用。

string 模块中的常量(ascii_letters、digits、punctuation)都是普通字符串,你可以对它们做索引、遍历,或用 in 判断。把它们和 random.choices() 结合起来,是生成随机令牌或密码的标准做法。

这些常量都是普通字符串,所以每次执行 char in string.ascii_letters 都会扫描整个字符串。这个扫描是 O(n) 的:耗时随字符串长度增长,偶尔用一次没问题,但放在紧凑的循环里就很浪费。如果要反复做成员检查,就先用它构建一个 set(letters = set(string.ascii_letters)),再对这个集合做判断,因为集合的查找无论条目多少都能保持高速。这里还有一个值得知道的工具是 string.Template,它做的是简单的 $name 替换:当模板来自用户输入或配置文件时,用它比把不可信的文本直接交给 f-string 或 str.format() 更安全,因为它没办法访问你的变量或调用方法。

python
import string

string.ascii_lowercase   # "abcdefghijklmnopqrstuvwxyz"
string.ascii_uppercase   # "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
string.ascii_letters     # 两者的组合
string.digits            # "0123456789"
string.punctuation       # 所有标点符号字符

当你需要检查字符,或者生成随机字符串时会很有用:

python
import string, random

chars = string.ascii_letters + string.digits
password = "".join(random.choices(chars, k=12))
Junostringstring 模块给你现成的字符集:string.ascii_letters、string.digits、string.punctuation。它们都是普通字符串,所以可以遍历,也可以从中抽样。每当你需要一个已知的字符集来构建或做校验时,它就很方便。
Junostringstring.ascii_letters、string.digits 这些都是普通字符串,可以索引、遍历,或用 in 判断。把它们和 random.choices() 搭配起来,就是生成随机令牌或密码的标准套路。注意 random 不适合用在真正的机密场景上。
Junostring 这些常量是普通字符串,所以 char in string.ascii_letters 每次调用都会扫描整个字符串:如果是在循环里反复检查,就先建一个 set。而当模板来自用户输入时,用带 $name 语法的 string.Template 比 f-string 更安全,因为它没法访问你的变量。

创建自己的模块 ​

任何一个 Python 文件都是一个模块。要在另一个文件中使用它,按文件名(不带 .py)导入即可。你可以导入整个模块,用点号语法使用它里面的内容,也可以直接导入特定的名字。

Python 导入一个模块时,会从头到尾把这个文件执行一遍,并把结果缓存到 sys.modules 里。之后再导入同一个模块,就会直接返回缓存的对象,不会重新执行文件。对于较大的项目,模块会被组织成包(package):也就是带有 __init__.py 文件的目录。

当你写 import utils 时,Python 会按顺序在 sys.path 列出的目录里查找,起点是你运行的脚本所在的文件夹。这正是最常见的"我这边能跑"式导入问题的根源:从不同的工作目录运行同一个文件,它期望的那个文件夹可能就不在路径里了,导入就会失败。可靠的解决办法是把项目当作一个包来运行(python -m mypackage.main),而不是直接指向一个孤立的文件。包就是带有 __init__.py 文件的模块目录(这个文件可以是空的;它的存在本身就是在告诉 Python:这个文件夹是可以导入的)。在一个包内部,像 from . import helpers 这样的相对导入表示"从这个包本身导入",这样即使包被重命名或移动,导入依然能正常工作。还有一个不太好把握的细节:importlib.reload() 会重新运行一个模块,但你之前已经导入的那些名字仍然指向旧的版本,所以除了在 REPL 里做点快速实验之外,重新加载并不是一个可靠的做法。

python
# utils.py
def clamp(value, lo, hi):
    return max(lo, min(value, hi))

PI = 3.14159
python
# main.py
import utils

utils.clamp(150, 0, 100)   # 100
utils.PI                    # 3.14159

from utils import clamp
clamp(50, 0, 100)           # 50

Python 会在导入方文件所在的目录(以及其他几个位置)里查找模块。对于较大的项目,模块会被组织成包:也就是带有 __init__.py 文件的目录。

Juno创建自己的模块 任何一个 .py 文件本身就已经是一个模块了:按它的文件名(去掉 .py)导入它,里面的函数和变量就都归你用了。import utils 然后 utils.clamp(...),或者用 from utils import clamp 只拿一个名字。这就是把一个越长越大的程序拆分到多个文件里的办法。
Juno创建自己的模块 导入一个模块会把它从头到尾执行一遍,然后缓存到 sys.modules 里,之后再导入就直接返回同一个对象,不会重新执行文件。正因为这样,顶层代码要写得轻一点,任何重量级的操作都会在导入的那一刻就跑起来。把相关的模块归到一个包里:一个带 __init__.py 的文件夹。
Juno创建自己的模块 导入是靠 sys.path 来解析的,起点是脚本自己所在的文件夹,这就是为什么从错误的目录运行一个孤立文件会导致导入失败。改成用包的方式运行它:python -m pkg.main。也别太信任 importlib.reload(),超出 REPL 里的快速试验就不靠谱了,因为你已经导入的名字仍然指向旧版本。

__name__ == "__main__" ​

当 Python 直接运行一个文件时,__name__ 会被设为 "__main__"。当同一个文件被作为模块导入时,__name__ 则是这个模块的名字。这个用法让你可以写这样的代码:直接执行这个文件时它才运行,被别的模块导入时则会被跳过。

if __name__ == "__main__": 是保护可执行模块代码的标准写法。它让一个模块既可以被导入(暴露它的函数),也可以被直接运行(带上测试或演示代码)。如果没有它,导入这个模块就会执行所有顶层代码,这几乎从来都不是你想要的。

对于你实际运行的那个文件,Python 会把 __name__ 设为 "__main__";而对于任何被导入的文件,__name__ 则是这个模块自己的名字。这个保护写法存在的意义,是为了防止导入时的副作用(side effects)(模块加载那一刻就会做的事情,比如解析参数、打开连接、跑一段演示)在别的文件导入你时被莫名其妙触发。经得住考验的写法是:把真正的工作放进一个 main() 函数里,在保护语句下调用它,这样导入方拿到的是干净的函数,不会带上启动流程,而这个文件自己单独运行时依然能正常工作。省掉这个保护写法,正是测试套件在导入某个模块的瞬间就莫名其妙跑起了别人写的演示代码的经典原因。

python
# utils.py
def clamp(value, lo, hi):
    return max(lo, min(value, hi))

if __name__ == "__main__":
    # 只有在这样运行时才会执行:python utils.py
    # 而在这样导入时不会执行:import utils
    print(clamp(150, 0, 100))   # 100

对于任何一个同时也想作为独立脚本使用的模块,这都是一种标准写法。

Juno__name__ == '__main__'if __name__ == "__main__": 让一个文件既能作为可导入的模块,也能作为可运行的脚本。守卫下面的代码在你执行 python utils.py 时会运行,而在别的文件 import utils 时会被跳过。很适合把一段快速的演示或测试塞在模块末尾。
Juno__name__ == '__main__' 只有在你直接运行的那个文件里,__name__ 才是 "__main__"。这个守卫防止你的演示或测试代码在模块被导入时悄悄运行,否则那种情况会毫无预警地发生。它是让一个文件同时充当库和脚本的标准做法。
Juno__name__ == '__main__' 这个守卫能阻止导入时的副作用——解析参数、演示代码、建立连接——在别人导入你时被触发。把真正的活儿放进 main(),在守卫下调用它:导入方只拿到函数,文件自己也能单独跑。省掉它,测试套件在导入模块的那一刻就会跑起你的演示代码。

标准库精选 ​

再介绍几个值得了解的模块。每一个都解决了一个常见问题,而这些问题如果自己动手实现,都得花不少功夫。

标准库内容非常丰富;下面列出的这些是你在生产代码中最常遇到的部分。要查完整的参考资料,docs.python.org/3/library 是最权威的来源。

标准库是一套经过精心筛选、测试充分、文档完善的模块集合。在动手引入第三方包之前,先看看标准库里有没有现成方案:functools、itertools、contextlib、dataclasses、typing 和 abc 各自提供的工具,常常是第三方包在重新发明的东西。

collections:一些专用的容器类型:

python
from collections import Counter, defaultdict, deque

Counter(["a", "b", "a", "c", "a"])   # Counter({'a': 3, 'b': 1, 'c': 1})
defaultdict(list)                      # 缺失键会自动创建的字典
deque([1, 2, 3], maxlen=5)            # 两端都能高效追加/弹出

itertools:处理可迭代对象的工具:

python
import itertools

list(itertools.chain([1, 2], [3, 4]))          # [1, 2, 3, 4]
list(itertools.islice(range(100), 5))          # [0, 1, 2, 3, 4]
list(itertools.combinations([1, 2, 3], 2))     # [(1, 2), (1, 3), (2, 3)]
list(itertools.product([0, 1], repeat=2))      # [(0,0), (0,1), (1,0), (1,1)]

sys:访问 Python 解释器本身:

python
import sys

sys.argv        # 命令行参数列表
sys.exit(1)     # 以某个状态码退出
sys.version     # Python 版本字符串

第三方包:除了标准库,pip 还能安装社区提供的包:

bash
pip install requests    # HTTP 库
pip install pandas      # 数据处理
pip install numpy       # 数值计算

第三方包超出了本指南的范围,但套路始终一样:先 pip install,再 import。

实战演练 ​

结合 random、string 和 datetime,生成带时间戳的唯一游戏 ID:

python
import random
import string
from datetime import datetime

def generate_game_id(length: int = 8) -> str:
    chars = string.ascii_uppercase + string.digits
    return "".join(random.choices(chars, k=length))

def timestamp() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

game_id = generate_game_id()
print(f"[{timestamp()}] Starting game {game_id}")

scores = [random.randint(50, 100) for _ in range(5)]
print(f"Round scores: {scores}")
print(f"Best: {max(scores)}")

用 pathlib 和 datetime 在一个目录中查找文件,并报告它们的大小:

python
from pathlib import Path
from datetime import datetime

def find_files(directory: str, pattern: str = "*.csv") -> list[Path]:
    return sorted(Path(directory).glob(pattern))

def timestamp() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

files = find_files(".", "*.md")[:3]
print(f"[{timestamp()}] Found {len(files)} file(s)")
for f in files:
    size = f.stat().st_size if f.exists() else 0
    print(f"  {f.name} ({size} bytes)")

从环境变量中读取应用配置(带类型化的默认值),并把结构化的访问日志写成换行分隔的 JSON:

python
import os
import json
from datetime import datetime
from pathlib import Path

def load_env_config() -> dict:
    return {
        "debug": os.environ.get("DEBUG", "false").lower() == "true",
        "port": int(os.environ.get("PORT", "8080")),
        "log_level": os.environ.get("LOG_LEVEL", "INFO"),
    }

def write_access_log(method: str, path: str, status: int) -> None:
    log_dir = Path("logs")
    log_dir.mkdir(exist_ok=True)
    entry = {
        "ts": datetime.now().isoformat(),
        "method": method,
        "path": path,
        "status": status,
    }
    with open(log_dir / "access.jsonl", "a") as f:
        f.write(json.dumps(entry) + "\n")

config = load_env_config()
print(f"Starting on port {config['port']}, debug={config['debug']}")
write_access_log("GET", "/users", 200)

换行分隔的 JSON(.jsonl)是一种常见的日志格式:每一行都是一个合法的 JSON 对象,这让逐行流式读取、追加和解析都变得很简单,不需要把整个文件都加载进来。