基础篇 / 第 2 章
第 2 章 · 变量、数据类型与运算符
a = b 不会复制数据、运算符的优先级顺序,以及类型之间怎么互相转换。无需前置知识,只要跑通过第 1 章的 print 即可。学完本章,你能读懂任意一段简单的 Python 代码,并能准确判断一个表达式的结果。变量:给对象贴一张标签
Python 的变量(variable)和其他语言有本质区别。其他语言里变量是一个「盒子」,类型写死在声明里;Python 里变量只是指向某个对象的名字(名字绑定,name binding)。理解这一点,后面所有「可变/不可变」的困惑都会消失。
用一张图理解「标签」模型
x = 1000
# 内存里有一个整数对象 1000,x 是指向它的标签
y = x
# y 也指向同一个对象,复制的是「指向关系」,不是数据本身
x = 2000
# 只是让 x 换了标签,y 依然指向原来的 1000
print(x, y) # 2000 1000
# 这就是为什么下面这段不会互相影响:
a = [1, 2]
b = a
b.append(3)
print(a) # [1, 2, 3] ← 指向同一个列表对象
# 而这种写法是「复制数据」,两者独立:
c = a[:] # 切片会生成新列表
c.append(4)
print(a, c) # [1, 2, 3] [1, 2, 3, 4]
1.1 赋值语句的基本形式
| 写法 | 含义 | 示例 |
|---|---|---|
| 单赋值 | 贴一个标签 | x = 10 |
| 链式赋值 | 多个名字指向同一对象 | x = y = z = 0 |
| 解包赋值 | 把序列拆开,分别贴标签 | a, b = 1, 2 |
| 交换赋值 | 一行交换两个变量 | a, b = b, a |
| 增强赋值 | 算完再赋回自己 | x += 1 |
| 带类型注解 | 给名字标注期望类型 | x: int = 10 |
| 海象赋值 | 表达式中顺便赋值 | if (n := len(s)) > 5: |
赋值方式全家福
# 普通
count = 0
# 链式:三个名字都指向同一个 0
x = y = z = 0
x = 1
print(y) # 0(y 仍指向原来的 0 对象)
# 解包:两边数量必须相等
a, b = 1, 2
x, y, z = 1, 2, 3
# 交换(不需要临时变量,这是 Python 的优雅之处)
a, b = 1, 2
a, b = b, a
print(a, b) # 2 1
# 增强赋值:等价于 x = x + 1
score = 0
score += 10 # + - * / // % ** 及位运算 &= |= ^= >>= 等都支持
score *= 2
# 类型注解:给人和工具(IDE、mypy)看的,不影响运行
age: int = 18
name: str = "小明"
coords: tuple[float, float] = (1.5, 2.5)
# 海象赋值:在表达式里赋值,避免重复计算
data = [1, 2, 3, 4, 5]
if (n := len(data)) > 3:
print(f"有 {n} 个元素") # 有 5 个元素
# 海象常见用法:处理 while 循环避免重复取值
import io
text = "hello world"
while chunk := text[:3]:
print(chunk)
text = text[3:]
# 输出:hel / lo (注意:空字符串是假值,循环会停)
x = y = z = 0 之后写 x = 1,y 仍然是 0,因为这只是让 x 换了标签。想让三者独立要用 x, y, z = 0, 0, 0。1.2 变量命名规则
变量名(标识符 identifier)必须「像名字」:字母或下划线开头,只能包含字母、数字、下划线,不能是关键字。
| 名称 | 规则 | 示例 |
|---|---|---|
| 字母开头 | 英文字母 | name、total |
| 下划线开头 | 表示「内部使用」的约定 | _cache |
| 数字可出现 | 但不能在开头 | item1 可以,1item 不行 |
| 区分大小写 | Name 与 name 不同 | 见下方对比 |
| 不能用关键字 | 如 if class def | 用 class_ 代替 |
合法与非法命名
# 合法
user_name = "李雷"
_user = 1
__init__ = "魔法方法"
item2 = [1, 2]
订单号 = "A001" # 中文标识符合法,但不推荐(可读性/团队规范)
# 非法
# 2fast = 1 数字不能开头
# user-name = 1 减号被当作减法
# class = "A" 关键字
# my var = 1 有空格
# 区分大小写
Name = "大写N"
name = "小写n"
print(Name, name) # 大写N 小写n —— 两个不同的变量!
1.3 命名风格约定
PEP 8(Python 官方风格指南)规定了几种命名约定,虽然语法上不强制,但团队协作时照着做能避免大量沟通成本。
| 对象 | 风格 | 示例 |
|---|---|---|
| 普通变量、函数 | 小写 snake_case | user_name、get_data() |
| 常量 | 全大写 SNAKE_CASE | MAX_SIZE = 100 |
| 类名 | 大写 CamelCase | class UserAccount: |
| 内部/不公开 | 单前导下划线 | _internal() |
| 触发特殊行为 | 双前后下划线 | __init__、__len__ |
# ruff: noqa 并不需要,真正有用的是在 pyproject.toml 里启用 select = ["N"](pep8-naming 规则)。1.4 常量与「伪常量」
Python 没有强制常量语法,但约定:全大写的名字表示常量,不要修改。
常量的约定与「软保护」
PI = 3.1415926
MAX_RETRY = 3
DEFAULT_NAME = "匿名"
print(PI) # 3.1415926
# PI = 3.14 # 技术上能改,但不该改——这是「约定」不是「强制」
# 如果真的想提供只读属性,可以用 property 或类属性(后续章节展开)
enum 模块)或用 typing.Final 标注,借助工具强制不被改。六大内置数据类型
Python 内置的核心数据类型可以先记这六个:int(整数)、float(浮点数)、str(字符串)、bool(布尔)、NoneType(空值)、以及第 5 章重点讲的容器(list、tuple、set、dict)。
2.1 整型 int
Python 的整数是任意精度的——不像其他语言会因超出范围而溢出,这是做密码学、计算超大数时的巨大优势。
整数:范围无限,不会溢出
a = 42
b = -7
c = 0
print(type(a)) # <class 'int'>
# 其他进制写法
print(0b1010) # 10 二进制
print(0o17) # 15 八进制
print(0xFF) # 255 十六进制
print(1_000_000) # 1000000 下划线分隔,提高可读性
# 任意精度:不会 OverflowError
big = 2 ** 1000
print(len(str(big))) # 302 位十进制数
# 位运算
print(5 & 3) # 1 与
print(5 | 3) # 7 或
print(5 ^ 3) # 6 异或
print(~5) # -6 取反
print(5 << 2) # 20 左移乘 4
print(5 >> 1) # 2 右移整除 2
2.2 浮点数 float
浮点数用 64 位存储(双精度),能表示约 15–16 位有效数字。它遵循 IEEE 754 标准,因此存在精度问题。
浮点数:精度问题的经典坑
price = 19.99
rate = 0.1
print(type(price)) # <class 'float'>
# 精度问题:0.1 + 0.2 不等于 0.3
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False!
# 正确比较浮点数的方式
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
# 特殊值
inf = float('inf')
nan = float('nan')
print(inf, -inf, nan) # inf -inf nan
print(nan == nan) # False(自己都不等于自己!)
# 科学计数法
big = 1.5e10 # 等于 1.5 * 10**10
small = 2.5e-4 # 等于 0.00025
# 转成整数
print(int(3.99)) # 3 向零取整
print(int(-3.99)) # -3
print(round(3.99)) # 4 四舍五入
print(round(3.99, 1)) # 4.0 保留 1 位小数
0.1 + 0.2 != 0.3 会导致对账差一分钱。应该用 decimal.Decimal 或整数存「分」。需要精确小数时用 Decimal
from decimal import Decimal, ROUND_HALF_UP
# 用字符串构造,避免先经过 float 的精度损失
price = Decimal('19.99')
total = price * 3
print(total) # 59.97 精确!
# 四舍五入(必须指定精度)
rounded = Decimal('2.345').quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
print(rounded) # 2.35
2.3 字符串 str
字符串是不可变的文本序列。可以用单引号、双引号或三引号包裹。
字符串的四种写法
# 单引号
s1 = 'Hello'
# 双引号(字符串里含单引号时用双引号,更方便)
s2 = "It's Python" # -> It's Python
# 三引号:多行字符串,也常用来写文档字符串
s3 = """第一行
第二行
第三行"""
# 带 f 前缀:在字符串里嵌入变量或表达式(第 4 章详讲)
name = "小明"
s4 = f"你好,{name}!今天第 {1 + 1} 天"
print(s4) # 你好,小明!今天第 2 天
# 相邻字符串自动拼接(长字符串换行时好用)
s5 = "hello " "world"
print(s5) # hello world
# 转义字符
print("换行\n结束") # 实际输出两行
print("制表\t对齐")
print("引号\"内嵌\"")
print("路径 C:\\new\\test") # 转义反斜杠
2.4 布尔值 bool 与 None
| 值 | 写法 | 含义 |
|---|---|---|
| 真 | True | 首字母大写 |
| 假 | False | 首字母大写 |
| 空值 | None | 「什么都不是」,不是 0 也不是空字符串 |
真假值的判断:Python 的「假值」
flag = True
print(flag + 1) # 2 True 相当于 1,False 相当于 0
# 以下都是「假值」(falsy),在 if 中等价于 False
print(bool(0)) # False
print(bool(0.0)) # False
print(bool("")) # False 空字符串
print(bool([])) # False 空列表
print(bool({})) # False 空字典
print(bool(())) # False 空元组
print(bool(set())) # False 空集合
print(bool(None)) # False None
# 以下都是「真值」(truthy)
print(bool(1)) # True
print(bool(-1)) # True 负数也是真
print(bool("0")) # True 字符串"0"非空!
print(bool([0])) # True 含元素的列表
# 正确的 None 判断
result = None
if result is None: # 用 is,不用 ==
print("还没有结果")
if not x: 之外还要理解:if x: 依赖真假值,而 if x == []: 是错的(对空列表判等和不判真假不等价于各种边界)。另外 None 判断永远用 is None,不要写 == None。False 和 0 都能参与运算(False + 1 == 1),但 None 不能——None + 1 会报 TypeError。因为「无」不能参与算术。2.5 用 type() 和 isinstance() 查看类型
检查类型
x = 42
print(type(x)) # <class 'int'>
print(type(x) is int) # True,精确判断
print(type(x) == int) # True
# 更推荐 isinstance:支持继承判断
print(isinstance(x, int)) # True
print(isinstance(True, int)) # True!bool 是 int 的子类
print(isinstance(3.14, float)) # True
print(isinstance("hi", str)) # True
print(isinstance([1], list)) # True
# isinstance 可以一次判断多个类型
print(isinstance(x, (int, float))) # True
# type 与 isinstance 的区别:
# isinstance(True, int) -> True (因为 bool 继承 int)
# type(True) is int -> False (精确判断)
isinstance(考虑继承),只有在需要「精确类型」时(比如区分 True 和 1)才用 type(x) is int。可变性与不可变性
这是 Python 最重要的概念之一。不可变(immutable)类型创建后不能修改,只能「生成新的」;可变(mutable)类型可以原地修改。
| 类型 | 可变? | 说明 |
|---|---|---|
int float complex | 不可变 | 重新赋值就是新对象 |
bool None | 不可变 | 单例值 |
str | 不可变 | 任何「修改」都是生成新字符串 |
tuple | 不可变 | 内容不可改,但元素可以是可变对象 |
bytes | 不可变 | 二进制数据 |
list | 可变 | 可以增删改 |
set | 可变 | 可以增删 |
dict | 可变 | 可以增删改键值 |
不可变 vs 可变:动手验证
# 字符串:不可变,赋值操作返回新对象
s = "hello"
print(id(s)) # 例如 140234...
s2 = s.upper() # 生成新字符串
print(s2) # HELLO
print(s) # hello(原串没变)
print(id(s) == id(s2)) # False,两个不同对象
# 但「看起来在改」的写法其实是在重新赋值
s += " world" # 等价于 s = s + " world",s 指向新字符串
print(s) # hello world
# 列表:可变,原地修改
nums = [1, 2, 3]
print(id(nums))
nums.append(4) # 原地修改,同一个对象
print(id(nums) == id(nums)) # 同样会变化
# 核心区别:不可变可以安全共享,可变要小心别名问题
a = [1, 2]
b = a
b.append(3) # a 也变成 [1, 2, 3](危险!)
# 解决:传参时复制
b = a[:] # 或 a.copy() 或 list(a)
b.append(3)
print(a) # [1, 2] 独立了
哈希与可作为字典键的条件
# 只有不可变且可哈希(hashable)的对象能做字典键 / 集合元素
d = {"name": "小明", "age": 18}
s = {1, 2, 3} # 集合元素也不能重复
# 列表和字典不可哈希,不能做键
try:
bad = {[1, 2]: "值"} # TypeError: unhashable type: 'list'
except TypeError as e:
print(e) # unhashable type: 'list'
# 但「不可变元素的容器」可以哈希(哈希基于内容)
key = (1, 2, 3) # 元组不可变 -> 可哈希
d2 = {key: "来自元组的键"} # 合法
print(d2[(1, 2, 3)]) # 来自元组的键
# 检查是否可哈希
print(hash("abc") is not None) # True
print(hash((1, 2)) == hash((1, 2))) # True,内容相同哈希相同
t = ([1], 2); t[0].append(3) 是合法的。所谓「浅不可变」。运算符:算术、比较与逻辑
Python 的运算符分几类,这里按用的频率逐一说明。
4.1 算术运算符
| 运算符 | 名称 | 示例 | 结果 |
|---|---|---|---|
+ | 加 / 拼接 | 1 + 2、"a" + "b" | 3、"ab" |
- | 减 / 差集 | 5 - 2、[1,2] - [2] | 3、[1] |
* | 乘 / 重复 | 3 * 4、"ab" * 3 | 12、"ababab" |
/ | 真除法 | 7 / 2 | 3.5(永远返回 float) |
// | 向下取整除 | 7 // 2 | 3(int 或 float) |
% | 取模 / 格式化 | 7 % 3 | 1 |
** | 幂 | 2 ** 10 | 1024 |
@ | 矩阵乘法 | NumPy 专用 | — |
除法的三个版本,别搞混
print(7 / 2) # 3.5 真除法,结果总是 float
print(7 // 2) # 3 整除,向下取整
print(-7 // 2) # -4 注意是「向下」,不是「向零」
print(7 % 2) # 1
print(-7 % 2) # 1 Python 保证 a = b*(a//b) + a%b
print(2 ** 0.5) # 1.4142135623730951 幂返回 float
# 百分比格式化(字符串的 % 运算符)
name, score = "小明", 95.5
print("%s 得了 %.1f 分" % (name, score)) # 小明 得了 95.5 分
print("%d%%" % 50) # 50% %% 表示字面量%
-7 // 2 的结果是 -4 不是 -3(其他多数语言向零截断)。这是 Python「向下取整 floor」的定义。如果你要的是向零截断,用 int(a / b) 或 math.trunc(a / b)。4.2 比较运算符
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
== | 等于 | 1 == 1.0 | True |
!= | 不等于 | 1 != 2 | True |
> < | 大于 / 小于 | 3 > 2 | True |
>= <= | 大于等于 / 小于等于 | 3 >= 3 | True |
is | 是不是同一个对象 | a is b | 身份比较 |
is not | 不是同一个对象 | a is not b | 身份不等 |
in | 成员运算符 | "a" in "cat" | 包含 |
not in | 非成员 | "z" not in "cat" | 不包含 |
== 与 is 的本质区别
a = [1, 2, 3]
b = [1, 2, 3] # 内容相同,但是两个不同的列表对象
c = a # 指向同一个列表对象
print(a == b) # True 内容相等
print(a == c) # True 内容也相等
print(a is b) # False 不是同一个对象
print(a is c) # True 是同一个对象
# == 调用 __eq__(比较内容),is 比较内存地址(id)
# 大多数时候你想用的是 ==
# 判 None 时必须用 is None
= 是赋值,== 才是比较。写 if x = 5: 会直接语法错误——这是新手最常见的报错之一。记住:一个等号是赋值,两个等号才是判断。4.3 逻辑运算符:and / or / not
| 运算符 | 含义 | 规则 |
|---|---|---|
and | 与 | 两边都为真才为真(短路) |
or | 或 | 有一边为真就为真(短路) |
not | 非 | 取反 |
短路求值的妙用
# and:左边为假就短路,不再算右边
def check(x):
print("检查中...")
return x > 0
if check(0) and check(1): # check(0) 为 False,右边不执行
print("都为正")
# 输出:检查中...(只打印一次)
# or:左边为真就短路
user = None
name = user or "匿名" # user 为假,取右边,不报错
print(name) # 匿名
# 常见惯用法
result = default_value or compute() # 若 default_value 为假才调 compute
# not 的优先级要加括号
print(not True == False) # not (True == False) = not False = True
print(not (True == False)) # 同上,显式加括号更清晰
# 链式比较:Python 独有,写起来像数学
age = 25
print(18 <= age <= 60) # True,等价于 18<=age and age<=60
print(18 < age < 25) # False
0 or "x" 返回 "x",5 and 7 返回 7。这既是坑也是利器(x or 默认值 惯用法)。4.4 赋值运算符与运算符优先级
| 运算符 | 等价写法 | 含义 |
|---|---|---|
= | — | 赋值 |
+= | x = x + v | 加后赋值 |
-= *= /= | 同上 | 减/乘/除后赋值 |
//= %= **= | 同上 | 整除/取模/幂后赋值 |
&= |= ^= >>= | 同上 | 位运算后赋值 |
运算符优先级(从高到低,高优先级先算)
优先级 运算符 例子
1 ** 2**3**2 = 8 右结合
2 ~ +(一元) -3
3 * / // % @ 2*3
4 + - 2+3
5 << >> 1<<3
6 & 5&3
7 ^ 5^3
8 | 5|3
9 < <= > >= in not in is is not
10 not not True
11 and a and b
12 or a or b
13 = += -= ...(赋值) x = 5
# 记忆口诀:算 → 比 → 逻 → 是(先算术,再比较,再逻辑,最后赋值)
优先级实测:搞不清就加括号
# 加法优先于比较
print(1 + 2 == 3) # True,等价于 (1+2)==3
# ** 优先于乘:右结合
print(2 ** 3 ** 2) # 2**(3**2) = 512
print((2 ** 3) ** 2) # 8**2 = 64
# 比较优先于 not
print(not 1 == 2) # not (1==2) = True
# and 优先于 or
print(True or False and False) # True or (False and False) = True
print((True or False) and False) # True and False = False
# 建议:混合运算一律加括号,可读性远比炫技重要
4.5 位运算符与身份运算符补充
位运算的实用场景
# 1. 权限系统:用二进制位表示开关
READ = 1 << 0 # 0001
WRITE = 1 << 1 # 0010
EXECUTE = 1 << 2 # 0100
user_perm = READ | WRITE # 0011 同时有读和写
print(bool(user_perm & READ)) # True,有读权限
print(bool(user_perm & EXECUTE)) # False,无执行权限
user_perm |= EXECUTE # 加权限
user_perm &= ~WRITE # 去权限(按位与非)
# 2. 快速判断奇偶
print(n & 1) # n 是奇数则结果为 1
# 3. 交换两个数(不用临时变量)
a, b = 5, 9
a, b = b, a
# 或者位运算法(实际不用,会降低可读性):
# a = a ^ b; b = a ^ b; a = a ^ b
类型转换
Python 不会隐式地在不兼容的类型间转换(这是好事,避免了 JavaScript 那些诡异行为)。转换必须显式调用,这样意图清晰。
5.1 常用转换函数
| 函数 | 转换 | 示例 | 注意 |
|---|---|---|---|
int(x) | 转整数 | int("42") → 42 | 失败抛 ValueError |
int(x, base) | 按进制转 | int("ff", 16) → 255 | 解析十六进制字符串 |
float(x) | 转浮点 | float("3.14") | — |
str(x) | 转字符串 | str(42) → "42" | — |
bool(x) | 转布尔 | bool(0) → False | 按真假值规则 |
list(x) | 转列表 | list("abc") | 字符串也能转(第 5 章) |
tuple(x) | 转元组 | tuple([1,2]) | — |
set(x) | 转集合 | set([1,1,2]) → {1,2} | 自动去重 |
repr(x) | 转「表示形式」 | repr("hi") → "'hi'" | 调试用,带引号 |
类型转换实战
# input 总是返回字符串,必须转换才能计算
age_str = input("年龄:") # "18"
age = int(age_str) # 18
print(age + 1) # 19
# 转换链:字符串 -> 浮点 -> 整数
price = float("19.99") # 19.99
whole = int(price) # 19
# 带默认值的安全转换
def to_int(s, default=0):
"""把字符串转成整数,失败返回默认值。"""
try:
return int(s)
except (ValueError, TypeError):
return default
print(to_int("abc", -1)) # -1
print(to_int("100")) # 100
# 二进制字符串转数字
print(int("1010", 2)) # 10
print(int("ff", 16)) # 255
# 注意:字符串之间不能直接比较大小!
print("10" > "9") # False 逐位比字符:'1'(49) < '9'(57)
print(int("10") > int("9")) # True 数字比较:10 > 9
「字符串比大小」的真相
# 字符串比较是「逐位比较 Unicode 码」,不是比数值
print("10" > "9") # False 先比 '1' 和 '9','1' 的码点更小
print("apple" < "banana") # True 比 'a' 和 'b'
print("abc" < "abd") # True 逐位往下比
# 所以:处理数字务必先转 int/float 再比较
age = "18" # 来自 input() 的字符串
print(age > "20") # False 错误!按字符比
print(int(age) > 20) # False 正确,按数值比
# 上一行恰好也返回 False,但当 age="9" 时两者结论就不同了:
print("9" > "20") # True 字符比:'9' > '2'
print(int("9") > 20) # False 数值比:9 < 20 ← 这才是真相
input() 拿到的值必须先转成数字再比较大小」。"9" > "20" 返回 True 就是最典型的翻车案例——看起来逻辑没写错,结果却完全相反。int("3.14") 会报 ValueError(字符串里有点不是合法整数)。正确做法是先 float() 再 int()。另外 int(True) 是 1,转换不会「丢失」类型的特殊含义。综合实战:变量与类型的综合运用
把本章知识串起来,写一个「学生成绩分析器」,涵盖变量、类型、运算、转换、判断。
score_analyzer.py
"""分析一组学生成绩,输出统计信息。要求只用本章学过的知识。"""
# 输入:姓名和分数(用元组列表表示,稍后第 5 章会用真正的列表)
records = [
("张三", 89),
("李四", 76),
("王五", 92),
("赵六", 58),
("钱七", 95),
]
def to_float(value, default=0.0):
"""安全地把任意输入转成 float。"""
try:
return float(value)
except (ValueError, TypeError):
return default
scores = [to_float(r[1]) for r in records]
total = sum(scores)
count = len(scores)
average = total / count if count else 0.0 # 防止除零
# 排序:按分数从高到低(用排序键,- 表示降序)
ranked = sorted(records, key=lambda r: -r[1])
print(f"人数: {count}")
print(f"总分: {total:.1f}")
print(f"平均分: {average:.2f}")
print(f"最高分: {max(scores)} 最低分: {min(scores)}")
# 等级评定
def grade(score):
if score >= 90:
return "优秀"
elif score >= 80:
return "良好"
elif score >= 60:
return "及格"
else:
return "不及格"
print("\n成绩排名:")
for rank, (name, score) in enumerate(ranked, start=1):
print(f" {rank}. {name} {score}分 {grade(score)}")
# 统计各等级人数(用字典)
counter = {}
for _, score in records:
g = grade(score)
counter[g] = counter.get(g, 0) + 1
print("\n等级分布:", counter)
# 及格率
passed = sum(1 for s in scores if s >= 60)
rate = passed / count * 100
print(f"及格率: {rate:.1f}%")
加上健壮的输入处理(可直接跑)
"""交互版成绩分析器:从命令行读入多行「姓名 分数」,直到空行结束。"""
def parse_record(line: str):
"""把一行输入拆成 (姓名, 分数),失败返回 None。"""
parts = line.split()
if len(parts) != 2:
return None
name, raw = parts
try:
score = float(raw)
except ValueError:
return None
if not 0 <= score <= 100: # 合法性校验
return None
return name, score
def main():
records = []
print("请逐行输入「姓名 分数」,空行结束:")
while True:
line = input("> ").strip()
if not line:
break
item = parse_record(line)
if item is None:
print(" 格式不对,应该是「姓名 分数」且分数在 0-100 之间")
continue
records.append(item)
if not records:
print("没有输入任何有效数据")
return
scores = [s for _, s in records]
total, count = sum(scores), len(scores)
print(f"\n共 {count} 人,总分 {total:.1f},平均 {total / count:.2f}")
print(f"最高 {max(scores):.1f},最低 {min(scores):.1f}")
best = max(records, key=lambda r: r[1])
print(f"第一名:{best[0]}({best[1]:.1f} 分)")
# 方差与标准差:衡量成绩离散程度
avg = total / count
var = sum((s - avg) ** 2 for s in scores) / count
print(f"方差 {var:.2f},标准差 {var ** 0.5:.2f}")
if __name__ == "__main__":
main()
运行结果
请逐行输入「姓名 分数」,空行结束:
> 张三 89
> 李四 76
> 王五 abc
格式不对,应该是「姓名 分数」且分数在 0-100 之间
> 赵六 120
格式不对,应该是「姓名 分数」且分数在 0-100 之间
> 钱七 95
>
共 4 人,总分 350.0,平均 87.50
最高 95.0,最低 76.0
第一名:钱七(95.0 分)
方差 63.19,标准差 7.95
max(records, key=lambda r: r[1]) 是「取最大键元素」的标准写法,比先排序再取第 0 个更高效(O(n) vs O(n log n)),第 11 章会专门讲高阶函数。运行结果
人数: 5
总分: 410.0
平均分: 82.00
最高分: 95.0 最低分: 58.0
成绩排名:
1. 钱七 95分 优秀
2. 王五 92分 优秀
3. 张三 89分 良好
4. 李四 76分 及格
5. 赵六 58分 不及格
等级分布: {'优秀': 2, '良好': 1, '及格': 1, '不及格': 1}
及格率: 80.0%
{rate:.1f}% 的写法:: 后面的 .1f 是格式说明,保留 1 位小数。这是 f-string 的核心用法,第 4 章会系统讲。类型标注与运行时行为
Python 3.5+ 支持类型注解(type hints)。它不影响运行,只服务于人、编辑器和类型检查器。本章只做入门介绍,第 13 章会完整展开。
类型标注的基本写法
# 变量标注
age: int = 18
name: str = "小明"
scores: list[int] = [90, 85, 92] # 内置泛型(3.9+)
# 函数标注
def add(a: int, b: int) -> int:
"""加法:接收两个整数,返回整数。"""
return a + b
def average(nums: list[float]) -> float:
if not nums:
return 0.0
return sum(nums) / len(nums)
# 关键:类型注解不做运行时检查,传错类型照样运行
print(add("1", "2")) # "12" 字符串拼接了!没有任何报错
# 想要真正的运行时类型校验,可以用:
def checked_add(a: int, b: int) -> int:
if not isinstance(a, int) or not isinstance(b, int):
raise TypeError("参数必须是整数")
return a + b
print(checked_add("1", "2"))
# TypeError: 参数必须是整数
def f(x: int) 传字符串进去照样能跑。注解的价值在于:编辑器补全、mypy 静态检查、文档可读性。要运行时校验必须自己写 isinstance 或用 pydantic 这类库。常见陷阱速查
本章的坑集中在这里,建议逐条亲自动手验证一次。
| 陷阱 | 错误写法 | 正确写法 | 原因 |
|---|---|---|---|
| 赋值写成了比较 | if x = 5: | if x == 5: | = 是赋值 |
| 字符串比大小 | "10" > "9" | int("10") > 9 | 字符串按字符比 |
| 浮点相等 | 0.1+0.2 == 0.3 | math.isclose(...) | 浮点精度 |
| 列表别名 | b = a; b.append(x) | b = a.copy() | 指向同一对象 |
| None 判断 | if x == None: | if x is None: | 用身份比较 |
| 整数除法误解 | 7 / 2 以为得 3 | 7 // 2 | / 是真除法 |
| 可变默认参数 | def f(x=[]): | def f(x=None): | 默认值只创建一次(第 6 章) |
| 字符串修改 | s[0] = "A" | s = "A" + s[1:] | 字符串不可变 |
| 优先级误解 | 混合 + * == and | 一律加括号 | 加括号更安全 |
把陷阱写成断言,测试你的理解
# 逐条验证,注释里是正确答案
assert (1 + 2 == 3) is True
assert ("10" > "9") is False # 字符串按码点比,'1' < '9'
assert (7 / 2) == 3.5
assert (7 // 2) == 3
assert (-7 // 2) == -4 # 向下取整
assert (2 ** 3 ** 2) == 512 # 右结合
a = [1, 2]
b = a
assert (a is b) is True
c = a.copy()
assert (a is c) is False
assert isinstance(True, int) is True # bool 继承 int
assert (type(True) is int) is False # 但精确类型不是
print("全部断言通过,你已经掌握本章了!")
本章小结
- 变量是标签:
a = b复制的是「指向关系」,不是数据本身;a = b = 0三个名字指向同一对象。 - 命名规范:小写 snake_case 给变量/函数,全大写给常量,CamelCase 给类;区分大小写,不能以数字开头,不能用关键字。
- 六大类型:
int(任意精度)、float(有精度问题)、str(不可变文本)、bool、None,外加第 5 章的容器。 - 假值规则:
00.0""[]{}()None都是假;但"0"[0]是真。 - None 判断:永远用
is None,不要用== None。 - 可变 vs 不可变:数字、字符串、元组不可变(可安全共享、可做字典键);列表、集合、字典可变(要防别名问题)。
- 算术三兄弟:
/真除法、//向下取整、%取余;-7 // 2 == -4。 - == 与 is:
==比内容,is比身份;isinstance比类型且支持继承。 - 逻辑短路:
and/or返回的是「决定结果的 operand」,不一定是布尔值;这带来了x or 默认值的惯用法。 - 优先级:算术 > 比较 >
not>and>or> 赋值;混合运算一律加括号。 - 类型转换必须显式:
int()float()str()bool();从input()拿到的必须先转数字再比较大小。 - 金额别用 float:需要精确小数就用
decimal.Decimal(用字符串构造)或整数存「分」。 - 类型注解不是强制:它只服务于工具和文档,传错类型照样运行;运行时校验要自己写
isinstance。
练习题
- 变量交换的多种写法:不借助第三方库,用三种方式交换
a = 5, b = 9的值(提示:元组解包、临时变量、位运算)。并解释为什么元组解包是首选。 - 真假值实验:对下列每个值用
bool()求真值并解释原因:0、0.0、""、[]、{}、()、set()、None、"0"、[0]、-1、0.0。 - 运算符优先级推导:不运行代码,写出下列表达式的结果,然后用实际运行验证:
2 ** 3 ** 2、(2 ** 3) ** 2、-2 ** 2、not True == False、True or False and False、1 + 2 * 3 ** 2、10 > 5 == 5。 - 可变性的影响:写一段代码,展示列表别名(aliasing)问题:定义
original = [1, 2, 3],分别用alias = original、copy1 = original[:]、copy2 = original.copy()、copy3 = list(original)四种方式创建副本,对每个副本append(99),打印original验证哪些方式相互独立。 - 除法三兄弟:对
7和2分别用/、//、%计算;再对-7和2做同样操作,并解释-7 // 2为何是-4而-7 % 2是1(验证7 == 2*(-4) + 1)。 - 安全的类型转换器:写一个函数
safe_convert(value, target, default=None),支持把输入转成int/float/str/bool,转换失败返回default。用至少 8 组数据测试,包括正常值、空字符串、None、汉字、列表。 - 不可变性的证明:用
id()证明字符串是不可变的:定义s = "hello",分别用s.upper()、s + "!"、s.replace("l", "L")处理,打印每次操作前后的id和内容,说明为什么原字符串没变。 - 位运算实现权限系统:用
READ=1<<0、WRITE=1<<1、EXECUTE=1<<2定义三个权限。给用户分配「读+写」,用&检查是否有读权限;再加执行权限;最后用&= ~WRITE移除写权限。每次操作后打印权限值的二进制表示。 - 综合:BMI 计算与分类:写一个程序,接收姓名、身高(米)、体重(千克),用
Decimal计算 BMI = 体重 / 身高²,并按中国标准分类(<18.5 偏瘦,18.5–23.9 正常,24–27.9 超重,≥28 肥胖)。要求对身高、体重做安全转换与合法性校验。 - 读懂真实报错:运行下面每行代码,记录抛出的异常类型和错误信息,并各写一句解释:
1 + "1"、[1,2][5]、"a" * 3、int("3.5")、None + 1、[1,2] + (3,)。
# 1. 元组解包(推荐,简洁)
a, b = b, a
# 2. 临时变量
tmp = a; a = b; b = tmp
# 3. 位运算
a ^= b; b ^= a; a ^= b
"0" 是非空字符串所以为真;[0] 含元素所以为真;-1 非零所以为真。规律:「空」或「零」为假,其余为真。** 右结合且比一元负号优先(所以 -2**2 == -4),以及 and 比 or 优先。alias = original 会污染原列表(输出 [1, 2, 3, 99]),其余三种(切片、.copy()、list())都生成新对象,original 保持 [1, 2, 3]。a == b*(a//b) + a%b,且余数与除数同号。-7//2 == -4 是「向下取整」;若要向零截断用 int(-7/2) == -3。def safe_convert(value, target, default=None):
try:
if target is bool:
return bool(value)
return target(value) # target 本身就是 int/float/str
except (ValueError, TypeError):
return default
print(safe_convert("42", int)) # 42
print(safe_convert("abc", int, -1)) # -1
print(safe_convert(None, int, 0)) # 0
print(safe_convert("3.5", int, 0)) # 0(int("3.5") 失败)
s 的 id 不变且内容仍是 "hello"。因为字符串不可变,任何「修改」都必须生成新对象。READ, WRITE, EXECUTE = 1, 2, 4
perm = READ | WRITE # 3 即 0b011
print(bool(perm & READ)) # True
perm |= EXECUTE # 7 即 0b111
perm &= ~WRITE # 5 即 0b101
print(bin(perm)) # 0b101
from decimal import Decimal
def bmi_category(bmi):
if bmi < Decimal("18.5"):
return "偏瘦"
elif bmi < Decimal("24"):
return "正常"
elif bmi < Decimal("28"):
return "超重"
return "肥胖"
h = Decimal(input("身高(米):"))
w = Decimal(input("体重(千克):"))
bmi = w / (h ** 2)
print(f"BMI = {bmi:.2f} 判定:{bmi_category(bmi)}")
'aaa' / ValueError / TypeError / TypeError。第 3 行不报错,因为 * 对字符串是重复——这正是「类型决定操作含义」的体现。