本章

基础篇 / 第 2 章

第 2 章 · 变量、数据类型与运算符

本章导读:这一章是整本教程的地基。你会学到怎么给东西起名字、Python 里有哪些「盒子类型」、为什么 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]
要点一句话总结:Python 的变量是「标签」,赋值操作是「重新贴标签」,不是「复制内容」。这就是为什么把列表传给函数后函数里的修改会影响外面。

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_caseuser_name、get_data()
常量全大写 SNAKE_CASEMAX_SIZE = 100
类名大写 CamelCaseclass 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 位小数
常见坑处理钱、金额、精确小数时不要用 float!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]  独立了
要点不可变类型有两大优势:① 可以安全地作为字典的键(key);② 可以安全地被多个地方共享,因为没人能改它。这就是为什么字典的键必须是不可变类型。

哈希与可作为字典键的条件

# 只有不可变且可哈希(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" * 312、"ababab"
/真除法7 / 23.5(永远返回 float)
//向下取整除7 // 23(int 或 float)
%取模 / 格式化7 % 31
**幂2 ** 101024
@矩阵乘法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.0True
!=不等于1 != 2True
> <大于 / 小于3 > 2True
>= <=大于等于 / 小于等于3 >= 3True
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
要点逻辑运算符返回的不一定是布尔值,而是「第一个决定结果的 operand」: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.3math.isclose(...)浮点精度
列表别名b = a; b.append(x)b = a.copy()指向同一对象
None 判断if x == None:if x is None:用身份比较
整数除法误解7 / 2 以为得 37 // 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 章的容器。
  • 假值规则:0 0.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。

练习题

  1. 变量交换的多种写法:不借助第三方库,用三种方式交换 a = 5, b = 9 的值(提示:元组解包、临时变量、位运算)。并解释为什么元组解包是首选。
  2. 参考答案要点
    # 1. 元组解包(推荐,简洁)
    a, b = b, a
    # 2. 临时变量
    tmp = a; a = b; b = tmp
    # 3. 位运算
    a ^= b; b ^= a; a ^= b
  3. 真假值实验:对下列每个值用 bool() 求真值并解释原因:0、0.0、""、[]、{}、()、set()、None、"0"、[0]、-1、0.0。
  4. 参考答案要点前 8 个是假值(空容器、零值、None);"0" 是非空字符串所以为真;[0] 含元素所以为真;-1 非零所以为真。规律:「空」或「零」为假,其余为真。
  5. 运算符优先级推导:不运行代码,写出下列表达式的结果,然后用实际运行验证:2 ** 3 ** 2、(2 ** 3) ** 2、-2 ** 2、not True == False、True or False and False、1 + 2 * 3 ** 2、10 > 5 == 5。
  6. 参考答案要点依次为 512、64、-4、True、True、19、True。易错点是 ** 右结合且比一元负号优先(所以 -2**2 == -4),以及 and 比 or 优先。
  7. 可变性的影响:写一段代码,展示列表别名(aliasing)问题:定义 original = [1, 2, 3],分别用 alias = original、copy1 = original[:]、copy2 = original.copy()、copy3 = list(original) 四种方式创建副本,对每个副本 append(99),打印 original 验证哪些方式相互独立。
  8. 参考答案要点只有 alias = original 会污染原列表(输出 [1, 2, 3, 99]),其余三种(切片、.copy()、list())都生成新对象,original 保持 [1, 2, 3]。
  9. 除法三兄弟:对 7 和 2 分别用 /、//、% 计算;再对 -7 和 2 做同样操作,并解释 -7 // 2 为何是 -4 而 -7 % 2 是 1(验证 7 == 2*(-4) + 1)。
  10. 参考答案要点Python 规定 a == b*(a//b) + a%b,且余数与除数同号。-7//2 == -4 是「向下取整」;若要向零截断用 int(-7/2) == -3。
  11. 安全的类型转换器:写一个函数 safe_convert(value, target, default=None),支持把输入转成 int/float/str/bool,转换失败返回 default。用至少 8 组数据测试,包括正常值、空字符串、None、汉字、列表。
  12. 参考答案要点
    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") 失败)
  13. 不可变性的证明:用 id() 证明字符串是不可变的:定义 s = "hello",分别用 s.upper()、s + "!"、s.replace("l", "L") 处理,打印每次操作前后的 id 和内容,说明为什么原字符串没变。
  14. 参考答案要点每次操作都返回新的字符串对象(id 不同),原 s 的 id 不变且内容仍是 "hello"。因为字符串不可变,任何「修改」都必须生成新对象。
  15. 位运算实现权限系统:用 READ=1<<0、WRITE=1<<1、EXECUTE=1<<2 定义三个权限。给用户分配「读+写」,用 & 检查是否有读权限;再加执行权限;最后用 &= ~WRITE 移除写权限。每次操作后打印权限值的二进制表示。
  16. 参考答案要点
    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
  17. 综合:BMI 计算与分类:写一个程序,接收姓名、身高(米)、体重(千克),用 Decimal 计算 BMI = 体重 / 身高²,并按中国标准分类(<18.5 偏瘦,18.5–23.9 正常,24–27.9 超重,≥28 肥胖)。要求对身高、体重做安全转换与合法性校验。
  18. 参考答案要点
    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)}")
  19. 读懂真实报错:运行下面每行代码,记录抛出的异常类型和错误信息,并各写一句解释:1 + "1"、[1,2][5]、"a" * 3、int("3.5")、None + 1、[1,2] + (3,)。
  20. 参考答案要点依次为 TypeError / IndexError / 无错输出 'aaa' / ValueError / TypeError / TypeError。第 3 行不报错,因为 * 对字符串是重复——这正是「类型决定操作含义」的体现。
Python 完全教程 · 第 2 章