Appearance
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) # 101.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.31.3 复数(complex)
复数由实部和虚部组成,虚部用 j 或 J 表示。
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\test2.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")) # True2.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.143. 布尔类型(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) # True4. 列表(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)) # 54.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)需要注意的是,上面的写法虽然给 width 和 height 加了类型注解,但 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)) # 36.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) # False6.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 = 06.5.3 使用 setdefault 取值并修改
有时,我们需要修改字典中某个可能不存在的键,比如在下面的代码里,我需要往字典 d 的 items 键里追加新值,但 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` 则被视作不相等,因为“键的顺序”也会作为对比条件
False7. 集合(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) 所有的不可变内置类型,都是可哈希的,比如 str、 int、 tuple、 frozenset 等;
(2) 所有的可变内置类型,都是不可哈希的,比如 dict、 list 等;
(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")) # True10. 总结对比
| 类型 | 关键字 | 有序 | 可变 | 可重复 | 示例 |
|---|---|---|---|---|---|
| 整数 | 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} |
| 冻结集合 | frozenset | ❌ | ❌ | ❌ | frozenset({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}假如我想合并 d1 和 d2 的值,拿到 {'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}使用这种方式, d1 和 d2 仍然是原来的值,不会因为合并操作被修改。
虽然上面的方案可以完成合并,但显得有些烦琐。使用动态解包表达式可以更简单地完成操作。
要实现合并功能,需要用到双星号 ** 运算符来做解包操作。在字典中使用 **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 个结果: country、 province、 city、 district。
修改函数的返回结果后,为了保证兼容,你还需要找到所有调用 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,已有的函数调用代码也不用进行任何适配性修改,因为函数结果只是多了一个新属性,没有任何破坏性影响。
