Part 1:Python语言核心 - 序列与容器

Python 数据类型 - 序列与容器

什么是序列?什么是容器

序列(Sequence)

核心特征

  • 有"顺序"
  • 可以被索引(通常支持 __getitem__
  • 可以被迭代
  • 通常有长度(__len__

典型序列:

类型 有序 可变
str
list
tuple

📌 关键认知

"有序 + 可索引" 才是序列的本质

容器(Container)

核心特征

  • 用来"装对象"
  • 支持成员判断:x in container
  • 不一定有顺序
  • 不一定可索引

典型容器:

类型 有序 可变
list
tuple
set
dict ❌(逻辑)

📌 容器 ≠ 序列

  • setdict 是容器,但不是序列

统一视角:Python 容器装的是什么?

Python 的容器存的不是"值",而是"对象的引用"

python 复制代码
a = 10
lst = [a]

内存模型是:

复制代码
lst ─▶ [ ● ]
          │
          ▼
         10

📌 这会直接影响:

  • 可变 / 不可变
  • 浅拷贝 / 深拷贝
  • 参数传递
  • 多容器共享数据

逐个拆解核心类型(从原理到行为)

str ------ 不可变字符序列

本质
  • Unicode 字符序列

  • 不可变对象

  • 是序列,不是容器(偏语义)

    s = "hello"

不可变的真正含义

不可变指的是一旦改变某个变量的值,那么该变量的内存地址也跟着改变。

复制代码
s[0] = "H"   # ❌ TypeError

但:

复制代码
s = s + "!"

📌 不是修改原字符串

而是:

  1. 创建新字符串对象
  2. 绑定新名字
为什么 str 要不可变?
  • 可 hash(能作为 dict key)
  • 安全(多线程、缓存)
  • 性能(intern、共享)

list ------ 可变、顺序容器

本质
  • 动态数组(array-like)

  • 存放 对象引用

  • 可变

    lst = [1, 2, 3]

可变体现在哪里?
复制代码
lst.append(4)
lst[0] = 100

📌 list 的"可变"是指:

容器结构可以变化,而不是元素本身

list 的一个"误区"
复制代码
a = [1, 2]
b = a
b.append(3)

结果:

复制代码
a == [1, 2, 3]

📌 原因:

  • ab 指向同一个 list 对象

tuple ------ 不可变的序列容器

本质
  • 固定长度

  • 不可变结构

  • 但内部元素可能是可变对象

    t = (1, [2, 3])
    t[1].append(4) # ✅

📌 不可变的是"容器结构",不是"内容递归冻结"

tuple 为什么存在?
  • 可作为 dict key
  • 比 list 轻量
  • 表达"语义上的不可变数据"

set ------ 无序、去重、哈希容器

本质
  • 基于 hash table

  • 元素必须 可 hash

  • 无序(不能索引)

    s = {1, 2, 3}

为什么不能有 list?
复制代码
{[1, 2]}  # ❌ TypeError

📌 因为:

  • list 是可变的
  • 可变对象 hash 不稳定
set 的核心用途
  • 去重
  • 集合运算(并、交、差)
  • 高效成员判断(O(1))

dict ------ 键值映射容器(Python 的核心)

本质
  • hash table

  • key → value 映射

  • key 必须可 hash

    d = {"a": 1, "b": 2}

dict 的核心特性
  • 键是唯一的
  • 插入顺序在 Python 3.7+ 保留(实现保证)

📌 注意:

dict 是"逻辑无序",不是"实现无序"

dict 的执行模型视角
复制代码
d[k] = v

等价逻辑:

  1. hash(k)
  2. 定位桶
  3. 比较 ==
  4. 存储引用

is VS ==

复制代码
a = [1, 2]
b = [1, 2]

a == b   # True
a is b   # False
  • ==:值相等(内容)
  • is:同一对象(id 相同)

📌 容器比较默认是 递归内容比较

id()

复制代码
id(obj)
  • 返回对象在当前解释器中的唯一标识
  • 通常是内存地址(CPython)

📌 用途:

  • 调试引用关系
  • 理解浅 / 深拷贝
  • 理解参数传递

小结

Python 中:

  • 一切皆对象
  • 容器存的是引用
  • 变量是名字
  • 可变性决定行为
  • hash 决定能否做 key / set 元素

进阶--------------------------------------------------------------------

Python 可变 (Mutable) VS 不可变 (Immutable) 对象

可变 vs 不可变,和"变量"没关系,和"对象"有关

  • ❌ 错误理解:变量能不能改
  • ✅ 正确理解:对象创建后,其"内部状态"能否改变

Python 世界的统一前提:一切皆对象。

创建一个变量并赋值,实际上是将一个对象绑定到一个变量名字上。

不可变对象:对象一旦创建,其内部状态不能再被修改。

典型不可变类型:

  • int
  • float
  • bool
  • str
  • tuple
  • frozenset

可变对象:对象创建后,其内部结构或内容可以被修改。

Python 浅拷贝 vs 深拷贝(copy / deepcopy)

核心不是"拷没拷",而是"拷贝到哪一层"。

复制代码
a = [1, 2]

真实内存结构(逻辑图):

复制代码
a ─────▶ [ ● , ● ]
           │   │
           ▼   ▼
           1   2

当容器里再装容器:

复制代码
a = [1, [2, 3]]
a ─────▶ [ ● , ● ]
           │   │
           ▼   ▼
           1  [2, 3]

📌 拷贝的复杂性,从从容器嵌套开始

浅拷贝:只复制"最外层容器",内部对象仍然共享创建一个新的容器对象,但容器内部的元素引用原对象

深拷贝:递归复制所有可变对象,直到不可变为止递归复制对象及其引用的所有可变子对象

最直观的浅拷贝方式

python 复制代码
import copy

a = [1, [2, 3]]
b = copy.copy(a)

内存关系:

复制代码
a ─────▶ [ ● , ● ]
           │   │
           ▼   ▼
           1  [2, 3]

b ─────▶ [ ● , ● ]
           │   │
           ▼   ▼
           1  [2, 3]   ← 共享

📌 外壳不同,内部共享

修改外层 vs 修改内层

复制代码
b.append(4)
  • a 不变

  • b

    b[1].append(99)

  • ab 一起变

📌 这就是浅拷贝的本质后果

常见"隐式浅拷贝"方式

写法 类型
a[:] list 浅拷贝
list(a) 浅拷贝
dict(a) 浅拷贝
set(a) 浅拷贝
a.copy() 浅拷贝

📌 它们都只拷贝一层

深拷贝

基本用法

python 复制代码
import copy

a = [1, [2, 3]]
b = copy.deepcopy(a)

内存关系:

复制代码
a ─────▶ [ ● , ● ]
           │   │
           ▼   ▼
           1  [2, 3]

b ─────▶ [ ● , ● ]
           │   │
           ▼   ▼
           1  [2, 3]   ← 新对象

📌 彻底隔离


修改验证

python 复制代码
b[1].append(99)
  • a 不变
  • b 改变

一个"非常容易误判"的例子

python 复制代码
a = [1, 2, 3]
b = copy.deepcopy(a)

你可能觉得这行代码"很重",但实际上:

📌 对纯不可变元素来说:

  • deepcopycopy
  • 因为 int 不可变,不会真的复制

拷贝 ≠ 等价

python 复制代码
a = [1, [2]]
b = copy.copy(a)
c = copy.deepcopy(a)

a == b == c    # True
a is b         # False
a[1] is b[1]   # True
a[1] is c[1]   # False

📌 判断拷贝效果,永远用 is

dict / set 的拷贝本质一样

dict 示例

复制代码
a = {"x": [1, 2]}
b = a.copy()
a ─▶ { "x": ● }
              │
              ▼
            [1, 2]

b ─▶ { "x": ● }   ← 共享

set 示例

python 复制代码
a = {1, 2, (3, 4)}
b = copy.copy(a)
  • tuple 不可变,安全
  • 若元素是可变对象(不允许)

深拷贝的"代价"和风险

1️⃣ 性能问题

  • 递归复制
  • 容器深度越大,成本越高

📌 不要"无脑 deepcopy"


2️⃣ 循环引用问题(copy 已处理)

复制代码
a = []
a.append(a)

deepcopy 内部使用 memo 表,避免无限递归。


3️⃣ 自定义对象的拷贝行为

复制代码
class A:
    def __init__(self):
        self.lst = []
  • 默认浅拷贝。

📌 可以通过:

  • __copy__
  • __deepcopy__

来自定义行为(高级用法)

python 复制代码
class Person:
    def __init__(self, name, friends=None):
        self.name = name
        self.friends = friends if friends is not None else []

# 创建对象
p1 = Person("Alice")
p1.friends.append("Bob")

# 浅拷贝(默认的各种拷贝方式)
p2 = p1                  # ❌ 引用,不是拷贝
p3 = Person(p1.name, p1.friends)  # ❌ 浅拷贝(列表是共享的)
import copy
p4 = copy.copy(p1)      # ⚠️ 浅拷贝
python 复制代码
p1 = Person("Alice")
p1.friends.append("Bob")

# 浅拷贝创建 p2
p2 = copy.copy(p1)
p2.name = "Alice Clone"  # ✅ 修改基本属性不影响原对象

print(p1.friends)  # ['Bob']
p2.friends.append("Charlie")  # ⚠️ 修改可变属性
print(p1.friends)  # ['Bob', 'Charlie']  ❌ 原对象也被修改了!
自定义类深拷贝实现

copy.deepcopy

python 复制代码
import copy

class Person:
    def __init__(self, name, friends=None):
        self.name = name
        self.friends = friends if friends is not None else []

# 深拷贝
p1 = Person("Alice", ["Bob"])
p2 = copy.deepcopy(p1)  # ✅ 完全独立的拷贝

p2.friends.append("Charlie")
print(p1.friends)  # ['Bob']      ✅ 不受影响
print(p2.friends)  # ['Bob', 'Charlie']

自定义 __deepcopy__ 方法:

python 复制代码
import copy

class Person:
    def __init__(self, name, friends=None, metadata=None):
        self.name = name
        self.friends = friends if friends is not None else []
        self.metadata = metadata if metadata is not None else {}
    
    def __deepcopy__(self, memo):
        """自定义深拷贝行为"""
        # memo 字典用于避免循环引用导致的无限递归
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        
        # 深拷贝所有属性
        result.name = copy.deepcopy(self.name, memo)
        result.friends = copy.deepcopy(self.friends, memo)
        result.metadata = copy.deepcopy(self.metadata, memo)
        
        return result

# 使用
p1 = Person("Alice", ["Bob"], {"id": 1})
p2 = copy.deepcopy(p1)  # 使用自定义的深拷贝

自定义 __copy____deepcopy__

python 复制代码
import copy

class Config:
    def __init__(self, settings=None):
        self.settings = settings if settings else {}
        self._cache = {}  # 不希望被拷贝的缓存
    
    def __copy__(self):
        """自定义浅拷贝"""
        cls = self.__class__
        new_obj = cls.__new__(cls)
        new_obj.settings = self.settings.copy()  # 只拷贝字典
        new_obj._cache = {}  # 新建空缓存
        return new_obj
    
    def __deepcopy__(self, memo):
        """自定义深拷贝"""
        cls = self.__class__
        new_obj = cls.__new__(cls)
        memo[id(self)] = new_obj
        
        # 深拷贝 settings
        new_obj.settings = copy.deepcopy(self.settings, memo)
        # 不拷贝缓存,新建空缓存
        new_obj._cache = {}
        
        return new_obj

# 使用
config1 = Config({"theme": "dark"})
config2 = copy.copy(config1)    # 使用自定义浅拷贝
config3 = copy.deepcopy(config1) # 使用自定义深拷贝

参数传递的本质

The essence of parameter passing.

Python 参数传递的本质:传递"对象引用的拷贝"。不是值传递,不是引用传递,这是 Python 自己的一套模型。

C/C++的世界

  • 传递值:拷贝值。
  • 引用传递:传地址。

Python 中

  • 没有变量地址的语法。
  • 变量本身不是对象。
  • 变量只是名字。

函数调用时:

python 复制代码
def f(x):
    ...

f(a)

真实过程:

  • 计算实参表达式 a 得到对象 10

  • 创建形参名字 x

  • x 指向同一个对象。

    python 复制代码
    a ─┐
       ├──▶ 10
    x ─┘
  • 函数参数 = 新名字 + 同一对象


python 复制代码
def f(x):
    x += 1

a = 10
f(a)
print(a)   # 10

真实发生的事:

python 复制代码
x += 1   # 等价于 x = x + 1
  • 创建新对象 11
  • 重新绑定 x
  • a 不受影响

📌 不是没传进去,而是"改了名字绑定"


复制代码
def f(lst):
    lst.append(3)

a = [1, 2]
f(a)
print(a)   # [1, 2, 3]

真实发生的事:

  • lsta 指向同一个 list
  • .append() 修改对象本身
  • 所有名字都"看到变化"

📌 不是引用传递,是"对象被改了"


python 复制代码
def f(lst):
    lst = [100]

a = [1, 2]
f(a)
print(a)   # ?

答案:

复制代码
[1, 2]

📌 因为:

  • lst = [100]重新绑定
  • 没有修改原 list 对象

1️⃣ 明确文档(推荐)

python 复制代码
def add_item(lst):
    """会原地修改 lst"""
    lst.append(1)

2️⃣ 内部拷贝(防副作用)

python 复制代码
def add_item(lst):
    lst = lst.copy()
    lst.append(1)
    return lst

3️⃣ 使用不可变对象(函数式思维)

python 复制代码
def add_item(t):
    return t + (1,)

为什么 dict / set 性能好速度快?

dict / set 使用的是"哈希表(hash table)",把"查找问题"从 O(n) 直接降到 O(1)

这里的 hash table 指的是数据结构 hash 。

在 Python 中:

复制代码
hash(obj)

👉 返回的是一个整数(int)

如果 a == b,则 hash(a) == hash(b)

hash(a) == hash(b),不一定 a == b

也就是说:

  • 允许 hash 冲突
  • 而且必然存在冲突

在 Python 中,hash 函数会把一个对象映射为一个整数值,该值用于在哈希表中快速计算存储位置;通过 hash 定位后,再用 == 解决冲突,从而实现平均 O(1) 的查找效率。

tex 复制代码
对象
 ↓
整数 hash(可能冲突)
 ↓
映射为桶索引
 ↓
hash 相同再用 ==
相关推荐
m0_662577972 小时前
Python迭代器(Iterator)揭秘:for循环背后的故事
jvm·数据库·python
Elnaij2 小时前
从C++开始的编程生活(20)——AVL树
开发语言·c++
似水明俊德2 小时前
12-C#
开发语言·数据库·oracle·c#
hanbr2 小时前
【C++ STL核心】vector:最常用的动态数组容器(第九天核心)
开发语言·c++
菜鸟‍3 小时前
【后端项目】苍穹外卖day01-开发环境搭建
java·开发语言·spring boot
青槿吖3 小时前
【保姆级教程】Spring事务控制通关指南:XML+注解双版本,避坑指南全奉上
xml·java·开发语言·数据库·sql·spring·mybatis
Yungoal3 小时前
B/S和C/S架构在服务端接收请求
c语言·开发语言·架构
浪潮IT馆3 小时前
Windows 达梦 8(DM8)数据库完整安装教程 + 命令行导入 .dmp 文件完整指南
数据库·windows