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 本章核心脉络
4.7 总结
今天的三个武器——通配符、参数表达式、expand 函数——是 Snakemake 的基石。掌握了它们,你就能从一个规则处理一个文件,进化到一条规则处理成百上千个文件。
- 通配符解决了「一条规则应对多种输入」的问题
- 参数表达式让输入路径可以动态计算,不再写死
- expand 函数帮你批量声明输出,让 pipeline 自动推导所有依赖
我的建议是:每定义一个规则前,先想想哪些部分是变化的,哪些是固定的。把变的部分用通配符表示,固定的部分直接写死。这样你的 pipeline 会非常清晰,也容易维护。