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,逐条比对的结果能直接告诉你差异出在哪一条指令:是门名写错、比特顺序颠倒,还是测量映射错位。这种“先判等、再定位”的验证方式,比反复打印电路图更可靠,也更适合放进自动化测试。
四、用一张图看清封装与验证的关系
下面的示意图把本节的对象关系集中呈现:左侧是原始电路的三类结构信息,中间是函数封装后的调用路径,右侧是等价验证的三层判据与分支结果。它强调的不是“步骤顺序”,而是“信息如何被完整搬运并被逐层核对”。
五、把验证写成可重复的检查
一次性在笔记本里打印 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、用 == 和逐条比对双重确认,并至少调用两次检查独立性。这个动作很小,但它是把“会写电路”推进到“会组织电路代码”的关键一步。