跳转至

第 2 章 数据类型与转换

本章配套 3 道例题,见 §例题。


1 六大标准数据类型

类型 关键字 可变? 有序? 竞赛角色
数字 int float bool complex 不可变 — 一切的基础
字符串 str 不可变 有序 字符串题、输入解析
列表 list 可变 有序 数组、栈、邻接表
元组 tuple 不可变 有序 可哈希的复合键、堆中元素
集合 set 可变 无序 去重、\(O(1)\) 判存在
字典 dict 可变 插入有序(3.7+) 哈希表、计数、记忆化

「可变 / 不可变」是 Python 里最重要的一条分界线,它决定了:

  1. 能不能作为 dict 的键或 set 的元素——只有不可变对象可哈希。

为什么?dict 和 set 用哈希值决定元素存放在哪个槽位。 如果一个对象放进去之后还能被改,它的哈希值就会跟着变, 而它的实际位置不会自动搬家——再去查它时会算出新槽位、扑空, 于是「明明放进去了却找不到」。禁止可变对象当键,就是从源头堵死这种情况。 2. 赋值时是复制还是共享引用——见 §6。

d = {(1, 2): "ok"}       # ✅ 元组可以作键
d = {[1, 2]: "no"}       # ❌ TypeError: unhashable type: 'list'

2 整数 int:Python 最大的竞赛优势

Python 的 int 没有位数上限,只受内存限制。这一条直接消灭了 C++ 选手的两大痛点:

>>> 2 ** 1000
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
>>> 12345678901234567890 * 98765432109876543210
1219326311370217952237463801111263526900

不会溢出,不需要开 long long,不需要写高精度。 10^18 级别的乘法在 C++ 里要小心翻倍溢出,Python 直接算。

代价是慢。大整数运算是 \(O(\text{位数})\) 甚至更高,不是 \(O(1)\)。 一个 \(10^6\) 位的数做一次乘法可能要几秒。所以:

能取模就取模——说的是每乘一步就取一次模,而不是把整个乘积累出来再取模。 单看一句 (a * b) % MOD,a * b 当然是先完整乘完的,% 改变不了求值顺序; 这条建议真正针对的是连乘的循环:

# ❌ prod 一路膨胀:n 个 10^9 相乘就是约 30n 个二进制位
prod = 1
for x in a:
    prod *= x
print(prod % MOD)

# ✅ 每轮结束 prod 都被压回 [0, MOD),任何时刻的中间值都不超过 MOD²
prod = 1
for x in a:
    prod = prod * x % MOD

两种写法结果一样,差别全在中间值的位数上:前者第 \(i\) 次乘法要扫 \(O(i)\) 位, 累计 \(O(n^2)\);后者每次乘法的两个操作数都在 \(10^9\) 量级,是常数时间。

如果操作数本身就可能远超 MOD(例如刚读进来还没处理的输入), 那就先各自取模再乘:(a % MOD) * (b % MOD) % MOD。 两个数已经落在 \([0, \text{MOD})\) 内时这两次取模是多余的,写 a * b % MOD 就够。

进制表示

0b1011        # 二进制 = 11
0o17          # 八进制 = 15
0xff          # 十六进制 = 255
1_000_000     # 下划线分隔符,纯粹为了可读性,值是 1000000

整数除法与取模的负数语义

这是 C++ 选手最容易栽的地方:

>>>  7 //  2      # 3
>>> -7 //  2      # -4    ← C++ 是 -3
>>>  7 // -2      # -4
>>> -7 %   2      # 1     ← C++ 是 -1
>>>  7 %  -2      # -1

Python 的 // 是向下取整(floor division),% 的结果符号跟除数一致。 C/C++ 的 / 是向零截断,% 的符号跟被除数一致。

两个实战后果:

  1. 取模永远是非负的(当模数为正),所以 (x % MOD) 不需要再 + MOD 修正:
MOD = 10 ** 9 + 7
print((-5) % MOD)      # 1000000002,已经是正的

C++ 里必须写 ((x % MOD) + MOD) % MOD,Python 不用。这是个好处。

  1. 向零截断要自己写:
import math
math.trunc(-7 / 2)          # -3
int(-7 / 2)                 # -3(int() 对 float 是截断)
-(-7 // 2) if ... else ...  # 别这么绕

或者用 math.floor / math.ceil 明确表达意图。整数除法向上取整的惯用写法:

(a + b - 1) // b            # a/b 向上取整(a, b 均为正)
-(-a // b)                  # 同上,且对负数也正确

其它整数操作

divmod(17, 5)          # (3, 2)  一次拿到商和余数
abs(-3)                # 3
pow(2, 10)             # 1024
pow(2, 100, 1000)      # 快速幂取模!内置的,C 实现,见第 81 章
(255).bit_length()     # 8      表示该数需要多少位
int.from_bytes(...)    # 字节转整数

pow(a, b, m) 是内置三参数快速幂,不要自己写——内置版是 C 实现, 比手写的 Python 快速幂快一个数量级。详见 快速幂与逆元。


3 浮点数 float

float 是 IEEE 754 双精度,约 15–17 位有效数字。

>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

永远不要用 == 比较浮点数:

EPS = 1e-9
if abs(x - y) < EPS:            # ✅
    ...

竞赛里的浮点原则:

  1. 能用整数就别用浮点。判断 \(a/b = c/d\) 应写成 \(a \cdot d = b \cdot c\)。
  2. 需要精确十进制时用 decimal.Decimal(见 浮点与科学计数法)。
  3. 需要精确分数时用 fractions.Fraction。

float 的特殊值:

float("inf")     # 正无穷,常用作初始最小值
float("-inf")    # 负无穷
float("nan")     # 非数,nan != nan 恒成立
math.inf         # 同 float("inf"),更常用

DP 初始化和最短路初始化都用 inf:

dist = [math.inf] * n

4 布尔 bool

bool 是 int 的子类,True == 1、False == 0:

>>> True + True
2
>>> sum([True, False, True])
2
>>> isinstance(True, int)
True

这带来一个极其好用的技巧——布尔值可以直接求和计数:

cnt = sum(x > 0 for x in a)             # 统计正数个数
cnt = sum(s[i] != t[i] for i in range(n))   # 统计不同位置数

假值(Falsy)清单

以下对象在布尔上下文里视为假,其余全为真:

False   None   0   0.0   0j   ''   []   ()   {}   set()   range(0)

所以判空可以直接写:

if not a:            # a 是空列表 / 空串 / 0 / None
    ...
while stack:         # 栈非空
    ...

坑:if not a 无法区分「空列表」和「None」和「0」。 当 0 是合法值时,必须写 if a is None。


5 复数 complex

z = 3 + 4j
z.real, z.imag       # 3.0, 4.0
abs(z)               # 5.0,即模长

竞赛中几乎只有一个用途:当二维点/向量用。

p = complex(x, y)
q = complex(u, v)
abs(p - q)                   # 两点距离,比 sqrt((x-u)**2 + (y-v)**2) 简洁
(q - p) * 1j                 # 向量逆时针旋转 90°
((q - p).conjugate() * (r - p)).imag   # 叉积(可判左右转)

见 计算几何入门。


6 可变性、id 与 is

a = [1, 2, 3]
b = a                # b 和 a 指向同一个列表
b.append(4)
print(a)             # [1, 2, 3, 4]  ← a 也变了!

c = a[:]             # 切片总是新建一个列表,把原来的元素引用逐个抄过去(浅拷贝)
c.append(5)
print(a)             # [1, 2, 3, 4]  ← 这次 a 没变

id(x) 返回对象的身份标识(CPython 里是内存地址)。 x is y 判断是不是同一个对象,x == y 判断值是否相等:

a = [1, 2]; b = [1, 2]
a == b        # True   值相等
a is b        # False  不是同一个对象

is 只应该用于和 None、True、False 比较:if x is None。 拿 is 比较数字或字符串会产生「有时对有时错」的诡异行为,根源是两个优化:

  • 小整数缓存:解释器启动时就把 \(-5\) 到 \(256\) 这些常用小整数各建好一个对象, 之后所有用到它们的地方共享同一份。所以 a = 100; b = 100 时 a is b 是 True, 换成 1000 就成了 False——两次分别建了新对象。
  • 字符串驻留:源码里形如标识符的字符串字面量会被登记进一张全局表并共享, 但运行时拼出来的字符串通常不进这张表。

两者都是为了省内存和加快比较,并不保证哪些值一定被共享; 依赖它写判断,换个 Python 版本就可能翻车。判相等永远用 ==。

浅拷贝的经典陷阱见 列表。


7 类型转换全表

Python 的类型转换是显式的函数调用,没有隐式类型转换(除了数字之间的自动提升)。

数字之间

函数 作用 注意
int(x) 转整数 对 float 是向零截断,不是四舍五入
int(s, base) 按进制解析字符串 int("ff", 16) → 255,int("1011", 2) → 11
float(x) 转浮点 float("1e9") 合法
complex(re, im) 转复数
bool(x) 转布尔 按 §4 的假值表
>>> int(3.9)         # 3   截断,不是 4
>>> int(-3.9)        # -3  向零,不是 -4
>>> round(3.5)       # 4
>>> round(2.5)       # 2   ← 银行家舍入!见第 23 章

int(s, base) 的 base 可以是 2–36,也可以是 0(自动识别 0b/0o/0x 前缀):

int("0b1011", 0)     # 11
int("0xff", 0)       # 255

数字 ↔ 字符串

方向 函数 例子
数 → 十进制串 str(n) str(255) → "255"
数 → 二进制串 bin(n) bin(11) → "0b1011"
数 → 八进制串 oct(n) oct(15) → "0o17"
数 → 十六进制串 hex(n) hex(255) → "0xff"
串 → 数 int(s, base) int("1011", 2) → 11

bin/oct/hex 带前缀,去掉前缀用切片或格式化:

bin(11)[2:]          # "1011"
format(11, "b")      # "1011"        推荐
format(11, "08b")    # "00001011"    补零到 8 位
format(255, "X")     # "FF"          大写十六进制

int(s) 的位数上限:出于安全考虑(超长数字串的进制转换是平方复杂度, 可被用来发起拒绝服务攻击),字符串转整数默认最多 4300 位。 这个限制在 3.11 引入,并回溯到了 3.9.14、3.10.7 等安全更新版本。 超了会抛 ValueError。需要时用 sys.set_int_max_str_digits(0) 解除。 竞赛中很少触发,但处理超长数字串时要知道。

容器之间

函数 作用 注意
list(x) 转列表 list("abc") → ['a','b','c']
tuple(x) 转元组
set(x) 转集合 会去重、丢失顺序
frozenset(x) 转不可变集合 可作 dict 键
dict(pairs) 由键值对建字典 dict([(1,'a'),(2,'b')])
str(x) 转字符串 对容器返回其 repr,如 "[1, 2]"
list("hello")                       # ['h','e','l','l','o']
"".join(['h','e','l','l','o'])      # 'hello'    ← 反向操作
list(map(int, "12345"))             # [1, 2, 3, 4, 5]  按位拆数字
dict(zip(keys, values))             # 两个列表压成字典

str(list) 不是把列表转成字符串!str([1,2]) 得到的是 "[1, 2]"(含方括号)。 想拼接元素用 "".join(map(str, a))。


8 例题

题号 题名 难度 题解
BISHI30 二进制数1 简单 题解
BISHI31 二进制不同位数 简单 题解
BISHI14 特殊的科学计数法 简单 题解

BISHI30 二进制数1(简单,位运算)

给定整数,统计其二进制表示中 1 的个数(数位相关,具体题面见 原题)。

三种写法,都用到本章的类型转换:

n = int(input())
print(bin(n).count("1"))            # 转成二进制串再数 1,最直白

等价写法(任选其一,都只输出一行):

# [片段]
format(n, "b").count("1")           # 不带 0b 前缀,效果相同
n.bit_count()                       # 最快,但 Python 3.10+ 才有,3.9 环境不可用

如果要手写(面试常问),用 n & (n - 1) 消去最低位的 1:

cnt = 0
while n:
    n &= n - 1                       # 每次消掉一个 1
    cnt += 1

原理见 位运算。

BISHI31 二进制不同位数(简单,位运算)

题面见 原题。

求两个数二进制表示中不同的位数。

异或后数 1 —— 异或的定义就是「相同为 0,不同为 1」:

a, b = map(int, input().split())
print(bin(a ^ b).count("1"))

BISHI14 特殊的科学计数法(简单)

解析科学计数法表示的数(题面见 原题)。

科学计数法的解析是类型转换的实战场景。关键判断:结果需要精确,还是允许浮点误差?

  • 允许误差 → float("1.23e5") 直接可用,Python 原生支持。
  • 需要精确 → 用 Decimal("1.23e5"),避免二进制浮点丢精度。
# [片段] 这里只演示「大整数不经过 float 也能安全解析」,完整题解见第 23 章
from decimal import Decimal

s = input().strip()
d = Decimal(s)                  # Decimal 原生支持 e 记号,且从字符串构造是精确的

完整题解与那道题的舍入/进位溢出细节,见 浮点与科学计数法 §6。


9 本章速查

场景 写法
大整数 直接算,不需要高精度;连乘循环里每步取模 prod = prod * x % MOD
向上取整 (a + b - 1) // b 或 -(-a // b)
模运算取正 直接 x % MOD,Python 保证非负
快速幂取模 pow(a, b, m),别手写
浮点比较 abs(x - y) < 1e-9
无穷大 math.inf
布尔计数 sum(cond for x in a)
判空 if not a;但 0 是合法值时用 is None
数转二进制串 format(n, "b"),不带前缀
串按进制转数 int(s, base)
数字逐位拆开 list(map(int, str(n)))
列表拼成串 "".join(map(str, a)),不是 str(a)
判同一对象 is 只用于 None/True/False