Skip to content

Python 基本数据类型

Python 中的基本数据类型可以分为以下几类:数字类型字符串布尔类型列表元组字典集合空值类型

1. 数字类型(Number)

1.1 整数(int)

Python 的整数没有大小限制,可以表示任意大的整数。

python
# 整数
a = 10
b = -5
c = 0

print(type(a))  # <class 'int'>
print(a + b)    # 5
print(a * b)    # -50

# Python 整数可以非常大
big_num = 10 ** 100
print(big_num)  # 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

# 不同进制表示
binary = 0b1010      # 二进制
octal = 0o12         # 八进制
hexadecimal = 0xA    # 十六进制

print(binary)        # 10
print(octal)         # 10
print(hexadecimal)   # 10

1.2 浮点数(float)

浮点数用于表示小数,遵循 IEEE 754 双精度标准。

python
# 浮点数
f1 = 3.14
f2 = -0.5
f3 = 1.0

print(type(f1))  # <class 'float'>

# 科学计数法
f4 = 1.5e2    # 1.5 × 10²
f5 = 3e-3     # 3 × 10⁻³

print(f4)  # 150.0
print(f5)  # 0.003

# ⚠️ 浮点数精度问题
print(0.1 + 0.2)          # 0.30000000000000004
print(0.1 + 0.2 == 0.3)   # False

# 使用 round() 或 decimal 模块解决精度问题
print(round(0.1 + 0.2, 1))  # 0.3

from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2'))  # 0.3

1.3 复数(complex)

复数由实部和虚部组成,虚部用 jJ 表示。

python
# 复数
c1 = 3 + 4j
c2 = complex(1, 2)  # 1 + 2j

print(type(c1))    # <class 'complex'>
print(c1.real)     # 3.0  (实部)
print(c1.imag)     # 4.0  (虚部)
print(c1 + c2)     # (4+6j)
print(abs(c1))     # 5.0  (模:√(3² + 4²))

1.4 数字类型转换

python
# 类型转换
print(int(3.9))       # 3      (截断小数,不是四舍五入)
print(int("42"))      # 42     (字符串转整数)
print(float(10))      # 10.0
print(float("3.14"))  # 3.14
print(complex(3))     # (3+0j)

# 常用数学运算
print(10 / 3)    # 3.3333333333333335  (真除法)
print(10 // 3)   # 3                   (整除/地板除)
print(10 % 3)    # 1                   (取余)
print(2 ** 10)   # 1024                (幂运算)

# 内置数学函数
print(abs(-7))       # 7
print(max(1, 5, 3))  # 5
print(min(1, 5, 3))  # 1
print(divmod(10, 3)) # (3, 1)  (同时返回商和余数)

2. 字符串(str)

字符串是不可变的 Unicode 字符序列。

2.1 创建字符串

python
# 单引号、双引号、三引号
s1 = 'hello'
s2 = "world"
s3 = '''多行
字符串'''
s4 = """也是
多行字符串"""

print(type(s1))  # <class 'str'>
print(s3)
# 多行
# 字符串

# 转义字符
print("hello\tworld")    # hello	world
print("hello\nworld")    # hello(换行)world
print("路径:C:\\Users")   # 路径:C:\Users

# 原始字符串(忽略转义)
print(r"C:\new\test")    # C:\new\test

2.2 字符串索引与切片

python
s = "Hello, Python!"

# 索引(从 0 开始,支持负索引)
print(s[0])     # H
print(s[-1])    # !
print(s[7])     # P

# 切片 [start:end:step],左闭右开
print(s[0:5])    # Hello
print(s[7:])     # Python!
print(s[:5])     # Hello
print(s[::2])    # Hlo yhn
print(s[::-1])   # !nohtyP ,olleH  (反转字符串)

2.3 常用字符串方法

python
s = "  Hello, World!  "

# 大小写转换
print(s.upper())        # "  HELLO, WORLD!  "
print(s.lower())        # "  hello, world!  "
print(s.title())        # "  Hello, World!  "
print(s.capitalize())   # "  hello, world!  "
print(s.swapcase())     # "  hELLO, wORLD!  "

# 去除空白
print(s.strip())    # "Hello, World!"
print(s.lstrip())   # "Hello, World!  "
print(s.rstrip())   # "  Hello, World!"

# 查找与替换
text = "Hello, Python! Hello, World!"
print(text.find("Hello"))       # 0   (返回首次出现的索引)
print(text.find("Java"))        # -1  (未找到返回 -1)
print(text.count("Hello"))      # 2   (出现次数)
print(text.replace("Hello", "Hi"))  # "Hi, Python! Hi, World!"
print(text.translate(text.maketrans(',!',',!'))) # 'Hello, Python! Hello, World!' (按规则一次性替换多个字符)

# 分割与拼接
csv = "apple,banana,cherry"
fruits = csv.split(",")
print(fruits)                   # ['apple', 'banana', 'cherry']
print(" | ".join(fruits))       # "apple | banana | cherry"
# str.partition(sep) 按照分隔符 sep 切分字符串,返回一个包含三个成员的元(part_before, sep, part_after)
print(csv.partiton(','))  	    # ('apple', ',', 'banana,cherry')

# 判断方法
print("abc123".isalnum())    # True  (字母或数字)
print("abc".isalpha())       # True  (纯字母)
print("123".isdigit())       # True  (纯数字)
print("hello".islower())     # True
print("HELLO".isupper())     # True
print("  ".isspace())        # True
print("Hello".startswith("He"))  # True
print("Hello".endswith("lo"))    # True

2.4 字符串格式化

python
name = "Alice"
age = 25
score = 95.5

# 1. f-string(推荐,Python 3.6+)
print(f"我叫{name},今年{age}岁")           # 我叫Alice,今年25岁
print(f"成绩:{score:.1f}")                 # 成绩:95.5
print(f"十六进制:{255:#x}")                # 十六进制:0xff
print(f"{'居中':=^20}")                     # ========居中========

# 2. format() 方法
print("{}今年{}岁".format(name, age))       # Alice今年25岁
print("{1}今年{0}岁".format(age, name))     # Alice今年25岁
print("{n}今年{a}岁".format(n=name, a=age)) # Alice今年25岁

# 3. % 格式化(旧式)
print("我叫%s,今年%d岁" % (name, age))     # 我叫Alice,今年25岁
print("圆周率约为%.2f" % 3.14159)           # 圆周率约为3.14

3. 布尔类型(bool)

布尔类型是 int 的子类,只有 True(1)和 False(0)两个值。

python
# 布尔值
t = True
f = False

print(type(t))       # <class 'bool'>
print(isinstance(t, int))  # True  (bool 是 int 的子类)

# 布尔运算
print(True and False)   # False
print(True or False)    # True
print(not True)         # False

# True/False 可以参与数值运算
print(True + True)   # 2
print(True * 10)     # 10
print(False + 1)     # 1

# 假值(Falsy)—— 以下值在布尔上下文中为 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

# 真值(Truthy)—— 非零、非空的值为 True
print(bool(1))        # True
print(bool(-1))       # True
print(bool("hello"))  # True
print(bool([1, 2]))   # True

# 比较运算返回布尔值
print(3 > 2)      # True
print(3 == 2)      # False
print(3 != 2)      # True
print(1 <= 1)      # True

# 链式比较
x = 5
print(1 < x < 10)    # True  (等价于 1 < x and x < 10)
print(1 < x > 3)     # True

4. 列表(list)

列表是有序、可变的序列,可以存储任意类型的元素。

4.1 创建列表

python
# 创建列表
list1 = [1, 2, 3, 4, 5]
list2 = ["a", "b", "c"]
list3 = [1, "hello", True, 3.14, [1, 2]]  # 可以混合类型
list4 = list()         # 空列表
list5 = list("hello")  # ['h', 'e', 'l', 'l', 'o']
list6 = list(range(5)) # [0, 1, 2, 3, 4]

print(type(list1))  # <class 'list'>
print(len(list1))   # 5

4.2 列表索引与切片

python
nums = [10, 20, 30, 40, 50]

# 索引
print(nums[0])     # 10
print(nums[-1])    # 50

# 切片(与字符串相同)
print(nums[1:3])   # [20, 30]
print(nums[::-1])  # [50, 40, 30, 20, 10]

# 修改元素
nums[0] = 100
print(nums)  # [100, 20, 30, 40, 50]

# 切片赋值
nums[1:3] = [200, 300]
print(nums)  # [100, 200, 300, 40, 50]

4.3 列表常用方法

python
fruits = ["apple", "banana"]

# 添加元素
fruits.append("cherry")         # 末尾追加
print(fruits)                   # ['apple', 'banana', 'cherry']

fruits.insert(1, "blueberry")   # 指定位置插入
print(fruits)                   # ['apple', 'blueberry', 'banana', 'cherry']

fruits.extend(["date", "fig"])  # 扩展列表
print(fruits)                   # ['apple', 'blueberry', 'banana', 'cherry', 'date', 'fig']

# 删除元素
fruits.remove("blueberry")     # 按值删除(只删第一个匹配)
print(fruits)                  # ['apple', 'banana', 'cherry', 'date', 'fig']

popped = fruits.pop()          # 弹出末尾元素
print(popped)                  # fig
print(fruits)                  # ['apple', 'banana', 'cherry', 'date']

popped = fruits.pop(1)         # 弹出指定索引
print(popped)                  # banana

del fruits[0]                  # 按索引删除
print(fruits)                  # ['cherry', 'date']

# 排序
nums = [3, 1, 4, 1, 5, 9, 2, 6]
nums.sort()                    # 原地升序排序
print(nums)                    # [1, 1, 2, 3, 4, 5, 6, 9]

nums.sort(reverse=True)        # 原地降序排序
print(nums)                    # [9, 6, 5, 4, 3, 2, 1, 1]

sorted_nums = sorted([3, 1, 2])  # 返回新列表,不修改原列表
print(sorted_nums)                # [1, 2, 3]

# 其他方法
nums = [1, 2, 3, 2, 1]
print(nums.count(2))     # 2      (统计出现次数)
print(nums.index(3))     # 2      (返回首次出现的索引)
nums.reverse()           # 原地反转
print(nums)              # [1, 2, 3, 2, 1]

4.4 列表推导式

python
# 基本推导式
squares = [x ** 2 for x in range(1, 6)]
print(squares)  # [1, 4, 9, 16, 25]

# 带条件的推导式
evens = [x for x in range(10) if x % 2 == 0]
print(evens)  # [0, 2, 4, 6, 8]

# 嵌套推导式
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# 带条件表达式的推导式
labels = ["偶数" if x % 2 == 0 else "奇数" for x in range(5)]
print(labels)  # ['偶数', '奇数', '偶数', '奇数', '偶数']

5. 元组(tuple)

元组是有序、不可变的序列。一旦创建就不能修改。

“括号”其实不是定义元组的关键标志——直接删掉两侧括号,同样也能完成定义,“逗号”才是让解释器判定为元组的关键

python
# 创建元组
t1 = (1, 2, 3)
t2 = ("a", "b", "c")
t3 = tuple([1, 2, 3])   # 从列表转换
t4 = ()                  # 空元组
t5 = (42,)               # 单元素元组必须加逗号

print(type(t1))  # <class 'tuple'>

# 注意:没有逗号不是元组
not_tuple = (42)
print(type(not_tuple))  # <class 'int'>

# 索引与切片(与列表相同)
print(t1[0])      # 1
print(t1[-1])     # 3
print(t1[1:])     # (2, 3)

# 元组不可变
# t1[0] = 10  # TypeError: 'tuple' object does not support item assignment

# 元组方法
t = (1, 2, 3, 2, 1)
print(t.count(2))   # 2
print(t.index(3))   # 2

# 元组拆包
a, b, c = (10, 20, 30)
print(a, b, c)  # 10 20 30

# 使用 * 收集剩余元素
first, *rest = (1, 2, 3, 4, 5)
print(first)  # 1
print(rest)   # [2, 3, 4, 5]

# 交换变量(本质是元组拆包)
x, y = 1, 2
x, y = y, x
print(x, y)  # 2 1

# 元组 vs 列表
# 1. 元组不可变,更安全
# 2. 元组可以作为字典的 key,列表不行
# 3. 元组占用更少的内存
import sys
print(sys.getsizeof([1, 2, 3]))   # 88(列表)
print(sys.getsizeof((1, 2, 3)))   # 64(元组)

5.1 返回多个结果,其实就是返回元组

函数可以一次返回多个结果,这其实是通过返回一个元组来实现的:

python
def get_rectangle():
    """返回长方形的宽和高"""
    width = 100
    height = 20
    return width, height


# 获取函数的多个返回值
result = get_rectangle()
print(result, type(result))
# 输出:
# (100, 20) <class 'tuple'>

将函数返回值一次赋值给多个变量时,其实就是对元组做了一次解包操作:

python
width, height = get_rectangle()
# 可以理解为:width, height = (width, height)

5.2 没有“元组推导式”

表达式并没有生成元组,而是返回了一个 生成器(generator)对象。因此它是生成器推导式,而非元组推导式。

不过幸运的是,虽然无法通过推导式直接拿到元组,但生成器仍然是一种可迭代类型,所以我们还是可以对它调用 tuple() 函数,获得元组:

python
>>> results = (n * 100 for n in range(10) if n % 2 == 0)
>>> results
<generator object <genexpr> at 0x10e94e2e0>

>>> results = tuple((n * 100 for n in range(10) if n % 2 == 0))
>>> results
(0, 200, 400, 600, 800)

5.3 存放结构化数据

和列表不同,在同一个元组里出现不同类型的值是很常见的事情,因此元组经常用来存放结构化数据。比如,下面的 user_info 就是一份包含名称、年龄等信息的用户数据:

python
>>> user_info = ('piglei', 'MALE', 30, True)
>>> user_info[2]
30

正因为元组有这个特点,所以 Python 为我们提供了一个特殊的元组类型:具名元组。

5.4 具名元组

python
>>> rectangle = (100, 20)
>>> rectangle[0]
100
>>> rectangle[-1]
20

元组经常用来存放结构化数据,但只能通过数字来访问元组成员其实特别不方便——比如我就完全记不住上面的 rectangle[0] 到底代表长方形的宽度还是高度。

为了解决这个问题,我们可以使用一种特殊的元组: 具名元组(namedtuple)。具名元组在保留普通元组功能的基础上,允许为元组的每个成员命名,这样便能通过名称而不止是数字索引访问成员。

创建具名元组需要用到 namedtuple() 函数,它位于标准库的 collections 模块里,使用前需要先导入:

python
from collections import namedtuple

Rectangle = namedtuple('Rectangle', 'width,height')

除了用逗号来分隔具名元组的字段名称以外,还可以用空格分隔: 'width height',或是直接使用一个字符串列表: ['width', 'height']

在 Python 3.6 版本以后,除了使用 namedtuple() 函数以外,你还可以用 typing.NamedTuple 和类型注解语法来定义具名元组类型。这种方式在可读性上更胜一筹:

python
class Rectangle(NamedTuple):
    width: int
    height: int

rect = Rectangle(100, 20)

需要注意的是,上面的写法虽然给 widthheight 加了类型注解,但 Python 在执行时并不会做真正的类型校验。也就是说,下面这段代码也能正常执行:

python
# 提供错误的类型来初始化
rect_wrong_type = Rectangle('string', 'not_a_number')

和普通元组比起来,使用具名元组的好处很多。其中最直观的一点就是:用名字访问成员( rect.width)比用普通数字( rect[0])更易读、更好记。

6. 字典(dict)

字典是键值对的无序集合(Python 3.7+ 保持插入顺序),键必须是不可变类型。

6.1 创建字典

python
# 创建字典
d1 = {"name": "Alice", "age": 25, "city": "Beijing"}
d2 = dict(name="Bob", age=30)
d3 = dict([("a", 1), ("b", 2)])
d4 = {}          # 空字典
d5 = dict()      # 空字典

# 使用 fromkeys 创建
d6 = dict.fromkeys(["a", "b", "c"], 0)
print(d6)  # {'a': 0, 'b': 0, 'c': 0}

print(type(d1))  # <class 'dict'>
print(len(d1))   # 3

6.2 访问与修改

python
person = {"name": "Alice", "age": 25, "city": "Beijing"}

# 访问
print(person["name"])          # Alice
# print(person["email"])       # ❌ KeyError

print(person.get("name"))      # Alice
print(person.get("email"))     # None  (不存在返回 None)
print(person.get("email", "未设置"))  # 未设置  (指定默认值)

# 修改 / 添加
person["age"] = 26              # 修改已有键
person["email"] = "a@test.com"  # 添加新键
print(person)  # {'name': 'Alice', 'age': 26, 'city': 'Beijing', 'email': 'a@test.com'}

# update 批量更新
person.update({"age": 27, "phone": "123"})
print(person)  # {'name': 'Alice', 'age': 27, 'city': 'Beijing', 'email': 'a@test.com', 'phone': '123'}

# 删除
del person["phone"]
print(person)  # {'name': 'Alice', 'age': 27, 'city': 'Beijing', 'email': 'a@test.com'}

popped = person.pop("email")
print(popped)   # a@test.com

last = person.popitem()  # 弹出最后一个键值对
print(last)     # ('city', 'Beijing')

6.3 字典遍历

python
scores = {"Alice": 90, "Bob": 85, "Charlie": 92}

# 遍历键
for name in scores:
    print(name, end=" ")         # Alice Bob Charlie
print()

# 遍历值
for score in scores.values():
    print(score, end=" ")        # 90 85 92
print()

# 遍历键值对
for name, score in scores.items():
    print(f"{name}: {score}")
# Alice: 90
# Bob: 85
# Charlie: 92

# 检查键是否存在
print("Alice" in scores)     # True
print("David" in scores)     # False
print("Alice" not in scores) # False

6.4 字典推导式

python
# 基本推导式
squares = {x: x ** 2 for x in range(1, 6)}
print(squares)  # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# 带条件
even_squares = {x: x ** 2 for x in range(1, 6) if x % 2 == 0}
print(even_squares)  # {2: 4, 4: 16}

# 反转键值
original = {"a": 1, "b": 2, "c": 3}
reversed_dict = {v: k for k, v in original.items()}
print(reversed_dict)  # {1: 'a', 2: 'b', 3: 'c'}

6.5 字典的常用操作

6.5.1 遍历字典

python
# 遍历获取字典所有的 key
for key in movie:
   print(key, movie[key])

# 一次获取字典的所有 key: value 键值对
for key, value in movie.items():
   print(key, value)

6.5.2 访问不存在的字典键

当用不存在的键访问字典内容时,程序会抛出 KeyError 异常,我们通常称之为程序里的 边界情况(edge case)。针对这种边界情况,比较常见的处理方式有两种:

(1) 读取内容前先做一次条件判断,只有判断通过的情况下才继续执行其他操作;

(2) 直接操作,但是捕获 KeyError 异常。

第一种写法:

python
if 'rating' in movie:
   rating = movie['rating']
else:
   rating = 0

第二种写法:

python
try:
   rating = movie['rating']
except KeyError:
   rating = 0

6.5.3 使用 setdefault 取值并修改

有时,我们需要修改字典中某个可能不存在的键,比如在下面的代码里,我需要往字典 ditems 键里追加新值,但 d['items'] 可能根本就不存在

python
try:
    d['items'].append(value)
except KeyError:
    d['items'] = [value]

可以使用dict.setdefault(key, default),视条件的不同,调用 dict.setdefault(key, default) 会产生两种结果:当 key 不存在时,该方法会把 default 值写入字典的 key 位置,并返回该值;假如 key 已经存在,该方法就会直接返回它在字典中的对应值。代码如下:

python
>>> d = {'title': 'foobar'}
>>> d.setdefault('items', []).append('foo') # 若 `key` 不存在,以空列表 `[]` 初始化并返回
>>> d
{'title': 'foobar', 'items': ['foo']}
>>> d.setdefault('items', []).append('bar') #  若 `key` 存在,直接返回旧值
>>> d
{'title': 'foobar', 'items': ['foo', 'bar']}

6.5.4 使用 pop 方法删除不存在的键

如果我们想删除字典里的某个键,一般会使用 del d[key] 语句;但如果要删除的键不存在,该操作就会抛出 KeyError 异常。

python
try:
    del d[key]
except KeyError:
    # 忽略 key 不存在的情况
    pass

但假设你只是单纯地想去掉某个键,并不关心它存在与否、删除有没有成功,那么使用 dict.pop(key, default) 方法就够了。只要在调用 pop 方法时传入默认值 None,在键不存在的情况下也不会产生任何异常:

python
d.pop(key, None)

注意:严格说来, pop 方法的主要用途并不是删除某个键,而是取出这个键对应的值。但我个人觉得,偶尔用它来执行删除操作也无伤大雅

6.6 字典的有序性和无序性

在 Python 3.6 版本以前,几乎所有开发者都遵从一条常识:“Python 的字典是无序的。”这里的无序指的是:当你按照某种顺序把内容存进字典后,就永远没法按照原顺序把它取出来了。

python
d = {}
d['FIRST_KEY'] = 1
d['SECOND_KEY'] = 2

for key in d:
    print(key)
    
# 2.7版本运行结果为
# SECOND_KEY
# FIRST_KEY
# 这种无序现象,是由字典的底层实现所决定的。

Python 里的字典在底层使用了 哈希表(hash table)数据结构。当你往字典里存放一对 key: value 时,Python 会先通过哈希算法计算出 key 的哈希值——一个整型数字;然后根据这个哈希值,决定数据在表里的具体位置。

因此,最初的内容插入顺序,在这个哈希过程中被自然丢掉了,字典里的内容顺序变得仅与哈希值相关,与写入顺序无关。在很长一段时间里,字典的这种无序性一直被当成一个常识为大家所接受。

字典变为有序只是作为 3.6 版本的“隐藏特性”存在。但到了 3.7 版本,它已经彻底成了语言规范的一部分。

如果你使用的 Python 版本没有那么新,也可以从 collections 模块里方便地拿到另一个有序字典对象 OrderedDict,它可以在 Python 3.7 以前的版本里保证字典有序:

python
from collections import OrderedDict
d = OrderedDict()
d['FIRST_KEY'] = 1
d['SECOND_KEY'] = 2

for key in d:
    print(key)
    
# FIRST_KEY
# SECOND_KEY

另外从功能上来说, OrderedDict 与新版本的字典其实也有着一些细微区别。比如,在对比两个内容相同而顺序不同的字典对象时,解释器会返回 True 结果;但如果是 OrderedDict 对象,则会返回 False

python
>>> d1 = {'name': 'piglei', 'fruit': 'apple'}
>>> d2 = {'fruit': 'apple', 'name': 'piglei'}
>>> d1 == d2  # 内容一致而顺序不同的字典被视作相等,因为解释器只对比字典的键和值是否一致
True

>>> d1 = OrderedDict(name='piglei', fruit='apple')
>>> d2 = OrderedDict(fruit='apple', name='piglei')
>>> d1 == d2 # 同样的 `OrderedDict` 则被视作不相等,因为“键的顺序”也会作为对比条件
False

7. 集合(set)

集合是无序、不重复的元素集,支持数学集合运算。

7.1 创建集合

python
# 创建集合
s1 = {1, 2, 3, 4, 5}
s2 = set([1, 2, 2, 3, 3])  # 自动去重
s3 = set("hello")           # {'h', 'e', 'l', 'o'}
s4 = set()                   # 空集合(不能用 {},那是空字典)

print(type(s1))  # <class 'set'>
print(s2)        # {1, 2, 3}
print(s3)        # {'l', 'o', 'e', 'h'}  (无序)

# 集合元素必须是不可变类型
# s = {[1, 2]}  # ❌ TypeError: unhashable type: 'list'
s = {(1, 2), "hello", 42}  # ✅ 元组、字符串、数字可以

正如上面的报错信息所示,集合里只能存放“可哈希”(hashable)的对象。假如把不可哈希的对象(比如上面的列表)放入集合,程序就会抛出 TypeError 异常。在使用集合时,可哈希性是个非常重要的概念,下面我们来看看什么决定了对象的可哈希性。

7.2 了解对象的可哈希性

在介绍字典类型时,我们说过字典底层使用了哈希表数据结构,其实集合也一样。当我们把某个对象放进集合或者作为字典的键使用时,解释器都需要对该对象进行一次哈希运算,得到哈希值,然后再进行后面的操作。

这个计算哈希值的过程,是通过调用内置函数 hash(obj) 完成的。如果对象是可哈希的, hash 函数会返回一个整型结果,否则将会报 TypeError 错误。

因此,要把某个对象放进集合,那它就必须是“可哈希”的。话说到这里,到底哪些类型是可哈希的?哪些又是不可哈希的呢?我们来试试看。

首先,那些不可变的内置类型都是可哈希的:

python
>>> hash('string')
-3407286361374970639
>>> hash(100)
# 有趣的事情,整型的 hash 值就是它自身的值
100
>>> hash((1, 2, 3))
529344067295497451

而可变的内置类型都无法正常计算哈希值:

python
>>> hash({'key': 'value'})
TypeError: unhashable type: 'dict'
>>> hash([1, 2, 3])
TypeError: unhashable type: 'list'

可变类型的不可哈希特点有一定的“传染性”。比如在一个原本可哈希的元组里放入可变的列表对象后,它也会马上变得不可哈希:

python
>>> hash((1, 2, 3, ['foo', 'bar']))
TypeError: unhashable type: 'list'

由用户定义的所有对象默认都是可哈希的:

python
>>> foo = Foo()
>>> hash(foo)
273594269

总结一下,某种类型是否可哈希遵循下面的规则:

(1) 所有的不可变内置类型,都是可哈希的,比如 strinttuplefrozenset 等;

(2) 所有的可变内置类型,都是不可哈希的,比如 dictlist 等;

(3) 对于不可变容器类型 (tuple, frozenset),仅当它的所有成员都不可变时,它自身才是可哈希的;

(4) 用户定义的类型默认都是可哈希的。

7.3 集合操作

python
s = {1, 2, 3}

# 添加
s.add(4)
print(s)  # {1, 2, 3, 4}

s.update([5, 6])
print(s)  # {1, 2, 3, 4, 5, 6}

# 删除
s.remove(6)     # 删除指定元素,不存在则报错
s.discard(10)   # 删除指定元素,不存在不报错
popped = s.pop() # 随机弹出一个元素
print(s)

s.clear()       # 清空集合
print(s)        # set()

7.4 集合运算

python
a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}

# 并集
print(a | b)           # {1, 2, 3, 4, 5, 6, 7, 8}
print(a.union(b))      # {1, 2, 3, 4, 5, 6, 7, 8}

# 交集
print(a & b)               # {4, 5}
print(a.intersection(b))   # {4, 5}

# 差集
print(a - b)               # {1, 2, 3}
print(a.difference(b))     # {1, 2, 3}

# 对称差集(在 a 或 b 中,但不同时在两者中)
print(a ^ b)                       # {1, 2, 3, 6, 7, 8}
print(a.symmetric_difference(b))   # {1, 2, 3, 6, 7, 8}

# 子集与超集
c = {1, 2}
print(c.issubset(a))      # True   (c 是 a 的子集)
print(a.issuperset(c))    # True   (a 是 c 的超集)
print(a.isdisjoint(b))    # False  (a 和 b 有交集)

# 集合推导式
s = {x ** 2 for x in range(1, 6)}
print(s)  # {1, 4, 9, 16, 25}

# 常见用途:列表去重
nums = [1, 3, 2, 3, 1, 4, 2]
unique = list(set(nums))
print(unique)  # [1, 2, 3, 4]  (顺序可能不同)

7.5 不可变集合(frozenset)

python
# frozenset 不可修改,可以作为字典的键或集合的元素
fs = frozenset([1, 2, 3])
print(fs)           # frozenset({1, 2, 3})
# fs.add(4)         # ❌ AttributeError

# 可以作为字典的键
d = {fs: "value"}
print(d)  # {frozenset({1, 2, 3}): 'value'}

8. 空值类型(NoneType)

None 是 Python 的空值,表示"没有值"。

python
x = None

print(type(x))       # <class 'NoneType'>
print(x is None)     # True  (推荐用 is 判断)
print(x == None)     # True  (不推荐用 == 判断)

# 常见用法
# 1. 函数默认返回值
def greet(name):
    print(f"Hello, {name}")

result = greet("Alice")  # Hello, Alice
print(result)             # None

# 2. 默认参数
def add_item(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

print(add_item(1))       # [1]
print(add_item(2))       # [2]  (每次调用创建新列表)

# 3. 占位 / 初始化
data = None
# ... 后续赋值
data = [1, 2, 3]

9. 类型检查与转换

9.1 类型检查

python
x = 42

# type() —— 返回精确类型
print(type(x))              # <class 'int'>
print(type(x) == int)       # True

# isinstance() —— 支持继承关系检查(推荐)
print(isinstance(x, int))          # True
print(isinstance(x, (int, float))) # True  (可以传入元组检查多个类型)
print(isinstance(True, int))       # True  (bool 是 int 的子类)

9.2 类型转换

python
# 转整数
print(int("42"))       # 42
print(int(3.99))       # 3   (截断,不是四舍五入)
print(int("0xff", 16)) # 255 (指定进制)
print(int("0b1010", 2))# 10
print(int(True))       # 1

# 转浮点数
print(float("3.14"))   # 3.14
print(float(10))       # 10.0
print(float("inf"))    # inf  (正无穷)

# 转字符串
print(str(42))         # "42"
print(str(3.14))       # "3.14"
print(str([1, 2, 3]))  # "[1, 2, 3]"
print(str(True))       # "True"

# 转列表
print(list("hello"))       # ['h', 'e', 'l', 'l', 'o']
print(list((1, 2, 3)))     # [1, 2, 3]
print(list({1, 2, 3}))     # [1, 2, 3]
print(list(range(5)))      # [0, 1, 2, 3, 4]

# 转元组
print(tuple([1, 2, 3]))    # (1, 2, 3)
print(tuple("abc"))        # ('a', 'b', 'c')

# 转集合
print(set([1, 2, 2, 3]))   # {1, 2, 3}
print(set("hello"))        # {'h', 'e', 'l', 'o'}

# 转字典(需要键值对序列)
print(dict([("a", 1), ("b", 2)]))  # {'a': 1, 'b': 2}
print(dict(zip("abc", [1, 2, 3]))) # {'a': 1, 'b': 2, 'c': 3}

# 转布尔
print(bool(0))    # False
print(bool(42))   # True
print(bool(""))   # False
print(bool("hi")) # True

10. 总结对比

类型关键字有序可变可重复示例
整数int---42
浮点数float---3.14
复数complex---3+4j
字符串str"hello"
布尔bool---True / False
列表list[1, 2, 3]
元组tuple(1, 2, 3)
字典dict键不可重复{"a": 1}
集合set{1, 2, 3}
冻结集合frozensetfrozenset({1, 2})
空值NoneType---None

*Python 3.7+ 字典保持插入顺序

11. 可变 vs 不可变

  • 不可变类型:int, float, str, tuple, frozenset, bool, None

  • 可变类型:list, dict, set

python
# 不可变类型 → 修改会创建新对象
a = "hello"
b = a
a = a + " world"
print(a)  # hello world
print(b)  # hello  (b 不受影响)

# 可变类型 → 修改会影响所有引用
a = [1, 2, 3]
b = a          # b 和 a 指向同一个列表
a.append(4)
print(a)  # [1, 2, 3, 4]
print(b)  # [1, 2, 3, 4]  (b 也变了!)

# 要创建独立副本,使用 copy
a = [1, 2, 3]
b = a.copy()   # 或 b = a[:]  或 b = list(a)
a.append(4)
print(a)  # [1, 2, 3, 4]
print(b)  # [1, 2, 3]  (b 不受影响)

# 嵌套结构需要深拷贝
import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
a[0].append(5)
print(a)  # [[1, 2, 5], [3, 4]]
print(b)  # [[1, 2], [3, 4]]  (b 不受影响)

如果要用其他编程语言的术语来解释,分别可以对应两种函数参数传递机制。

(1) 值传递(pass-by-value):调用函数时,传过去的是变量所指向对象(值)的拷贝,因此对函数内变量的任何修改,都不会影响原始变量

(2) 引用传递(pass-by-reference):调用函数时,传过去的是变量自身的引用(内存地址),因此,修改函数内的变量会直接影响原始变量

12. 一些小技巧

如何快速合并字典

在 Python 里,合并两个字典听上去挺简单,实际操作起来比想象中麻烦。下面有两个字典:

python
d1 = {'name': 'apple'}
d2 = {'price': 10}

假如我想合并 d1d2 的值,拿到 {'name': 'apple', 'price': 10},最简单的做法是调用 d1.update(d2),然后 d1 就会变成目标值。但这样做有个问题:它会修改字典 d1 的原始内容,因此并不算无副作用的合并。

要在不修改原字典的前提下合并两个字典,需要定义一个函数:

python
def merge_dict(d1, d2):
    # 因为字典是可修改的对象,为了避免修改原对象
    # 此处需要复制一个 d1 的浅拷贝对象
    result = d1.copy()
    result.update(d2)
    return result

使用 merge_dict 可以拿到合并后的字典:

python
>>> merge_dict(d1, d2)
{'name': 'apple', 'price': 10}

使用这种方式, d1d2 仍然是原来的值,不会因为合并操作被修改。

虽然上面的方案可以完成合并,但显得有些烦琐。使用动态解包表达式可以更简单地完成操作。

要实现合并功能,需要用到双星号 ** 运算符来做解包操作。在字典中使用 **dict_obj 表达式,可以动态解包 dict_obj 字典的所有内容,并与当前字典合并:

python
>>> d1 = {'name': 'apple'}
# 把 d1 解包,与外部字典合并
>>> {'foo': 'bar', **d1}
{'foo': 'bar', 'name': 'apple'}

因为解包过程会默认进行浅拷贝操作,所以我们可以用它方便地合并两个字典:

python
>>> d1 = {'name': 'apple'}
>>> d2 = {'price': 10}

# d1、d2 原始值不会受影响
>>> {**d1, **d2}
{'name': 'apple', 'price': 10}

除了使用 ** 解包字典,你还可以使用单星号 * 运算符来解包任何可迭代对象:

python
>>> [1, 2, *range(3)]
[1, 2, 0, 1, 2]

>>> l1 = [1, 2]
>>> l2 = [3, 4]
# 合并两个列表
>>> [*l1, *l2]
[1, 2, 3, 4]

合理利用 *** 运算符,可以帮助我们高效构建列表与字典对象。

Python 3.9+ 版本。在这个版本中,字典类型新增了对 | 运算符的支持。只要执行 d1 | d2,就能快速拿到两个字典合并后的结果:

python
>>> d1 = {'name': 'apple'}
>>> d2 = {'name': 'orange', 'price': 10}
>>> d1 | d2
{'name': 'orange', 'price': 10}
>>> d2 | d1  # 运算顺序不同,会影响最终的合并结果
{'name': 'apple', 'price': 10}

让函数返回 NamedTuple

在日常编码中,我们经常需要写一些返回多个值的函数。举个例子,下面这个地理位置相关的函数,用 Python 的标准做法返回了多个结果:

python
def latlon_to_address(lat, lon):
    """返回某个经纬度的地理位置信息"""
    ...
    # 返回多个结果——其实就是一个元组
    return country, province, city

# 所有的调用方都会这样将结果一次解包为多个变量
country, province, city = latlon_to_address(lat, lon)

但有一天,产品需求变了,除了国家、省份和城市,调用方还需要用到一个新的位置信息:“城区”(district)。因此 latlon_to_address() 函数得增加一个新的返回值,返回 4 个结果: countryprovincecitydistrict

修改函数的返回结果后,为了保证兼容,你还需要找到所有调用 latlon_to_address() 的地方,补上多出来的 district 变量,否则代码就会报错:

python
# 旧的调用方式会报错:ValueError: too many values to unpack
# country, province, city = latlon_to_address(lat, lon)

# 增加新的返回值
country, province, city, district = latlon_to_address(lat, lon)
# 或者使用 _ 忽略多出来的返回值
country, province, city, _ = latlon_to_address(lat, lon)

但以上这些为了保证兼容性的批量修改,其实原本可以避免。

对于这种 未来可能会变动 的多返回值函数来说,如果一开始就使用 NamedTuple 类型对返回结果进行建模,上面的改动会变得简单许多:

python
from typing import NamedTuple

class Address(NamedTuple):
    """地址信息结果"""
    country: str
    province: str
    city: str

def latlon_to_address(lat, lon):
    return Address(
        country=country,
        province=province,
        city=city,
    )

addr = latlon_to_address(lat, lon)
# 通过属性名来使用 addr
# addr.country / addr.province / addr.city

假如我们在 Address 里增加了新的返回值 district,已有的函数调用代码也不用进行任何适配性修改,因为函数结果只是多了一个新属性,没有任何破坏性影响。

Released under the MIT License.