4. 函数封装初探

在上一节中,我们已经能够用 Qiskit 手工搭建一段固定的量子电路。但当同一段电路需要在多个实验、多个脚本甚至多个笔记本中反复出现时,逐行复制 QuantumCircuit、h、cx、measure 的写法会带来两个直接问题:一是代码重复,二是修改时容易漏改某一处。函数封装正是解决这两个问题的第一步——把“一段固定的电路逻辑”变成一个可调用、可复用、可验证的 Python 函数。

本节的核心目标很明确:将一段固定的量子电路逻辑改写为 Python 函数,让函数返回一个 QuantumCircuit 对象,并验证函数调用结果与原始电路等价。这里的关键词是“返回对象”和“等价验证”。前者决定了函数不是打印电路、不是画图,而是把电路作为一等公民交还给调用者;后者决定了我们不能只看“看起来一样”,而要用可判定的方式确认两次构造的电路在结构上一致。

一、先明确“原始电路”是什么

为了让讨论具体,我们选定一段有代表性的固定电路:3 个量子比特、3 个经典比特,先对 q0 施加 Hadamard 门制造叠加态,再用 q0 控制 q1 和 q2 各做一次 CNOT,形成三比特 GHZ 态,最后全部测量。原始写法如下:

from qiskit import QuantumCircuit

qc_original = QuantumCircuit(3, 3)
qc_original.h(0)
qc_original.cx(0, 1)
qc_original.cx(0, 2)
qc_original.measure([0, 1, 2], [0, 1, 2])

这段代码就是我们要封装的“固定逻辑”。注意它包含三类信息:寄存器规模(3 量子比特 + 3 经典比特)、门序列(h、cx、cx)、测量映射(量子位 i 测到经典位 i)。封装时必须完整保留这三类信息,否则函数返回的电路就不再与原始电路等价。

二、把逻辑搬进函数体

最直接的封装方式,是让函数在内部构造电路并返回它。函数不需要参数也能工作,因为这段逻辑是“固定”的:

from qiskit import QuantumCircuit

def build_ghz_circuit():
    """构造 3 比特 GHZ 态并测量的固定电路。"""
    qc = QuantumCircuit(3, 3)
    qc.h(0)
    qc.cx(0, 1)
    qc.cx(0, 2)
    qc.measure([0, 1, 2], [0, 1, 2])
    return qc

这里有三个值得强调的细节。第一,函数内部使用局部变量 qc,每次调用都会创建一个全新的 QuantumCircuit 实例,因此多次调用之间不会互相污染——这一点对后续复用至关重要。第二,return qc 返回的是电路对象本身,而不是它的字符串表示或图像,调用方拿到后可以继续追加门、转译、运行或组合。第三,函数名和文档字符串应说明“构造什么”,而不是“执行什么”,因为函数只负责搭建,不负责运行。

如果希望函数更具通用性,可以把比特数作为参数,但本节聚焦“固定逻辑”的封装,保持无参形式反而更能突出“返回对象 + 等价验证”这一主线。实际工程中,是否参数化取决于复用场景,不必在第一次封装时就追求万能。

三、等价性到底验证什么

“调用结果与原始电路等价”不能靠肉眼比对代码,而应落到可检查的电路属性上。对同一段固定逻辑而言,最实用的等价判据有三层:

  • 规模一致:num_qubits 与 num_clbits 相同,保证寄存器没有多建或漏建。
  • 指令序列一致:按顺序比较 data 中每条指令的操作名、作用比特与经典位,保证门序列和测量映射没有错位。
  • 参数一致:若指令带参数(如旋转角),参数值也应相同;本例门均无参数,但仍应把这一层纳入检查习惯。

Qiskit 提供了便捷的比较手段:QuantumCircuit 之间可以直接用 == 判断是否相等,其内部正是基于上述结构信息。也可以手动遍历 qc.data 做逐条比对,便于在不等时定位差异。下面的验证代码同时给出两种方式:

qc_new = build_ghz_circuit()

# 方式一:直接相等判断
print(qc_new == qc_original)          # 期望输出 True

# 方式二:逐条比对,便于定位差异
def describe(circuit):
    return [(inst.operation.name,
             [circuit.find_bit(q).index for q in inst.qubits],
             [circuit.find_bit(c).index for c in inst.clbits])
            for inst in circuit.data]

print(describe(qc_new) == describe(qc_original))

若两次构造完全一致,两个判断都应输出 True。如果出现 False,逐条比对的结果能直接告诉你差异出在哪一条指令:是门名写错、比特顺序颠倒,还是测量映射错位。这种“先判等、再定位”的验证方式,比反复打印电路图更可靠,也更适合放进自动化测试。

等价验证建议至少调用函数两次,分别与原始电路比较。这样既能确认函数返回值正确,也能间接确认函数没有依赖或修改外部可变状态——两次调用应得到两个独立且都等价的电路对象。

四、用一张图看清封装与验证的关系

下面的示意图把本节的对象关系集中呈现:左侧是原始电路的三类结构信息,中间是函数封装后的调用路径,右侧是等价验证的三层判据与分支结果。它强调的不是“步骤顺序”,而是“信息如何被完整搬运并被逐层核对”。

原始电路 qc_original 寄存器规模 3 qubits / 3 clbits 门序列 h(0) → cx(0,1) → cx(0,2) 测量映射 q[i] → c[i] 三类信息必须完整保留 封装 build_ghz_circuit() 局部变量 qc = QuantumCircuit(3,3) 追加 h / cx / cx / measure return qc 每次调用 生成独立新实例 返回对象 等价验证 ① 规模一致 ② 指令序列一致 ③ 参数一致 全部通过 True 存在差异 False False 时逐条比对定位差异

五、把验证写成可重复的检查

一次性在笔记本里打印 True 并不足以支撑长期复用。更稳妥的做法是把等价检查写成一个小函数或测试用例,让它在每次修改封装逻辑后自动运行。下面给出一个可直接使用的检查函数,它同时覆盖“单次等价”和“多次调用相互独立”两个关注点:

def check_equivalence(builder, reference, times=2):
    results = []
    for _ in range(times):
        candidate = builder()
        results.append(candidate == reference)
    return all(results)

print(check_equivalence(build_ghz_circuit, qc_original))
# 期望输出 True

这个检查函数的价值在于:它把“函数返回对象是否正确”变成了一个布尔结论,便于在脚本、CI 或教学检查点中复用。若某次重构把 cx(0, 2) 误写成 cx(1, 2),检查会立刻返回 False,而逐条比对能进一步指出差异出现在第三条指令的比特位置上。

不要用“电路图看起来一样”代替等价判断。绘图会省略部分细节、受布局影响,且无法自动比较;而 == 与 data 逐条比对是可重复、可自动化的判据。等价验证应优先使用后者。

六、封装带来的实际收益与边界

完成这一步后,调用方只需一行 qc = build_ghz_circuit() 就能拿到一段结构确定的电路,随后可以自由地追加门、拼接其他电路、转译到目标后端或提交运行。修改电路逻辑时也只改函数体一处,所有调用点同步生效。这正是“模块封装与复用”的起点。

同时要清楚当前封装的边界:函数返回的是结构等价的电路,而不是“运行结果等价”的证明。结构一致是运行结果一致的必要前提,但若后续还要比较含随机采样的测量统计,则需要另行设计实验层面的验证。本节先把结构等价这一层做扎实,后续的复用、参数化与组合才有可靠基础。

建议读者现在就用自己上一节写过的固定电路做一次改写:抽出函数、返回 QuantumCircuit、用 == 和逐条比对双重确认,并至少调用两次检查独立性。这个动作很小,但它是把“会写电路”推进到“会组织电路代码”的关键一步。