4. 通配符与参数化:告别硬编码,让规则活起来

前几章我们写的 Snakefile,说白了都是「死」的。

你要处理十个样本,就得写十条规则。你想换个输入文件后缀,得把所有规则翻出来改一遍。我第一次用 Snakemake 干这种事时,改了二十多个样本就后悔了——太累了,还容易漏改。

通配符就是来解决这个问题的。它让一条规则能匹配成百上千个不同的输入。今天我们就把它彻底讲透。

4.1 通配符是什么?

通配符,简单理解就是一个占位符。Snakemake 在运行时会根据你实际需要的输出文件,自动把占位符替换成真实的值。

看个最直观的例子。之前我们写处理单个样本的规则是这样的:

rule process_sample_A:
    input:
        "data/sample_A.fastq"
    output:
        "results/sample_A.clean.fastq"
    shell:
        "fastp -i {input} -o {output}"

如果要处理 sample_B、sample_C……就得复制粘贴 n 遍。用通配符改一下:

rule process_sample:
    input:
        "data/{sample}.fastq"
    output:
        "results/{sample}.clean.fastq"
    shell:
        "fastp -i {input} -o {output}"

看到了吗?{sample} 就是通配符。你告诉 Snakemake:我需要 results/sample_B.clean.fastq,它就知道去拿 data/sample_B.fastq 来生成它。

通配符名字你自己定,但建议保持语义清晰。别用 {x}、{a} 这种,万一规则多了,自己都看不懂。

4.2 通配符的匹配规则

通配符不是随便写的,它遵循一些基本规则。我把常用的总结在下面:

写法 含义 匹配示例
{wildcard} 匹配任意非空字符串,不含路径分隔符 / sample_A、batch1
{wildcard, regex} 用正则表达式约束通配符能匹配的内容 {sample, [a-z]+}
{wildcard: qualifier} 指定数据类型(如 int, float) {batch, int}

多通配符的情况也很常见。比如你要同时按批次和样本拆分:

rule merge_batch:
    input:
        "data/{batch}/{sample}.txt"
    output:
        "results/{batch}_merged.txt"
    shell:
        "cat {input} > {output}"

这里 {batch} 和 {sample} 是两个独立通配符。Snakemake 会生成它们的交叉组合。

我曾经在这里踩过一个坑:如果输入路径中有两个通配符,但它们的顺序写反了,Snakemake 也能运行,但生成的文件结构和你想的完全不一样。检查多通配符规则时,一定要想想最终的路径长什么样。

4.3 参数表达式:通配符的「计算器」

{wildcards} 不只是字符串替换,它还能参与计算。这就要用到参数表达式。

参数表达式用 lambda 或函数来动态决定输入文件。举个例子:批次效应检测中,经常需要比较两个样本批次。你可以这样写:

rule compare_batches:
    input:
        batch_a = lambda wildcards: f"results/{wildcards.batch1}_processed.txt",
        batch_b = lambda wildcards: f"results/{wildcards.batch2}_processed.txt"
    output:
        "comparisons/{batch1}_vs_{batch2}.txt"
    shell:
        "diff {input.batch_a} {input.batch_b} > {output}"

lambda wildcards: ... 这个模式很常用。Snakemake 会把当前匹配到的所有通配符打包成一个 wildcards 对象传给你的函数。

我习惯在需要复杂逻辑时用单独的函数,而不是 inline 的 lambda,代码更容易调试:

def get_control_path(wildcards):
    # 实际项目中可能是查数据库或配置文件
    if wildcards.treatment == "drug_A":
        return "results/untreated_ctl.txt"
    else:
        return "results/vehicle_ctl.txt"

rule treat_vs_control:
    input:
        treatment = "results/{treatment}_processed.txt",
        control = get_control_path
    output:
        "comparisons/{treatment}_vs_ctl.txt"
    shell:
        "compare.py {input.treatment} {input.control} -o {output}"

参数表达式里的函数可以访问 wildcards 对象的所有属性。你还可以用 wildcards.keys() 在调试时打印所有通配符的值。运行 Snakemake 时加 -p 参数也能看到每条命令实际替换后的样子。

4.4 expand 函数:批量生文件的好帮手

我们经常需要定义一组输出文件。比如你手头有 10 个样本,需要生成 10 个质控报告。这时候 expand 函数就派上用场了。

它的用法很直接:

SAMPLES = ["sample_A", "sample_B", "sample_C"]

rule all:
    input:
        expand("results/{sample}_qc.html", sample=SAMPLES)

expand 会遍历 SAMPLES 列表,把 {sample} 替换成每个值,生成:

results/sample_A_qc.html
results/sample_B_qc.html
results/sample_C_qc.html

多个通配符时,默认生成笛卡尔积:

BATCHES = ["batch1", "batch2"]
SAMPLES = ["s1", "s2"]

expand("results/{batch}/{sample}.txt", batch=BATCHES, sample=SAMPLES)
# 生成:
# results/batch1/s1.txt, results/batch1/s2.txt
# results/batch2/s1.txt, results/batch2/s2.txt

如果要逐个对应而不是笛卡尔积,加个 zip 参数:

expand("results/{batch}_{sample}.txt", zip, batch=BATCHES, sample=SAMPLES)
# 生成:
# results/batch1_s1.txt
# results/batch2_s2.txt
expand 参数 行为 示例输出
默认(笛卡尔积) 所有组合 batch1_s1, batch1_s2, batch2_s1, batch2_s2
zip 一对一配对 batch1_s1, batch2_s2
zip_longest 一对一,不足的补 None batch1_s1, batch2_s2, None_s3

用 zip 时,我吃过一次亏:两个列表长度不一样,Snakemake 直接报错。所以一定要确保列表等长,或者用 zip_longest 处理。

4.5 用 expand 做批次检测的交叉比较

实战中我们用得最多的场景:批次效应检测需要做两两比较。假设有 4 个批次,你要比较所有批次对:

BATCHES = ["B1", "B2", "B3", "B4"]

rule all:
    input:
        expand("comparisons/{b1}_vs_{b2}.pcadata.txt",
               b1=BATCHES, b2=BATCHES)

rule pca_comparison:
    input:
        "results/{b1}_pca.txt",
        "results/{b2}_pca.txt"
    output:
        "comparisons/{b1}_vs_{b2}.pcadata.txt"
    shell:
        "compare_pca.py {input} -o {output}"

这个 pipeline 会生成 B1 vs B1、B1 vs B2……共 16 个比较文件。有些自比较(B1 vs B1)是多余的,你可以过滤掉:

rule all:
    input:
        expand("comparisons/{b1}_vs_{b2}.pcadata.txt",
               b1=BATCHES, b2=[b for b in BATCHES if b != "B4"])

或者用列表推导式去掉自比较:

PAIRS = [(b1, b2) for b1 in BATCHES for b2 in BATCHES if b1 != b2]

rule all:
    input:
        expand("comparisons/{b1}_vs_{b2}.pcadata.txt", zip, b1=[p[0] for p in PAIRS], b2=[p[1] for p in PAIRS])

嗯,这里我再多一句:实际项目里,我常常把批次列表和参数配置放在一个单独的 YAML 文件中。这样换数据时,只用改配置文件,不用碰 Snakefile。

4.6 本章核心脉络

通配符与参数化核心逻辑 通配符 {wildcard} 规则执行 输出文件 参数表达式 expand 批量生成 多通配符 / 笛卡尔积 zip / zip_longest 配对 实战要点 1. 通配符名字保持语义清晰,避免无意义的缩写 2. 多通配符时注意路径组合顺序,用 -p 参数验证实际命令 3. expand 的笛卡尔积和 zip 行为不同,按需选择

4.7 总结

今天的三个武器——通配符、参数表达式、expand 函数——是 Snakemake 的基石。掌握了它们,你就能从一个规则处理一个文件,进化到一条规则处理成百上千个文件。

  • 通配符解决了「一条规则应对多种输入」的问题
  • 参数表达式让输入路径可以动态计算,不再写死
  • expand 函数帮你批量声明输出,让 pipeline 自动推导所有依赖

我的建议是:每定义一个规则前,先想想哪些部分是变化的,哪些是固定的。把变的部分用通配符表示,固定的部分直接写死。这样你的 pipeline 会非常清晰,也容易维护。