CS1602计算导论
第 3 讲Part 1 计算基础与 Python 入门AI Level 0

字符串、变量与输入输出

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("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 是给程序用的,把值交回调用的地方,可以接着参与计算

看两个几乎一样的函数:

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

练习

  1. 写一个函数 celsius_to_fahrenheit(c),把摄氏度转成华氏度(公式 F = C * 9/5 + 32),返回结果保留一位小数。注意是 return 不是 print。
  2. 写一个程序,读入两个整数,输出它们的和、差、积、商(保留两位小数)。用 f-string 输出,格式为 12 + 5 = 17。
  3. 下面这个函数有什么问题?改对它:
    def double(n):
        print(n * 2)
    
    result = double(21)
    print(result + 1)
  4. 写一个函数 initials(full_name),输入 "Zhang San Feng",返回 "Z.S.F."。(提示:用 .split() 把字符串按空格切开)
  5. 用 f-string 输出一个九九乘法表的前三行,每个乘积占 4 格宽度右对齐。
  6. 解释下面两段代码为什么结果不同:
    x = 10
    def f():
        x = 20
    f()
    print(x)          # 输出什么?
    
    x = 10
    def g():
        return 20
    x = g()
    print(x)          # 输出什么?

本讲的配套上机题在 Lab 3。