你在终端里运行一个程序,屏幕上只闪过一句“请输入金额:”,然后就不动了。你输入 18.5,按下回车,程序又打印出“应付:18.50 元”。这几秒钟里,其实已经发生了一次完整的交流:程序先把提示写到输出流,再从输入流读回一行文字,接着把文字转换成数字,完成计算,最后把结果整理成适合人阅读的样子。
刚开始学 Python 时,我们很容易把这件事缩成两个函数:输出用 print(),输入用 input()。这样记能写出第一个程序,却不够应付后面的真实场景。进度提示为什么有时迟迟不出现?明明输入了 12,为什么不能直接和整数相加?同一个字符串用 str() 和 repr() 看起来为什么不一样?脚本放进自动任务后,input() 为什么突然收到 EOF?这些问题都不是 Python 在故意刁难你,而是输入和输出背后一直有“文本、类型、流和格式”这几层规则在工作。
这一章就从终端出发,把一条数据从“人敲下键盘”到“程序处理完并显示或保存”的路线走完整。你会学会控制 print() 的分隔、结尾、目标位置和刷新时机;用 f-string 做清楚而稳定的格式化;把 input() 得到的文字安全地转换并验证;也会看到交互式输入、管道输入、命令行参数和文件输出各自适合什么场景。
这一章谈的是文本输入与文本输出。终端里看到的数字、日期和列表,最后都要以字符的形式显示;键盘输入的数字也先以字符串进入程序。把“值本身”和“值的文本表示”分开,是理解本章所有细节的主线。

print() 不只是把内容放到屏幕上,还能控制参数之间如何分隔、行尾如何处理、数据写到哪里,以及是否立即刷新缓冲区。
先看最熟悉的一行:
print("订单已创建")它默认把文字写到标准输出,也就是 sys.stdout。在普通终端里,标准输出通常连接着屏幕,所以我们会看到:
订单已创建“标准输出”听起来有点抽象,你可以先把它理解成程序默认的出声通道。这个通道通常通向终端,也可以被操作系统重定向到文件,或者接到另一个程序的输入上。print() 并不负责决定文字最终显示在哪里;它只把文本写进当前指定的文本流。
print() 可以一次接收多个对象:
product = "手账本"
quantity = 3
unit_price = 12.5
print("商品", product, "数量", quantity, "单价", unit_price)输出是:
商品 手账本 数量 3 单价 12.5这里有两步默认动作。第一步,每个对象都按 str() 的方式转成文字;第二步,这些文字之间用一个空格连接。最后,print() 再补上换行符 \n。所以它的常用参数可以读成这样:
print(*objects, sep=" ", end="\n", file=None, flush=False)*objects 表示可以放零个、一个或多个要输出的对象。sep、end、file 和 flush 是关键字参数,调用时要写出参数名。不要把它们误当成普通输出内容:
# 错误理解:这只会把 "、" 也当成一个输出对象
print("语文", "数学", "、")
# 正确:指定对象之间的分隔符
print("语文", "数学", sep="、")sep 只在多个对象之间出现。只有一个对象时,改变 sep 看不出效果:
print("2026", "08", "13", sep="-")
print("A", "B", "C", sep="")
print("只有一个对象", sep="|")输出:
2026-08-13
ABC
只有一个对象end 决定这次输出结束时追加什么。默认的 "\n" 会换行,改成空字符串就会让下一次输出接在后面:
print("下载中", end="")
print("...", end="")
print("完成")输出在同一行:
下载中...完成如果你想用空格、箭头或别的文字收尾,也可以直接设置:
print("姓名", end=":")
print("林晓")print() 不带任何输出对象时,也不是“什么都不做”。它仍然会写出 end,所以 print() 本身就是输出一个空行。这个细节在分隔报告区块时很方便。
sep 和 end 必须是字符串或 None。写 sep=1、end=False 都会报 TypeError。如果传入 None,Python 会采用该参数的默认值,而不是把单词 None 打出来。
默认的 file=None 会使用标准输出。只要目标对象提供接收字符串的 write() 方法,print() 也能把内容写过去。最常见的目标就是文本文件:
with open("receipt.txt", "w", encoding="utf-8") as report:
print("商品:手账本", file=report)
print("数量:3", file=report)
print("应付:37.50 元", file=report)这段代码运行时,三行内容不会出现在屏幕上,而会写入 receipt.txt。注意文件以文本模式打开,因为 print() 写的是字符串。二进制文件接收的是 bytes,不能直接拿来当 print(file=...) 的目标。
有时我们还想把错误消息和正常结果分开。标准错误 sys.stderr 就是另一条文本通道:
import sys
print("处理完成:28 条记录")
print("警告:2 条记录缺少邮箱", file=sys.stderr)这样做的好处是,使用者把正常输出重定向到文件时,警告仍然可以单独显示或单独收集。
为了减少频繁写入的开销,输出流可能先把文字放在缓冲区里,攒到合适时机再真正送出去。在交互式终端中,标准输出通常按行缓冲,遇到换行时容易立即显示;但标准输出被重定向到文件或管道时,往往会按块缓冲。于是,下面这种不换行的进度提示可能暂时看不到:
import time
for step in range(1, 6):
print(f"\r处理进度:{step}/5", end="")
time.sleep(0.5)
print("\n处理完成")\r 是回车字符,会把光标移回当前行开头。可问题是,我们又把 end 设成了空字符串,迟迟没有换行。想让每一步及时出现,可以显式刷新:
import time
for step in range(1, 6):
print(f"\r处理进度:{step}/5", end="", flush=True)
time.sleep(0.5)
print("\n处理完成")flush=True 的意思是这次写完就强制把缓冲内容交给下游。它适合进度、倒计时、等待提示等“现在就必须让人看到”的输出,不需要无条件加在每一次 print() 上。
下面的实验台把 sep、end、file 和 flush 放在同一个可操作场景中。先改动选项,再观察屏幕输出、文件输出和缓冲区状态分别发生了什么。
你可能见过这种情况:在交互式解释器里直接输入一个字符串变量,结果带着引号和 \n;换成 print() 后,引号没了,\n 真的变成换行。差别来自两种表示目标。
str() 倾向于给人阅读,repr() 倾向于给开发者检查对象。我们用包含换行和制表符的字符串对比一下:

同一个对象可以有两种呈现方式:一种照顾阅读体验,另一种保留边界与转义痕迹,方便调试和排查问题。
message = "第一行\n\t第二行"
print(str(message))
print("---")
print(repr(message))输出:
第一行
第二行
---
'第一行\n\t第二行'str(message) 展示文字表达的内容;repr(message) 保留引号,并把换行和制表显示为转义序列。调试“这里到底是一个空格还是制表符”“末尾是否多了换行”时,repr() 往往更有用。
print() 接收非字符串对象时,会像 str() 那样转换它们:
data = ["北京", "上海"]
print(data)
print(str(data))
print(repr(data))对于列表,这三行看起来可能完全一样,因为有些对象没有专门面向普通读者的表示,str() 会和 repr() 使用相同或相近的结果。不要因此以为二者永远等价。字符串、日期、自定义对象都可能让这种差别变得明显。
f-string 默认按对象自己的格式化规则显示。你也可以用 !s 强制采用 str(),用 !r 强制采用 repr():
path = "资料\n归档"
print(f"给用户看:{path!s}")
print(f"给开发者看:{path!r}")开发阶段还有一个很省事的写法:在表达式后加 =,同时显示表达式文本和值。
price = 19.9
quantity = 3
print(f"{price=}, {quantity=}, {price * quantity=}")可能得到:
price=19.9, quantity=3, price * quantity=59.699999999999996这恰好也提醒了我们:调试表示负责忠实暴露对象,不保证结果像账单一样漂亮。面向用户输出金额时,仍要写明确的格式说明。
把 repr() 理解成“尽量把隐藏字符和对象边界照出来”更实用。它常用于调试和日志检查,但“能被解释器重新读取”不是对每一种对象都能兑现的保证。
把变量塞进一句话时,f-string 通常是最顺手的写法。在字符串开头加 f 或 F,再把表达式放进花括号:
name = "周宁"
price = 16
quantity = 4
print(f"{name}购买了{quantity}件商品,应付{price * quantity}元。")花括号里不只可以放变量,也可以放运算、索引和方法调用。Python 会先求出表达式的值,再把结果格式化成文字:
items = ["铅笔", "橡皮", "尺子"]
print(f"第一件商品:{items[0]}")
print(f"商品总数:{len(items)}")
print(f"大写编号:{'a-17'.upper()}")不过,能写复杂表达式不等于应该写。下面两种写法都能运行,第二种更容易检查:
price = 18.8
quantity = 3
discount = 0.9
# 能运行,但一行承担了计算和展示两件事
print(f"实付:{price * quantity * discount:.2f} 元")
# 先给结果命名,再展示
total = price * quantity * discount
print(f"实付:{total:.2f} 元")当公式再长一点时,先计算再格式化能减少括号写错,也便于给计算结果写测试。
f-string 的常见结构是:
{表达式!转换方式:格式说明}!r、!s 属于转换方式;冒号后的内容负责宽度、对齐、填充、符号、精度和显示类型。你不必一次背下全部符号,先掌握最常用的组合:

f-string 的格式说明可以拆成几件小事理解:选择变量值,再决定显示宽度、对齐方向与数字精度。
name = "订书机"
stock = 27
price = 8.5
rate = 0.126
print(f"{name:<10}") # 左对齐,最小宽度 10
print(f"{stock:>10d}") # 十进制整数,右对齐
print(f"{price:>10.2f}"
输出字段的“宽度”是最小宽度,不是截断长度。字符串比指定宽度更长时,Python 会完整输出,而不会悄悄剪掉后半截:
print(f"{'超长商品名称':<4}")仍会完整显示“超长商品名称”。
f-string 用单个花括号标记表达式。如果你就是想输出字面量花括号,要把它写成两次:
name = "周宁"
print(f"配置写法:{{'name': '{name}'}}")输出:
配置写法:{'name': '周宁'}初学时常见的 SyntaxError,不少就是少写了一个右花括号,或者把普通字符串忘记加 f:
name = "周宁"
print("你好,{name}") # 不报错,但只会原样输出 {name}
print(f"你好,{name}") # 才会替换变量如果对齐符、宽度和精度挤在一对花括号里让你眼花,可以在下面的实验室逐项调整。每次只改一个条件,再对照生成的 f-string 和收据效果,格式说明会更容易记住。
格式化最容易学偏的地方,是只记住 .2f,却不知道每个符号在解决什么。我们用一张终端账单把常见规则串起来。
.2f 表示用定点小数形式显示,并保留小数点后两位;, 可以添加千位分隔;% 会把值乘以 100 后再加百分号:
subtotal = 1234567.5
discount_rate = 0.085
saved = subtotal * discount_rate
print(f"原价:{subtotal:,.2f} 元")
print(f"折扣率:{discount_rate:.1%}")
print(f"节省:{saved:,.2f} 元")输出:
原价:1,234,567.50 元
折扣率:8.5%
节省:104,938.24 元这里的精度控制的是“怎么显示”,不会把 subtotal 或 saved 变量本身改掉:
value = 3.1415926
text = f"{value:.2f}"
print(text) # 3.14
print(value) # 3.1415926
print(type(text)) # <class 'str'>格式化结果一定是字符串。需要继续做数学运算时,应使用原来的数字,而不是拿显示文本再转回去。
浮点数在计算机中通常是二进制近似值,所以某些十进制计算会露出很长的尾巴。.2f 只负责把结果按两位小数呈现,不会自动把浮点运算变成精确的财务计算。对金额精度有严格要求时,应在后续课程中使用 decimal.Decimal,不要把“看起来是两位小数”误当成“内部已经精确到分”。
终端报告有时需要明确显示正负变化:
change = 18
temperature = -3.5
print(f"库存变化:{change:+d}")
print(f"温度变化:{temperature:+.1f}")输出:
库存变化:+18
温度变化:-3.5整数还可以用 b、o、x 显示二进制、八进制和十六进制。加上 # 会保留进制前缀:
permission = 42
print(f"二进制:{permission:#b}")
print(f"八进制:{permission:#o}")
print(f"十六进制:{permission:#x}")固定宽度编号常用零填充:
order_id = 37
print(f"订单号:{order_id:06d}")输出 订单号:000037。这里的 6 是字段最小宽度,0 是补零要求,d 表示十进制整数。
< 表示左对齐,> 表示右对齐,^ 表示居中。符号后面的数字是最小字段宽度,符号前还可以放一个填充字符:
print(f"{'商品':<10}{'数量':>6}{'金额':>12}")
print("-" * 28)
rows = [
("记事本", 2, 25.0),
("彩色铅笔", 12, 46.8),
("文件收纳盒", 1,
这能在等宽字体的终端中排出规整列。你也可以写 {title:-^20},让标题居中并用短横线填满字段。
不过,中文字符在不同终端、字体和宽度计算规则下不一定和拉丁字母占相同显示宽度。简单演示通常没问题,正式报表若要求严格列对齐,最好改用表格库或导出 CSV,而不是不断手调数字。
格式说明由值的类型解释。数字认识 .2f,日期时间对象则认识 %Y、%m、%d、%H、%M、%S 这些日期时间代码:
from datetime import datetime
created_at = datetime(2026, 8, 13, 9, 5, 7)
print(f"创建日期:{created_at:%Y-%m-%d}")
print(f"创建时间:{created_at:%H:%M:
输出:
创建日期:2026-08-13
创建时间:09:05:07
完整记录:2026年08月13日 09:05%m 是补零后的月份,%M 才是分钟;大小写写错会得到完全不同的内容。涉及月份名称、星期名称时,结果还可能受系统区域设置影响。如果你要得到跨环境稳定的年月日数字格式,%Y-%m-%d 这样的纯数字组合更稳妥。
现在换到输入端。input() 会先显示可选的提示文字,而且提示末尾不会自动换行;然后它从标准输入读取一行,去掉这一行末尾的换行符,把剩余内容作为字符串返回。

用户输入首先是文本;只有通过类型转换和业务范围检查,才算得到程序真正能使用的数据,否则就要回到输入环节重试。
name = input("请输入姓名:")
print(name)
print(type(name))无论用户输入“林晓”、27 还是 3.14,返回类型都是 str。终端看起来像数字,只是因为字符串内容刚好由数字字符组成。
来看一个非常典型的坑:
quantity = input("购买数量:")
print(quantity + "1")输入 2,输出会是 21,因为两个字符串执行的是拼接。若直接写 quantity + 1,还会因为字符串和整数不能相加而报 TypeError。
需要计算时,要明确转换:
quantity_text = input("购买数量:")
quantity = int(quantity_text)
print(f"再加一件后共有 {quantity + 1} 件")常见转换可以这样选:
int(text):十进制整数,例如 "42"、"-7"。float(text):带小数或科学计数法的数,例如 "3.5"、"1e3"。“看起来像数字”不能作为转换依据。手机号 "013800000000" 一旦转成整数,开头的零就丢了;学号也不应该拿来加减乘除。
input() 只去掉行末换行,不会自动去掉输入两端的空格:
raw_name = input("姓名:")
name = raw_name.strip()strip() 会去掉两端空白,适合处理姓名、选项等普通文本。数字转换函数本身可以接受两端空白,但提前 strip() 能让空输入判断更清楚:
raw_age = input("年龄:").strip()
if raw_age == "":
print("年龄不能为空")
else:
age = int(raw_age)如果用户输入 "十二"、"3.5" 或 "10岁",int() 会抛出 ValueError。尤其要注意,int("3.0") 也不成立:这个字符串不是整数写法。不要为了绕过错误随手写 int(float(text)),因为它会把 "3.9" 变成 3,悄悄改变用户输入的含义。
还有一个很隐蔽的转换坑:
raw = input("是否继续(yes/no):")
answer = bool(raw)只要 raw 不是空字符串,bool(raw) 就是 True。因此 bool("False")、bool("no") 都是 True。解析“是/否”应该比较允许的文字,而不是调用 bool():
answer = input("是否继续(yes/no):").strip().lower()
is_continue = answer in {"yes", "y"}只写 int(input(...)) 的程序,用户敲对时很简洁,敲错时却会直接结束并显示异常。真实交互更适合把“读取、转换、检查范围、失败后重试”组成一个循环。
以年龄为例,"abc" 无法转换是格式错误,-3 能转换成整数但不在允许范围内,是业务规则错误。这两种情况应该给不同提示:
while True:
raw = input("请输入年龄(0~150):").strip()
try:
age = int(raw)
except ValueError:
print("请输入整数,例如 18。")
continue
if not 0 <= age <= 150:
print("年龄必须在 0 到 150 之间。")
continue
break
print这个循环可以按四步读:
先用 input() 取得原始字符串,并清理两端空白。保留原始输入能让我们明确知道“用户写了什么”。
在尽量小的 try 代码块里调用 int()。这里只捕获 ValueError,避免把其他编程错误也误报成“用户输入错了”。
转换成功后再检查范围。格式合法不代表业务上可接受,年龄、价格、数量都有各自的限制。
两层检查都通过才 break。循环结束时,age 已经是可用整数,后面的代码不必再猜它是否合法。
多个地方都要读取正整数时,可以写一个函数。函数要么返回满足条件的值,要么继续提示,不要一会儿返回字符串、一会儿返回整数:
def read_positive_int(prompt):
while True:
raw = input(prompt).strip()
try:
value = int(raw)
except ValueError:
print("请输入整数。")
continue
if value <= 0:
print("请输入大于 0 的整数。")
continue
return value
选项输入也适合用集合限定:
def read_delivery_method():
choices = {"1": "自提", "2": "快递", "3": "同城配送"}
while True:
raw = input("配送方式(1 自提 / 2 快递 / 3 同城配送):").strip()
if raw in choices:
return choices[raw]
print("请输入 1、2 或 3。")
method = read_delivery_method()
这里没有使用 eval(),也没有让程序“猜”用户的意思,而是把允许值写成清楚的映射。输入范围越明确,程序行为越容易解释和测试。
下面的流程模拟器会把原始字符串、转换结果、范围检查、重试和 EOF 分支逐步摆出来。可以故意输入空白、文字、小数和越界整数,看看每一种输入在哪一步被拦住。
在终端里调用 input(),我们习惯了程序停下来等人按回车。但标准输入不一定连接键盘,也可能来自文件、管道、自动评测器、定时任务或已经关闭的输入流。
input() 读到输入结束而没有新内容时,会抛出 EOFError。在 macOS 或 Linux 终端中,空行状态下按 Control-D 可以发出 EOF;Windows 终端常见的是 Control-Z 后再按回车。更重要的是,当脚本在非交互环境中运行时,EOF 可能无需人工按键就立即到来。

输入不一定来自真人敲键盘,也可能来自文件或管道;无论来源是什么,程序都需要知道何时遇到输入结束并停止等待。
用户直接按回车,input() 会返回 "";输入流结束,则会抛出 EOFError。这两件事必须分开处理:
try:
name = input("姓名:").strip()
except EOFError:
print("\n没有可读取的输入,程序结束。")
else:
if name == "":
print("姓名不能为空。")
else:
print(f"你好,{name}。")空字符串表示“读到了一行,只是这一行没有内容”;EOF 表示“已经没有下一行可以读”。如果把两者混为一谈,程序可能在输入耗尽后无限重试。
上一节的验证函数在纯交互终端里很好用,但放到自动任务中,如果输入用完,就应该结束或交给上层处理,而不是假装用户还会再输一次:
def read_quantity():
while True:
try:
raw = input("数量:").strip()
except EOFError:
print("\n输入已结束,取消本次操作。")
return None
try:
quantity = int(raw)
except ValueError:
print("请输入整数。")
continue
返回 None 是这个小程序约定的“没有得到数量”。更大的程序也可以选择重新抛出异常、返回错误状态或直接退出,关键是把策略写明白。
假设 sum_price.py 从标准输入读取两个数字:
price = float(input("单价:"))
quantity = int(input("数量:"))
print(f"{price * quantity:.2f}")在交互终端里,提示语很友好。可如果把两行数据通过管道送进程序,提示文字也会混进标准输出,给后续程序解析带来麻烦。面向人和面向机器的输出要求不同:
要连续处理直到 EOF,可以直接遍历 sys.stdin:
import sys
total = 0.0
for line_number, line in enumerate(sys.stdin, start=1):
text = line.strip()
if text == "":
continue
try:
total += float(text)
except ValueError:
print(f"第 {line_number} 行不是数字:
这种程序不会逐行询问用户,而是把标准输入看成一批数据。它既能读取重定向文件,也能接收上一个命令的输出。
不要写一个捕获 EOFError 后立刻 continue 的无限循环。输入流一旦结束,下一次 input() 通常仍然只会得到 EOF,程序会空转。遇到 EOF 应退出循环、返回特殊结果,或者把异常交给调用者。
input() 适合程序运行到某一步时再询问。另一些工具需要在启动时一次拿到参数,例如:

实用脚本常把命令行参数交给程序处理,再将正式结果写入文件,同时把运行提示单独留在终端。
python total.py 19.9 3Python 会把命令行参数放在 sys.argv 列表中。第一个元素 sys.argv[0] 通常是脚本名,后面的元素才是用户提供的参数,而且它们依然都是字符串:
import sys
print(sys.argv)执行:
python show_args.py apple 12通常会看到:
['show_args.py', 'apple', '12']不要直接读取 sys.argv[1],否则缺少参数时会抛出 IndexError。一个基础版本可以这样写:
import sys
if len(sys.argv) != 3:
print("用法:python total.py <单价> <数量>", file=sys.stderr)
raise SystemExit(2)
try:
price = float(sys.argv[1])
quantity = int(sys.argv[2])
except ValueError:
print("错误:单价应为数字,数量应为整数。",
这段代码仍然遵守同一条路线:命令行先提供字符串,程序再转换、验证、输出。SystemExit(2) 让错误调用得到非零退出状态,方便外部脚本判断任务失败;正常结果独占标准输出,错误说明走标准错误。
参数多起来以后,手动检查 sys.argv 会很繁琐。标准库里的 argparse 能生成帮助信息、处理可选参数并完成基础类型转换。这里只要先掌握 sys.argv 的字符串本质;等你开始写正式命令行工具,再把参数解析交给 argparse。
终端会先按自己的规则切分命令,再把结果交给 Python。想把“彩色 铅笔”当成一个参数,需要加引号:
python label.py "彩色 铅笔"引号通常由终端解释,不会成为 sys.argv[1] 内容的一部分。不同操作系统和 shell 的引用规则可能有细节差异,因此涉及路径和空格时,要在目标环境中实际测试。
程序调试时把结果打印到屏幕很方便,任务稳定后,我们常常希望保留一份报告。因为 print() 面向的是文本流,把输出目标从终端换成文件只需要调整 file。
下面把同一份订单同时显示在终端并写入文件:
items = [
("记事本", 2, 12.5),
("彩色铅笔", 1, 18.8),
]
with open("order-report.txt", "w", encoding="utf-8") as report:
print("订单明细", file=report)
print("-" * 30
with 代码块结束后,文件会被正确关闭;即使写入过程中发生异常,也会执行必要的清理。文本文件显式写 encoding="utf-8",能减少程序换一台机器后中文乱码的概率。
文件模式选错,损失可能比格式错位严重:
"w" 表示写入。如果同名文件已经存在,原内容会被清空。"a" 表示追加。新内容写到文件末尾,适合持续记录。"r" 表示只读,不能作为写入报告的模式。"b" 的模式是二进制模式,读写的是字节,不是本章的普通字符串。写日志式记录时可以用追加:
from datetime import datetime
with open("run.log", "a", encoding="utf-8") as log_file:
now = datetime.now()
print(f"{now:%Y-%m-%d %H:%M:%S} 任务完成", file=log_file)如果程序每次都应该产生一份全新报告,使用 "w" 合理;如果历史不能丢,就用 "a",或者先生成带日期时间的唯一文件名。不要在没想清楚的情况下机械照抄 "w"。
文件对象也有 write() 方法:
with open("note.txt", "w", encoding="utf-8") as file:
file.write("第一行\n")
file.write("第二行\n")write() 不会自动添加换行,而且参数必须已经是字符串;print() 会把对象转成文字,能用 sep 和 end,默认还会换行。输出自然语言报告时,print(..., file=file) 往往更省心;需要精确控制每个字符时,write() 更直接。
打开重要文件前先确认模式。open(path, "w") 会清空已有内容。教学示例可以写临时报告,真实项目中要先设计备份、唯一命名或原子替换策略,不能把“成功打开文件”误认为“安全写入文件”。
有人会发现,eval(input()) 好像很省事:用户输入 1 + 2,它直接得到 3;输入 [1, 2],它直接得到列表。问题是,eval() 不是“智能类型转换”,而是在执行 Python 表达式。只要输入来自不可信用户,就等于把执行代码的权力交了出去。
下面这种写法不要使用:
# 危险示例:不要运行,不要用于处理用户输入
result = eval(input("请输入算式:"))
print(result)即使你只希望收到加减乘除,用户输入也可能访问程序环境、调用函数或执行其他表达式。给 eval() 传一个看似受限的全局字典,也不能把它变成可靠的安全沙箱。
如果只需要一个整数,就用 int();需要小数就用 float();需要选择操作符,就写白名单分支:
left = float(input("第一个数:"))
operator = input("运算符(+、-、*、/):").strip()
right = float(input("第二个数:"))
operations = {
"+": lambda a, b: a + b,
"-": lambda a, b: a - b,
"*": lambda a, b: a * b,
程序只接受字典中列出的四个操作符。输入规则可能多写几行,却非常清楚:哪些内容能通过、通过后做什么,都由代码决定。
如果确实需要解析由字符串表示的 Python 字面量,标准库有 ast.literal_eval(),它只接受有限的字面量和容器结构,比 eval() 收敛得多。但对普通的新手输入场景,优先选择 JSON、CSV 或自己定义的简单格式;格式越明确,错误提示越容易写好。
现在把前面的知识合在一起。我们做一个终端结算工具:既支持启动时传入单价和数量,也允许没有参数时交互输入;结果同时显示并可选择保存。代码不追求功能多,而是把输入、验证、格式化和输出位置分开。
import sys
from datetime import datetime
def parse_price(text):
try:
price = float(text)
except ValueError:
raise ValueError("单价必须是数字。")
if price < 0:
raise ValueError("单价不能为负数。")
return price
def parse_quantity
这段程序有几处值得带走的习惯:
parse_price() 和 parse_quantity() 只负责把字符串变成符合规则的值。make_report() 只负责组织文本,因此同一份报告既能打印,也能写文件。with 代码块中。main() 返回状态码,程序入口再把它交给 SystemExit。你不必马上照着这种结构写所有小练习。但当一个脚本从五行长到五十行时,把“读取”“解析”“业务计算”“呈现”拆开,会比在一个巨大的 try 里塞满所有代码更容易排错。
下面的程序想让用户输入是否保存。用户输入 no 时,为什么仍然会打印“已保存”?
save = bool(input("是否保存(yes/no):"))
if save:
print("已保存")下面的代码在输入结束后有什么问题?请让它在 EOF 时安静退出。
while True:
try:
line = input()
except EOFError:
continue
print(line.upper())请把下面的数据打印成三列:姓名左对齐、分数右对齐且保留一位小数、通过率显示一位百分比。标题下方输出 26 个短横线。
records = [
("林晓", 88.5, 0.92),
("周宁", 100, 1.0),
("陈一鸣", 76.25, 0.8),
]写一个循环计算器,要求如下:
+、-、*、/,不得使用 eval()。g 格式,避免无意义地固定显示很多个零。最后回到开头那次终端交流:print() 把对象转换成文本并写向某个流,input() 从标准输入读回一行字符串,转换和验证决定这行文字能否成为程序需要的值,f-string 再负责把结果整理给人看。屏幕、管道、命令行参数和文件看似是不同入口与出口,其实都遵循同一条原则:先弄清楚数据从哪里来、当前是什么类型、允许什么内容,再决定用什么格式把它送到哪里。
格式宽度是最小宽度。若姓名可能很长,真实程序还要决定是否换行、截断或改用更适合的表格输出方式。