跳到主要内容

7. 输入与输出

对应官方文档:7. Input and Output 一句话导读:这一章解决「程序怎么把话说清楚、怎么把数据存下来」——从格式化输出一路讲到文件与 JSON,学完能让程序既会漂亮地打印,也能把结果落盘复用。

前面几章我们一直在 print(),其实程序对外说话的方式不止一种:表达式语句、print()、文件对象的 write() 都算。这一章村长按 7.1、7.2 两大块走,7.1 下面四个小节讲「怎么排版」,7.2 下面两个小节讲「怎么存文件」。凡是涉及真实文件的示例,统一用 tempfile 建临时目录,复制粘贴就能跑,不会在你硬盘上留下垃圾。

7.1 更漂亮的输出格式​

先分清两个老朋友:str() 给人看,返回「可读」的版本;repr() 给解释器看,返回「带引号、带转义」的版本。格式化时想选哪个,可以用 !s、!r、!a 显式指定(下面 f 字符串里会用到)。官方一共给了四套排版办法,我们按推荐程度从新到旧挨个讲。

7.1.1 f 字符串(格式化字符串字面量)​

在字符串前面加一个 f,里面用花括号 {} 嵌表达式,Python 会当场求值并转成字符串。这是 3.6 之后最推荐的做法:短、直观、还能直接写运算。花括号里 : 后面跟格式规范,! 后面跟转换方式,= 是 3.8 起自带的「调试打印」写法。

name = '村长'
age = 18
print(f'{name} 今年 {age} 岁') # -> 村长 今年 18 岁
print(f'调试用:{age=}, {age * 2=}') # -> 调试用:age=18, age * 2=36
print(f'repr 转换:{name!r}') # -> repr 转换:'村长'
pi = 3.1415926
print(f'保留两位:{pi:.2f},靠右 10 位:{pi:>10.2f}')
print(f'千分位:{1234567:,},带前缀十六进制:{255:#x}')
print(f'居中:{"hi":^10}|') # -> 居中: hi |

格式规范是套小语言,常用的记这几条就够了:

写法含义例子结果
:10 / :>10占 10 位,数字右对齐f'{42:>5}' 42
:<10 / :^10左对齐 / 居中f'{"a":^5}' a
:.2f保留两位小数f'{3.14159:.2f}'3.14
:,千分位分隔f'{1000000:,}'1,000,000
:b / :x / :e二进制 / 十六进制 / 科学计数f'{10:b}'1010
!r / !s / !a用 repr / str / ascii 转换f'{"a"!r}''a'

宽度和精度还能用另一对花括号动态传进去,比如 f'{value:{width}.{precision}f}',写报表时特别顺手。

村长划重点:优先用 f 字符串,别再 + 拼一堆 str() 了。记一个口诀:! 管「转成什么字符串」,: 管「排成什么样子」,= 管「连名字一起打出来」。

7.1.2 str.format() 方法​

f 字符串之前的正统做法:字符串里写 {} 占位,调用 .format() 把值填进去。它俩共用同一套格式规范,所以 : 后面那些 :>10、:.2f 完全一样。占位符可以写位置编号、写名字,还能用索引和中括号取容器里的元素。

print('{}, {} and {}'.format('a', 'b', 'c')) # -> a, b and c
print('{0} {1} {0}'.format('a', 'b')) # -> a b a(0 号可复用)
print('{name} 约等于 {value:.2f}'.format(name='pi', value=3.14159))
print('{:>10} | {:<10}'.format('right', 'left'))
print('{0!r:^12}'.format('hi')) # 先 repr 再居中
table = {'Sjoerd': 4127, 'Jack': 4098}
print('Jack: {0[Jack]:d}; Sjoerd: {0[Sjoerd]:d}'.format(table))

位置参数和关键字参数可以混着用,但位置参数必须排在前面。这招在「格式串要复用、或者动态拼出来」的场景里仍比 f 字符串方便,因为 {} 可以留空之后再填。

7.1.3 手工字符串格式化​

不想学格式规范也行,字符串自带三个对齐方法:rjust(width) 右对齐、ljust(width) 左对齐、center(width) 居中,缺的位用空格补(可以传第二个参数换填充字符)。它们不会截断:内容比 width 还长就原样返回。数字补零另有 zfill(width),它会认得开头的正负号,把零插到符号后面。

for x in range(1, 4):
print(str(x).rjust(3), str(x * x).rjust(4), str(x ** 3).rjust(5))
print('12'.zfill(5), '-3.14'.zfill(8), '3.14'.zfill(8))
print('abc'.ljust(6) + '|', 'abc'.center(8) + '|')
print('7'.rjust(4, '*'))
1 1 1
2 4 8
3 9 27
00012 -003.14 0003.14
abc | abc
***7

现在基本只在拼接简单表格时用到它们,日常排版交给 f 字符串就好。

7.1.4 老式 % 格式化​

最老的写法是从 C 的 printf 借来的:字符串里放 %s、%d、%f 之类占位,右边用 % 接一个值或元组。官方把它放在最后,意思很明确——这是遗留写法,新代码别用,但读老项目、老日志配置时还得认得。

import math
print('The value of pi is approximately %5.3f.' % math.pi)
print('%s has %d items, %5.2f%% done' % ('cart', 3, 62.5))
print('%(name)s = %(value).2f' % {'name': 'pi', 'value': 3.14159})

注意字面上想输出一个百分号得写 %%。它不支持 f 字符串那些动态表达式,类型写错还会直接抛 TypeError,所以除了维护旧代码,没有理由再用它。

7.2 读写文件​

open(filename, mode, encoding=...) 返回一个文件对象,模式决定你能干什么:

模式含义
r只读(默认),文件不存在会报错
w只写,先清空原文件,不存在则创建
a追加写,保留原内容
r+可读可写
b二进制模式,如 rb / wb,和上面的组合用

默认以文本模式打开,读写的是 str;加 b 就变成 bytes。文本模式牵扯编码,官方建议显式写 encoding='utf-8',别依赖平台默认值(Windows 中文环境常是 GBK,换个机器就乱码)。最重要的是:处理文件一律用 with,它会在块结束时自动 close(),哪怕中途抛异常也不漏。

村长划重点:with open(...) 是铁律。手动 f.close() 一旦被异常跳过,缓冲区里的数据可能没写进去,文件句柄也会一直占着。另外 'w' 是先清空,误用一次数据就没了,写之前确认模式。

7.2.1 文件对象的方法​

常用方法就这几个:read(size) 一次读 size 个字符(省略就读到末尾)、readline() 读一行、readlines() 读成列表、write(s) 写入并返回写入的字符数、tell() 报告当前位置、seek(offset, whence) 移动位置、close() 关闭。文件对象本身还能直接 for 逐行遍历,这是最省内存的写法。

import tempfile, os

with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, 'demo.txt')
with open(path, 'w', encoding='utf-8') as f:
print('write 返回写入字符数:', f.write('第一行\n第二行\n'))
with open(path, 'r', encoding='utf-8') as f:
print('read():', repr(f.read()))
with open(path, 'r', encoding='utf-8') as f:
print('readline():', repr(f.readline()))
print('tell():', f.tell())
f.seek(0)
print('seek(0) 后再读:', repr(f.readline()))
with open(path, 'r', encoding='utf-8') as f:
print('readlines():', [line.strip() for line in f.readlines()])
with open(path, 'r', encoding='utf-8') as g:
print('逐行遍历:', [line.strip() for line in g])
print('with 结束后自动关闭:', g.closed)
with open(path, 'a', encoding='utf-8') as f:
print('追加一行', file=f) # print 也能直接写进文件
with open(path, 'r', encoding='utf-8') as f:
print('追加后:', repr(f.read()))

seek(offset, whence) 的 whence 默认是 0(相对文件开头),1 表示相对当前位置、2 表示相对文件末尾;文本模式下只有 seek(0, 2) 这类相对末尾的用法比较稳,随意偏移容易落到多字节字符中间。想处理图片、音频这类非文本,就用 rb / wb 读写 bytes。

7.2.2 用 json 保存结构化数据​

字符串 write() 进去、read() 出来还是字符串,列表和字典得自己拼、自己解析,太麻烦。标准库 json 模块专治这个:把 Python 的 dict / list / str / int / float / bool / None 转成 JSON 字符串,再转回来。

  • json.dumps(obj) → 转成字符串;json.loads(s) → 从字符串还原。
  • json.dump(obj, f) → 直接写进文件对象;json.load(f) → 从文件对象读出来。
import json, tempfile, os

data = {'name': '村长', 'tags': ['Python', '教程'], 'count': 3, 'ok': True}
s = json.dumps(data, ensure_ascii=False)
print('序列化:', s)
print('反序列化:', json.loads(s))
print('排版后:\n' + json.dumps(data, ensure_ascii=False, indent=2))
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, 'data.json')
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False)
with open(path, 'r', encoding='utf-8') as f:
print('从文件读回:', json.load(f))

村长划重点:默认 ensure_ascii=True 会把中文转义成 \uXXXX,想让文件里直接看得见汉字就写 ensure_ascii=False。另外 JSON 不是「什么都能存」:元组会被转成列表,自定义类、日期、集合都塞不进去,得先自己转成基础类型;真要存任意 Python 对象,那属于 pickle 的活(但它有安全风险,别拿来读不可信数据)。

本章小结​

  • 输出优先用 f 字符串:f'{expr}' 嵌表达式,!r / !s 选转换,:>10、:.2f、:, 管排版,= 管调试打印。
  • str.format() 用 {} 占位、支持编号与命名并共用同一套格式规范;rjust / ljust / center / zfill 做手工对齐且不会截断;% 是遗留写法,认识即可。
  • 文件用 open(path, mode, encoding='utf-8') 打开,r / w / a / r+ / b 决定读写方式;read / readline / readlines / write / seek / tell 是核心方法。
  • 一律用 with 自动关闭文件,忘了 close() 可能丢数据;文本模式显式指定编码,'w' 会先清空文件要当心。
  • json.dumps / loads 处理字符串,dump / load 处理文件;加 ensure_ascii=False 保留中文,且只支持基础类型,元组会变列表。