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

模块和标准库

docs.scrimba.com

Python 自带了大量可以立即使用的工具:随机数、数学、日期、文件路径等等。这些工具存在于模块中,你可以用 import 将它们引入到代码中。在文件和异常章节中你已经使用过 import json。本章节完整介绍导入的用法,并介绍标准库最有用的部分。

Python 的标准库为常见问题提供了经过测试、文档完善的解决方案。模块是代码组织的单位:每个文件是一个模块,每个包含 __init__.py 的目录是一个包。import 系统查找模块,在需要时编译它们,并将它们缓存在 sys.modules 中,所以它们只被加载一次。

一次 import 会执行真实的工作:Python 定位模块,运行一次,然后将结果缓存在 sys.modules(所有已加载模块的字典)中,这样第二次导入同名模块时只是一次快速查找,而不会重新运行。import requests 将模块对象绑定到你当前命名空间(代码该点可见的名称表)中的名称 requestsfrom requests import get 执行相同的加载但只绑定 get。真实项目中回报最高的是 Python 搜索的位置(sys.path)、目录如何变成可导入的包(__init__.py),以及如何避免循环导入,所有这些内容都在下面介绍。

导入模块

最简单的导入将整个模块引入,让你可以用点号来使用它的内容。你也可以从模块中导入特定的名称,直接使用它们而无需前缀。别名可以缩短长名称。

import module 将模块对象绑定到当前作用域中的名称 modulefrom module import name 只绑定 name。别名(import module as alias)在第三方库中很常见。避免 from module import *:它会污染命名空间,使得不清楚名称来自哪里。

from module import name 不会跳过任何工作:Python 仍然会加载和运行整个模块,然后只绑定你要求的那个名称。需要了解的陷阱是循环导入,其中两个模块在加载时都试图导入对方,所以其中一个会看到一个半建成的模块,某个名称会丢失。通常的修复方法是将有问题的 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 npimport 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 模块生成随机数并做出随机选择。在游戏、模拟、随机采样以及任何需要不可预测性的地方使用它。设置一个种子会使结果可重复:相同的种子每次都产生相同的序列。

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

random 产生伪随机数:它们看起来很随机,但来自由一个起始值(种子)驱动的固定公式,所以相同的种子会在每台机器上给出相同的序列,这正是使测试可重复的原因。在生产中很重要的规则:永远不要将 random 用于任何人可能利用的东西,比如密码、会话令牌或密码重置代码。它在设计上是可预测的,所以看到足够多的输出的攻击者可以猜测其余部分。对于这些情况,改用 secrets 模块,它从操作系统的安全随机源中获取。如果你不想切换 API,random.SystemRandom() 会在熟悉的 random 方法名称后面给你同样的安全源。

python
import random

random.random()              # 0 到 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 的 ** 运算符对整数底数和指数返回 intmath.log(x, base) 计算任意底数的对数;math.log(x) 计算自然对数。

在实际代码中遇到的细节是 nan("不是数字"的缩写,从未定义的结果如 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 对整数输入保持为 intmath.log(x, base) 可以接受任意底数,math.log(x) 是自然对数。
Junomath 陷阱是 naninfnan 永远不等于自己,所以用 math.isnan() 测试,不要用 ==。用 math.isclose() 比较浮点数而不是 ==,因为舍入会使精确相等不可靠。对于整数数组,那是 numpy 的地盘。

datetime

datetime 模块处理日期和时间。datetime.now() 给你当前的日期和时间。strftime() 将其格式化为字符串。strptime() 将字符串解析为 datetime。timedelta 表示你可以加或减的时长。

datetimedatetimedelta 是主要的类。strftime() 使用格式代码将 datetime 格式化为字符串。strptime() 给定一个格式模式解析字符串。timedelta 支持算术:你可以将时长加或减于日期,用 <>- 比较 datetime。

生产中的陷阱是一个简单的 datetime.now()朴素的:它没有附加时区,所以它实际上不知道它代表世界的哪一部分。来自不同机器的两个朴素 datetime 看起来可比但实际上不是,这种歧义会导致偏移数小时的错误。修复方法是使用感知的 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小时(24h)09
%M分钟42
%B完整月份名称January
Junodatetimedatetime.now() 给你当前的日期和时间。strftime()%Y-%m-%d 之类的代码把它转成字符串,strptime() 反过来,把字符串解析回来。timedelta 是一个时长,你可以加或减,所以 today + timedelta(days=1) 就是明天。
Junodatetimedatetimedatetimedelta 是核心。strftime() 用代码格式化,strptime() 用匹配的模式解析。日期可以做算术:两个日期相减得到 timedelta,向日期加一个 timedelta 来向前或向后移动。
Junodatetime 一个简单的 datetime.now() 是朴素的,没有时区,这在涉及两台机器时会安静地破坏。用 datetime.now(timezone.utc) 存储和传递 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() 返回生成器(它们一次产生一项而不是构建完整列表),所以如果你需要循环两次或提前知道计数,把它们包装在 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 and pathlibpathlib.Path 把文件路径当作对象来处理:.exists().read_text().write_text().glob()。`/` 运算符连接片段,所以 p / "report.csv" 读起来像一个真实路径。在新代码中优先使用 pathlib 而不是较老的 os.path
Junoos and pathlibPath/ 连接并为你处理操作系统分隔符,所以路径保持可移植。os.environ 像字典一样用于环境变量,os.environ.get("KEY", "default") 是安全的读取方式,不会因缺失的键而崩溃。
Junoos and pathlibPath/ 构建路径,永远不要胶合字符串,在 Windows 上会破坏。.glob() 和它们的伙伴返回生成器,如果你需要循环两次,就 list() 它们。用 os.environ.get(key, default) 读取环境变量:括号形式在变量未设置时会引发 KeyError,这正是新部署的情况。

timeit

timeit 测量代码需要多长时间才能运行。当你想比较两种方法并选择更快的一种时,它很有用。多次运行代码以获得稳定的测量。

timeit.timeit(stmt, setup, number) 通过运行 stmt number 次并返回经过的总时间(以秒为单位)来计时。setup 字符串在计时循环之前运行一次。将结果除以 number 来获得每次调用的时间。更多重复会减少系统调度中的噪声。

timeit微基准的正确工具:计时一个小代码片段,隔离地运行多次,以便调度噪声平均化。它为你减少了那些噪声,但单次运行仍然不可靠,所以取多个重复的最佳值而不是一个数字。要避免的错误是使用它来找到整个程序中的慢速:它只计时你交给它的片段,所以它不能告诉你哪个函数实际上很慢。对于那种情况,使用 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_lettersdigitspunctuation)是纯字符串,你可以索引、迭代或用 in 测试。将它们与 random.choices() 结合是生成随机令牌或密码的标准方式。

这些常数是普通字符串,所以检查 char in string.ascii_letters 每次都扫描整个字符串。那个扫描是 O(n):工作随字符串长度而增长,一次没问题,在紧密循环中浪费。如果你重复测试成员关系,首先构建一个 setletters = set(string.ascii_letters))并检查它,因为集合查找无论它包含多少条目都保持快速。这里另一个有用的部分是 string.Template,它做纯 $name 替换:当一个模板来自用户输入或配置文件时,它比把不可信的文本交给 f 字符串或 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_lettersstring.digitsstring.punctuation。它们是纯字符串,所以你可以循环或从中采样。无论何时你需要一个已知的字母表来构建或检查,都很方便。
Junostringstring.ascii_lettersstring.digits 和伙伴们是你可以索引、迭代或用 in 测试的纯字符串。将它们与 random.choices() 配对,你已经有了随机令牌或密码的标准配方。注意 random 对于真实的秘密不安全。
Junostring 常数是纯字符串,所以 char in string.ascii_letters 每次调用都扫描整个东西:如果你在循环中检查,构建一个 set 一次。当一个模板来自用户输入时,string.Template$name 比 f 字符串更安全,因为它不能到达你的变量。

创建你自己的模块

任何 Python 文件都是一个模块。要从另一个文件中使用它,用文件名(不含 .py)导入它。你可以导入整个模块并用点号使用它的内容,或直接导入特定的名称。

当 Python 导入一个模块时,它从上到下执行文件一次,并将结果缓存在 sys.modules 中。相同模块的后续导入返回缓存的对象而不重新运行文件。对于较大的项目,模块被组织成:带 __init__.py 文件的目录。

当你写 import utils 时,Python 按顺序搜索 sys.path 中的目录,从你运行的脚本的文件夹开始。这是最常见的"在我的机器上工作"导入错误的根源:从不同的工作目录运行一个文件,它期望的文件夹可能不在路径上,所以导入失败。可靠的修复是作为包运行项目(python -m mypackage.main)而不是指向 Python 一个松散的文件。一个是一个带 __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 文件的目录。

JunoCreating your own modules 任何 `.py` 文件已经是一个模块:用不含 `.py` 的文件名导入它,它的函数和变量就是你的了。import utils 然后 utils.clamp(...),或用 from utils import clamp 拉一个名称。这就是你如何跨文件分割一个增长的程序的方式。
JunoCreating your own modules 导入一个模块从上到下运行一次,然后缓存在 sys.modules 中,所以后续导入返回相同的对象而不重新运行文件。保持顶级代码轻量,因为那个原因,任何重工作在导入时运行。将相关模块分组成包:一个带 __init__.py 的文件夹。
JunoCreating your own modules 导入通过 sys.path 解析,从脚本自己的文件夹开始,这就是为什么从错误的目录运行一个松散的文件会破坏导入。改为作为包运行它:python -m pkg.main。并且不要相信 importlib.reload() 超越快速 REPL 戳,因为你已经导入的名称保持指向旧版本。

__name__ == "__main__"

当 Python 直接运行一个文件时,__name__ 被设置为 "__main__"。当同一个文件被导入为一个模块时,__name__ 是模块名称。这个模式让你写代码,当你直接执行文件时运行,但当文件被另一个模块导入时被跳过。

if __name__ == "__main__": 是可执行模块代码的标准保护。它让一个模块既可导入(暴露其函数)又可直接运行(带测试或演示代码)。没有它,导入模块会执行任何顶级代码,这几乎从不是想要的。

Python 为你实际运行的文件设置 __name__"__main__",为任何导入的东西设置为模块自己的名称。保护存在是为了防止导入时副作用(代码在模块加载时做什么,如解析参数、打开连接或运行演示)在另一个文件导入你时立即激发。保持的模式:把真实工作放在 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 是权威来源。

标准库是经过精心策划的一套经过充分测试、文档完善的模块。在转向第三方包之前,检查标准库是否有解决方案:functoolsitertoolscontextlibdataclassestypingabc 各提供第三方包经常重新实现的工具。

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

实践中

结合 randomstringdatetime 生成带时间戳的唯一游戏 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)}")

使用 pathlibdatetime 在目录中查找文件并报告它们的大小:

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 对象,这使得流、追加和逐行解析而不加载整个文件很直接。