---
title: "德州扑克服务端里的赋值、浅拷贝和深拷贝"
description: "用一桌德州扑克说明赋值、浅拷贝、深拷贝各在什么时候用：下注要改真实玩家，用赋值；洗牌和行动顺序只动容器，用浅拷贝；牌局推演要在副本上改到底，用深拷贝。"
pubDate: 2025-03-28
tags: ["Python"]
---

下面用一桌德州扑克的状态做例子，说明赋值、浅拷贝和深拷贝各在什么时候用。先定义用到的对象：

```python
import copy
import random
from dataclasses import dataclass, field

FULL_DECK = tuple(rank + suit for rank in "23456789TJQKA" for suit in "shdc")


@dataclass
class Player:
    name: str
    stack: int
    hole_cards: list = field(default_factory=list)
    bet: int = 0
    folded: bool = False


@dataclass
class Table:
    players: list
    deck: list = field(default_factory=list)
    board: list = field(default_factory=list)
    pot: int = 0
```

牌用 `"As"` 这样的两字符字符串表示，字符串不可变；`Player` 和 `Table` 里都是可变的状态。

## 核心区别

| 操作 | 代码 | 效果 | 什么时候用 |
|---|---|---|---|
| 赋值 | `b = a` | 两个名字指向同一个对象 | 要操作的就是原来那个对象 |
| 浅拷贝 | `copy.copy(a)` / `a.copy()` / `a[:]` / `list(a)` | 新容器，元素还是原来那些 | 只改容器本身，元素不可变或本来就该共享 |
| 深拷贝 | `copy.deepcopy(a)` | 递归复制整个对象图 | 要在副本上改到底，原来那份一点都不能动 |

## 赋值：要改的就是原来那个对象

轮到某位玩家下注，扣的必须是他本人的筹码：

```python
def place_bet(table, seat, amount):
    player = table.players[seat]  # 赋值：player 就是桌上的这位玩家
    player.stack -= amount
    player.bet += amount
    table.pot += amount
```

这里要的就是同一个对象。如果写成 `player = copy.copy(table.players[seat])`，扣的是副本的筹码，桌上的玩家一个筹码没少，底池却多了 `amount`。

反过来，赋值不能用来留底：`before = table` 之后，`table` 怎么变，`before` 就怎么变。

## 浅拷贝：换一个容器，里面的东西照旧

**洗牌。** 整副牌的母本 `FULL_DECK` 所有牌桌共用，每桌开局要一个自己的列表去洗、去发：

```python
def new_deck():
    deck = list(FULL_DECK)  # 浅拷贝：新列表，里面还是那 52 个字符串
    random.shuffle(deck)
    return deck
```

`FULL_DECK` 是 tuple，不能原地洗，`list()` 复制出一个新列表。列表里的 52 个字符串还是母本里那些对象，但字符串不可变，共享没有风险，所以浅拷贝就够。

**行动顺序。** 一轮下注要按顺序问每位还没弃牌的玩家。弃牌的人要从这一轮的顺序里拿掉，但他的座位还在：

```python
def fold(order, player):
    player.folded = True  # 改的是桌上的玩家
    order.remove(player)  # 只从这一轮的行动顺序里拿掉


table = Table(players=[Player("Alice", 1000), Player("Bob", 1000), Player("Carol", 1000)])
order = table.players[:]  # 浅拷贝：新列表，玩家还是桌上那几位
fold(order, order[1])  # Bob 弃牌
```

`order` 是新列表，`remove` 只影响它，座位表 `table.players` 还是三个人。列表里的玩家却还是桌上那几位，所以弃牌标记直接记在真实的 Bob 身上，这正是想要的。

这个场景正好能对比三种写法：

- 写成 `order = table.players`，`remove` 会把 Bob 从座位表上删掉；
- 写成 `order = copy.deepcopy(table.players)`，弃牌标记记在副本上，桌上的 Bob 还在牌局里；
- 只有浅拷贝两边都对。

## 深拷贝：在副本上改到底，原来那份不能动

**牌局推演。** 翻牌之后，AI 想推演「跟注 100，再把转牌和河牌发完」会是什么局面。推演要扣筹码、改底池、从牌堆发牌，真实牌桌一点都不能动：

```python
def simulate(table, seat, amount):
    sim = copy.deepcopy(table)  # 玩家、牌堆、公共牌全部复制一份
    place_bet(sim, seat, amount)
    while len(sim.board) < 5:
        sim.board.append(sim.deck.pop())
    return sim
```

推演结束，真实牌桌的筹码、牌堆、公共牌都没变。换成 `copy.copy(table)`，`sim` 虽然是新的 `Table`，`sim.players`、`sim.deck`、`sim.board` 却还是原来那几个列表：玩家的筹码被扣了，牌堆少了两张，公共牌变成了五张。只有底池没变，因为 `pot` 是整数，`+=` 只是让副本的属性指向一个新的整数。

**每条街的快照。** 记录牌局历史、做回放时，每条街结束存一份 `copy.deepcopy(table)`。直接存 `table`，存下的是一个还会继续变的引用。

**代价。** 深拷贝要递归复制整张对象图。推演一次无所谓，要跑成千上万次时，可以只复制推演中会改的部分：

```python
def clone(table):
    return Table(
        players=[copy.copy(p) for p in table.players],
        deck=list(table.deck),
        board=list(table.board),
        pot=table.pot,
    )
```

玩家逐个浅拷贝，因为推演只改筹码和下注额，都是整数；牌堆和公共牌要增删，各复制一个新列表；牌本身是字符串，不用复制。在这张三人桌上，它比 `copy.deepcopy` 快好几倍。前提是推演代码守规矩：副本和原件共用同一个手牌列表，一旦有人往副本的 `hole_cards` 里 `append`，改的就是真实手牌。
