字符串、变量与输入输出
Fundamentals II: Strings, Variables and I/O
程序要和人打交道:读进来的东西怎么处理,算出来的结果怎么呈现。这一讲还会引入函数,它是这门课里第一件用来对付复杂度的工具。
本讲结束后你应当能
- 说清楚赋值语句到底做了什么,并用「名字指向值」的模型解释 id() 和 is
- 用 f-string 把变量嵌进输出,控制小数位和对齐
- 处理 input() 返回的字符串,并知道转换失败时会发生什么
- 定义带参数和返回值的函数
- 分清 print() 和 return 的区别,不再混用
本页目录
变量
赋值做了什么
x = 10
print(x)
x = 20 # 同一个名字,指向新的值
print(x)
= 读作”把右边的值,绑定到左边的名字上”。它不是数学里的等号。数学里 x = x + 1 是个假命题,程序里却完全合法,意思是”把 x 现在的值加 1,再绑回 x”。
count = 5
count = count + 1 # 先算右边 5+1=6,再绑给 count
print(count)
命名规则
变量名必须以字母或下划线开头,后面可以跟字母、数字、下划线。区分大小写,age 和 Age 是两个变量。
不能用 Python 的关键字(if、for、class、return 这些)。
import keyword
print(keyword.kwlist)
比规则更重要的是习惯:
- 用有意义的名字。
student_count比n好,n比x好 - 多个单词用下划线连接:
max_score、user_name(这叫 snake_case,是 Python 的约定) - 不要用
l(小写 L)、O(大写 o)单独做变量名,它们和1、0长得太像
变量到底是什么
内存可以想象成一长条格子,每个格子有一个编号,这个编号叫地址。数据存在格子里,程序靠地址找到它。
x = 10 做的是两件事:把 10 放进内存的某个位置,然后让 x 这个名字指向那个位置。
变量不是盒子,而是一个贴在值上的名字。 很多人把变量想象成一个盒子,里面装着值,赋值就是换掉盒子里的东西。这个比喻在只有数和字符串时不会出错,但到了 L5 的列表就会把人带偏。
正确的模型是一个箭头:名字 → 值。x = 20 把 x 这个箭头改指到了另一个格子,原来那个 10 并没有被换掉。
id() 告诉你一个名字当前指向的值存在哪个地址:
x = 10
print(id(x))
y = x # 让 y 指向同一个地方
print(id(y))
print(x is y) # is 问的是「同一个吗」
y = x 之后两个 id 一样,说明赋值复制的是箭头,不是值本身。内存里只有一个 10,两个名字都指着它。
== 和 is 不是一回事
== 问「值相不相等」,is 问「是不是同一个对象」:
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # 内容一样
print(a is b) # 但不是同一个
(方括号那个写法叫列表,L5 讲。这里借它说明问题,因为数和短字符串上看不出区别。)
两个列表内容完全一样,所以 == 是 True;但它们是分别造出来的两个对象,在内存里占两块地方,所以 is 是 False。就像两张一模一样的复印件:内容相同,不是同一张纸。
这一节现在只需要留个印象。L5 讲列表时会把它彻底讲透,那一讲一大半内容是这个模型的直接后果。
同步赋值
等号左右都可以是用逗号隔开的多项,数量对上就行:
a, b = 7, 3
print(a, b)
# 右边先整个算完,再一一对应绑过去
# 所以交换两个变量不需要中间变量
a, b = b, a
print(a, b)
关键是执行顺序:右边先整体求值,然后才一一绑给左边的名字。 所以 a, b = b, a 里,右边先取到当前的 b 和 a(3 和 7),再绑回去,交换就完成了。
在 C、Java 里交换两个变量需要一个临时变量。这个写法后面会反复出现:函数返回多个值时用它接住,L4 的循环里配 enumerate 时也用它。
复合赋值
x = 10
x += 3 # 等价于 x = x + 3
print(x)
x -= 5
x *= 2
x //= 3
print(x)
+=、-=、*=、/=、//=、%=、**= 都可以这么用。写法更短,而且左边的名字只出现一次,改名字的时候不容易漏。
注释
# 后面到行末的内容 Python 会跳过,是写给人看的。
# 这一行是注释
x = 3.14 # 也可以写在语句后面
print(x)
好的注释解释为什么,而不是复述代码在做什么:
# 差:把 x 加一
x += 1
# 好:跳过表头那一行
x += 1
字符串
三种引号
a = 'single'
b = "double"
c = """三个引号
可以跨行
写多行文本"""
print(a, b)
print(c)
单引号和双引号完全等价。有一个实用差别:字符串里要用哪种引号,外面就用另一种,省得写转义。
print("他说:'你好'")
print('She said "hello"')
拼接与重复
first = "Shanghai"
second = "Jiao Tong"
print(first + " " + second) # + 拼接
print("-" * 30) # * 重复
print(first * 2)
长度、索引与比较
s = "Python"
print(len(s)) # 长度
print(s[0]) # 第一个字符,下标从 0 开始
print(s[-1]) # 最后一个
print(s[2:5]) # 切片,含头不含尾
字符串的比较是按字典序,逐个字符比 ASCII 编号:
print("apple" < "banana") # 'a' < 'b'
print("Zebra" < "apple") # 大写字母编号更小
print("abc" < "abd") # 前两个相同,比第三个
print("abc" < "abcd") # 前缀更短的在前
字符串是不可变的,不能改其中某个字符:
s = "hello"
s = s.upper() # 这是生成新字符串,再绑给 s
print(s)
s = "hello"
s[0] = "H" # 这个会报错
字符串的完整方法在 L6 集中讲,这里够用了。
类型转换
type() 给出一个值的类型:
print(type(42))
print(type(3.14))
print(type("42"))
print(type(True))
int()、float()、str() 在类型之间转换:
print(int("42") + 1) # 字符串 → 整数
print(float("3.14") * 2) # 字符串 → 浮点数
print(str(42) + "!") # 数 → 字符串
print(int(3.99)) # 浮点数 → 整数,直接截断,不是四舍五入
print(int(-3.99)) # 负数也是往零的方向截断
转换失败会抛异常:
print(int("abc"))
print(int("3.14")) # 这个也会失败,猜猜为什么
int() 只认整数形式的字符串。"3.14" 要先 float() 再 int()。
输入
input() 永远返回字符串
# 讲义里点运行会弹出输入框,试着输入一个数字
age = input("请输入你的年龄:")
print("你输入的是:", age)
print("它的类型是:", type(age))
无论输入什么,input() 拿到的都是字符串。 输入 18,得到的是 "18" 而不是 18。
这是新生第一个月最高频的错误:
n = input("输入一个数:")
print(n * 3) # 想要三倍,得到的是重复三遍
正确写法是先转换:
n = int(input("输入一个数:"))
print(n * 3)
eval():能用,但要小心
eval() 把字符串当成 Python 表达式来求值:
print(eval("1 + 2 * 3"))
print(eval("2 ** 10"))
看起来很方便:eval(input()) 就能直接读进一个数,或者一个表达式。
格式化输出
为什么需要它
要输出”张三的成绩是 87.5 分”,其中姓名和分数来自变量。用拼接写出来很啰嗦:
name = "张三"
score = 87.5
print(name + "的成绩是 " + str(score) + " 分") # 能用,但很啰嗦
f-string(这门课的主线写法)
在引号前面加一个 f,然后把变量直接写进 {} 里:
name = "张三"
score = 87.5
print(f"{name}的成绩是 {score} 分")
{} 里可以放任何表达式:
a, b = 7, 3
print(f"{a} + {b} = {a + b}")
print(f"{a} / {b} = {a / b}")
print(f"平均分:{(87 + 92 + 65) / 3}")
控制格式
{值:格式} 可以控制小数位、宽度、对齐、进制:
pi = 3.14159265
print(f"{pi:.2f}") # 保留两位小数
print(f"{pi:.4f}")
print(f"{pi:10.2f}") # 总宽度 10,右对齐
print(f"{pi:<10.2f}|") # 左对齐
print(f"{pi:^10.2f}|") # 居中
n = 255
print(f"{n:b}") # 二进制
print(f"{n:o}") # 八进制
print(f"{n:x}") # 十六进制小写
print(f"{n:X}") # 十六进制大写
m = 5
print(f"{m:b}") # 101
print(f"{m:08b}") # 补零到 8 位,对齐时很有用
# 对齐输出一张表
data = [("张三", 87.5), ("李四", 92.0), ("王五", 65.25)]
for name, score in data:
print(f"{name:<6}{score:>8.1f}")
调试时有个很好用的写法:{变量=} 会同时打印名字和值。
x = 42
y = [1, 2, 3]
print(f"{x=}")
print(f"{y=}")
你会在旧代码里见到的两种写法
f-string 之前,Python 还有过两代格式化语法。这门课统一用 f-string,但你读别人的代码会遇到前两代:
name, score = "张三", 87.5
# 第一代:C 风格的 %
print("%s的成绩是 %.1f 分" % (name, score))
# 第二代:str.format()
print("{}的成绩是 {:.1f} 分".format(name, score))
# 第三代:f-string —— 用这个
print(f"{name}的成绩是 {score:.1f} 分")
三行输出一样。f-string 的优势是变量就写在它出现的位置,不用在末尾的括号里数第几个参数对应第几个占位符。
print 的 sep 和 end
print("a", "b", "c") # 默认用空格分隔
print("a", "b", "c", sep="") # 不分隔
print("a", "b", "c", sep=" | ") # 自定义分隔符
print("2026", "09", "14", sep="-")
print("不换行", end="")
print(" ← 接上了")
函数
为什么需要函数
设想你要写一个几千行的程序。如果全部平铺在一个文件里从上写到下:
- 改动一处很难确定会不会影响别处
- 同样的逻辑要复制粘贴很多遍,改的时候容易漏
- 别人(包括三个月后的你)读不懂
函数是把一段逻辑打包起来、起个名字、以后直接用名字调用。 它是编程里对抗复杂度最基本的手段:把大问题切成小问题,一次只想一个。
你已经在用别人写好的函数了:print()、len()、int()、input()。现在学着写自己的。
三要素:名字、参数、返回值
def area_of_circle(r): # def 关键字,函数名,参数
return 3.14159 * r * r # 返回值
print(area_of_circle(1))
print(area_of_circle(2.5))
def是定义函数的关键字area_of_circle是函数名,命名规则和变量一样r是参数(parameter),是这个函数的输入return后面是返回值,是这个函数的输出- 函数体要缩进
缩进决定了什么属于函数
Python 用缩进划分代码块。连续的、相同缩进的语句属于同一块。
def greet(name):
print("准备打招呼") # 缩进了,属于函数
print(f"你好,{name}") # 缩进了,属于函数
print("这一行没缩进,不属于函数") # 函数外
greet("张三")
约定是每级缩进 4 个空格。不要混用 Tab 和空格,看起来一样,Python 却会报错。VS Code 默认会把 Tab 转成空格,一般不用操心。
定义了不等于运行了
def say_hi():
print("hi")
print("程序开始")
# 上面定义了函数,但一次都没运行
say_hi() # 现在才运行
say_hi() # 可以运行很多次
函数定义只是”登记”这段代码,不会执行它。 只有调用(写上名字加括号)才会运行。
而且必须先定义后调用:
say_bye()
def say_bye():
print("bye")
参数传递
调用时括号里的值叫实参(argument),会按顺序绑给定义时的形参(parameter):
def rectangle_area(width, height):
return width * height
print(rectangle_area(3, 4)) # width=3, height=4
print(rectangle_area(4, 3)) # 顺序不同,这里结果一样但不总是
也可以指定名字,这样顺序就无所谓了:
def introduce(name, age, city):
print(f"{name},{age} 岁,来自{city}")
introduce("张三", 18, "上海")
introduce(age=18, city="上海", name="张三") # 用名字指定
函数内外是隔离的
函数里定义的变量叫局部变量,函数一结束就消失,外面看不见:
def compute():
result = 42 # 局部变量
return result
print(compute())
print(result) # 报错:外面没有 result
这个隔离是好事。写函数的时候不必担心变量名和别处重复:
def f():
x = "函数里的 x"
return x
x = "外面的 x"
print(f())
print(x) # 没被函数改掉
完整的作用域规则(LEGB)在 L14 讲,现在记住”函数内外互不干扰”就够了。
return
return 做两件事:把值交回调用处,并立即结束函数。
def check(n):
if n > 0:
return "正数"
return "非正数" # 只有上面没 return 时才会执行
print(check(5))
print(check(-3))
return 后面的代码不会执行:
def demo():
print("这行会执行")
return 1
print("这行永远不会执行")
print(demo())
可以一次返回多个值(实际上是打包成一个元组):
def min_max(a, b, c):
return min(a, b, c), max(a, b, c)
low, high = min_max(3, 9, 5)
print(low, high)
没有 return 的函数,返回值是 None:
def no_return():
print("我只打印,不返回")
result = no_return()
print("返回值是:", result)
print("类型是:", type(result))
print() 和 return 的区别
这是新生最高频、也最难自己发现的错误。 请认真读这一节。
它们看起来都”给出了结果”,做的却是完全不同的事:
print()是给人看的,把内容显示在屏幕上。显示完就没了,程序里拿不到return是给程序用的,把值交回调用的地方,可以接着参与计算
看两个几乎一样的函数:
def add_print(a, b):
print(a + b) # 只显示
def add_return(a, b):
return a + b # 交回值
add_print(2, 3) # 屏幕上出现 5
add_return(2, 3) # 屏幕上什么都没有
第二个函数明明算对了,屏幕上却什么都没有:它把值交回来了,而调用处没有接住。
区别在于能不能接着用:
def add_print(a, b):
print(a + b)
def add_return(a, b):
return a + b
x = add_print(2, 3) # 屏幕显示 5,但 x 是 None
y = add_return(2, 3) # 屏幕没显示,但 y 是 5
print("x =", x)
print("y =", y)
def add_print(a, b):
print(a + b)
# 想拿结果继续算,就彻底失败了
total = add_print(2, 3) + add_print(4, 5)
None + None 直接报 TypeError。
这条规则还有一个直接的现实后果:实验平台是调用你提交的函数、比对返回值来判分的。 用 print 不 return 的函数,平台拿到的是 None,一分不得。哪怕算法完全正确,屏幕上看起来也完全对。
用别人写好的函数
Python 自带大量现成的函数,按模块组织。用 import 引入:
import math
print(math.sqrt(16)) # 开方
print(math.pi) # 圆周率
print(math.floor(3.7)) # 向下取整
print(math.ceil(3.2)) # 向上取整
print(math.factorial(5)) # 阶乘
有了 math.pi,前面那个圆面积函数可以写得更准:
import math
def area_of_circle(r):
return math.pi * r ** 2
print(f"{area_of_circle(1):.6f}")
模块的完整机制(怎么找、怎么装、怎么写自己的)在 L10。现在你只要会 import 模块名 然后 模块名.函数名() 就够了。
小结
=是绑定不是数学等号,读作”变成”。变量是指向值的名字,不是盒子id()给地址,is问是不是同一个。比值用==,只有和None比才用is- 同步赋值
a, b = b, a:右边先整体求值,所以交换不需要临时变量 - 变量名要有意义,用 snake_case,别覆盖
list、str、sum这些内置名 - 字符串按字典序比较,大写字母排在小写前面;字符串不可变
int()截断,round()四舍五入(且是银行家舍入)input()永远返回字符串,要数就得自己转eval()危险,需要数就用int()/float()- 格式化输出统一用 f-string,
{值:.2f}控小数位,{值:>10}控对齐 - 函数三要素:名字、参数、返回值。定义不等于运行,必须先定义后调用
- 函数内外隔离,局部变量出了函数就没了
print给人看,return给程序用。函数内部一律return
练习
- 写一个函数
celsius_to_fahrenheit(c),把摄氏度转成华氏度(公式F = C * 9/5 + 32),返回结果保留一位小数。注意是return不是print。 - 写一个程序,读入两个整数,输出它们的和、差、积、商(保留两位小数)。用 f-string 输出,格式为
12 + 5 = 17。 - 下面这个函数有什么问题?改对它:
def double(n): print(n * 2) result = double(21) print(result + 1) - 写一个函数
initials(full_name),输入"Zhang San Feng",返回"Z.S.F."。(提示:用.split()把字符串按空格切开) - 用 f-string 输出一个九九乘法表的前三行,每个乘积占 4 格宽度右对齐。
- 解释下面两段代码为什么结果不同:
x = 10 def f(): x = 20 f() print(x) # 输出什么? x = 10 def g(): return 20 x = g() print(x) # 输出什么?
本讲的配套上机题在 Lab 3。