字符串、变量与输入输出
Fundamentals II: Strings, Variables and I/O
程序要和人打交道:读进来的东西怎么处理,算出来的结果怎么呈现。这一讲还会引入函数,它是这门课里第一件用来对付复杂度的工具。
本讲结束后你应当能
- 说清楚赋值语句在内存里做了什么,对比 C 和 Python 的变量,并用「名字指向对象」的模型解释 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长得太像
变量到底是什么
先看两段代码。左边是 C 语言,右边是 Python:
int x = 10;
x = 20;
x = 10
x = 20
C 这门课不学,这两行只需要看懂两点:C 在第一次使用 x 之前要写明它的类型,int 表示整数;每条语句以分号结尾。
两段代码运行完,x 都是 20。但执行 x = 20 的时候,原来那个 10 去了哪里?两种语言的回答不同。
内存是一长串字节,每个字节都有一个编号,这个编号叫地址。程序靠地址找到存在内存里的数据。
C 的变量就是一块内存。 int x = 10 让编译器划出 4 个字节,起名叫 x,10 就存在这 4 个字节里。x = 20 直接把这 4 个字节改写成 20,10 被覆盖了。这很像一个装东西的盒子。
Python 的变量是一个引用。 x = 10 做了两件事:先在内存里造一个整数对象 10,再让 x 记下这个对象的地址。x 里只存一个地址,不存 10。这种存着地址、指向某个对象的名字,叫做引用。x = 20 时,Python 另造一个整数对象 20,再让 x 改指过去。原来那个 10 没有被改写,只是 x 不再指向它。
C 里要实现「指向」,得专门用一种叫指针的写法,这门课不涉及。Python 里每个名字都是引用,不需要任何额外写法。
盒子的理解在只有数和字符串的时候推不出错误的结论,所以很多人一直这样理解 Python 的变量,直到 L5 的列表才发现解释不了。从一开始就用「名字 → 对象」的箭头来理解,后面会省很多事。
id() 返回一个名字当前指向的对象的地址:
x = 10
print(id(x))
y = x # 让 y 指向同一个对象
print(id(y))
print(x is y) # is 问的是「同一个吗」
y = x 之后两个 id 一样,说明赋值复制的是地址,不是对象本身。内存里只有一个 10,两个名字都指着它。
类型记在对象上
既然 x 里只有一个地址,Python 怎么知道它是整数还是字符串?
L2 画过 n = 2026 在内存里的样子:这个对象一共 28 个字节,其中第 8 到 15 字节记着它的类型。类型是对象自己带着的。 type(x) 做的事情,就是顺着 x 里的地址找到对象,再读出类型那一栏。
所以 Python 不用声明类型,同一个名字可以先后指向不同类型的对象:
x = 2026
print(type(x))
x = "2026" # 同一个名字,现在指向一个字符串
print(type(x))
C 的做法相反:那 4 个字节里只有值,没有任何记录说明它是整数。类型只写在代码里,也就是 int x 的那个 int,编译器据此决定怎么解读这 4 个字节。所以 C 的变量必须先声明类型,声明之后也不能再改。
不可变的对象可以放心共用
y = x 之后,两个名字指向同一个 10。那么改了 x,y 会不会跟着变?
x = 10
y = x # y 和 x 指向同一个 10
x = x + 1 # 另造一个 11,再让 x 指过去
print(x, y)
print(x is y)
不会。整数对象造出来之后就不能再改,这叫不可变。字符串也是不可变的,下一节会看到。x = x + 1 并没有把那个 10 改成 11,而是另造了一个 11,再让 x 指过去,y 还指着原来的 10。
正因为不可变,Python 可以放心地让很多名字共用一个对象,省去复制。CPython 还会预先造好 -5 到 256 这些常用的小整数,程序里用到的 10 都是同一个对象。
L5 的列表可以原地修改。到那时,两个名字共用一个列表,改了其中一个,另一个也会跟着变。
== 和 is 不是一回事
== 问「值相不相等」,is 问「是不是同一个对象」:
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # 内容一样
print(a is b) # 但不是同一个
(方括号那个写法叫列表,L5 讲。这里借它说明问题,因为小整数会被共用,在数上演示 is 结果说不准。)
两个列表内容完全一样,所以 == 是 True;但它们是分别造出来的两个对象,在内存里占两块地方,所以 is 是 False。就像两张一模一样的复印件:内容相同,不是同一张纸。
两种设计的取舍
| C | Python | |
|---|---|---|
| 存一个整数 | 4 字节 | 8 字节地址 + 28 字节对象 |
| 取值 | 直接读这 4 个字节 | 先读地址,再找到对象 |
| 类型记在哪 | 变量上:必须先声明 | 对象上:不用声明 |
| 赋值一个大对象 | 把内容整块复制 | 只复制 8 字节的地址 |
| 要留神的地方 | 类型定死,不能换 | 几个名字共用一个可变对象 |
| 设计出发点 | 贴近硬件,速度优先 | 规则统一,写起来灵活 |
(字节数是 64 位电脑上 CPython 的情况。)
单看一个数,Python 多用了好几倍的空间,速度也慢一些。换来的是:类型跟着对象走,名字不用声明类型;所有东西按同一种方式处理,不论指向的是一个整数还是一个装了一百万个数的列表,赋值都只复制一个地址;连函数都可以像数一样赋给一个名字,本讲后面会看到。
C 的设计出发点是贴近硬件、运行得快,写操作系统、写嵌入式程序的人需要这一点。Python 的出发点是写起来省事:现在的计算机足够快,多数程序慢一点没有关系,程序员的时间更宝贵。
代价是几个名字可以共用一个对象。对不可变的整数和字符串,这不会带来问题;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")
函数也是对象
Python 里每个名字都是引用,函数名也不例外。执行到 def 这一行时,Python 把函数体做成一个函数对象,再让函数名指向它。所以函数也可以赋给别的名字:
def square(n):
return n * n
f = square # 没有括号:不是调用
print(f(7))
print(f is square)
print(type(square))
f = square 和 y = x 是同一回事:f 和 square 指向同一个函数对象。
注意 f = square 后面没有括号。不加括号,得到的是函数本身;加上括号,就是调用它、拿到返回值。 如果写成 f = square(7),意思就变成先调用,再把返回值 49 绑给 f。
前面说不要拿 list 当变量名,原因也在这里:list 本来指向一个内置函数,list = [1, 2, 3] 让它改指一个列表,那个内置函数就无法再调用了。
函数能赋给名字,也就能当作参数传给别的函数。L6 用 sorted 排序时,key=len 就是把 len 这个函数传进去。
参数传递
调用时括号里的值叫实参(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 模块名 然后 模块名.函数名() 就够了。
小结
=是绑定,读作”变成”。Python 的变量是引用:名字里存着地址,指向内存里的对象- C 的变量就是一块内存,类型要先声明;Python 的类型记在对象上,所以不用声明
- 整数和字符串不可变,「改」都是造新对象再改指,所以可以放心共用
id()给地址,is问是不是同一个。比值用==,只有和None比才用is- 同步赋值
a, b = b, a:右边先整体求值,所以交换不需要临时变量 - 变量名要有意义,用 snake_case,别覆盖
list、str、sum这些内置名 - 字符串按字典序比较,大写字母排在小写前面;字符串不可变
int()截断;round()取最接近的整数,正好一半时往偶数靠(银行家舍入)input()永远返回字符串,要数就得自己转eval()危险,需要数就用int()/float()- 格式化输出统一用 f-string,
{值:.2f}控小数位,{值:>10}控对齐 - 函数三要素:名字、参数、返回值。定义不等于运行,必须先定义后调用
- 函数也是对象:
f = square让两个名字指向同一个函数,加上括号是调用 - 函数内外隔离,局部变量出了函数就没了
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。