Lesson 07 正式进入第二阶段“生物信息学编程基础”,核心是:
Function├─ 参数├─ return├─ default arguments├─ keyword arguments├─ scope└─ 异常处理
这一课真正要建立的思维是:
Function 是把一段有明确功能的逻辑包装起来,让它可以反复调用。
1. Function 的基本结构:输入 → 处理 → 输出
1.1 def:定义一个 function
最简单的例子:
def sequence_length(seq): return len(seq)
调用:
sequence_length(”ATGCGT”)
一个 Python function 的基本结构是:
def function_name(parameters): code return result
可以把它理解成:
input ↓ parameters ↓┌────────────────────┐│ function ││ ││ process data │└────────────────────┘ ↓ return ↓ output
例如:
def gc_content(seq): gc = seq.count(”G”) + seq.count(”C”) return gc / len(seq)
这里:
gc_content → function nameseq → parameter函数内部两行 → processingreturn ... → output
1.2 和 R function 对照
这个概念对你其实非常熟悉。
R:
gc_content <- function(seq) { gc <- stringr::str_count(seq, ”[GC]”) return(gc / nchar(seq))}
Python:
def gc_content(seq): gc = seq.count(”G”) + seq.count(”C”) return gc / len(seq)
思维几乎完全一样:
R Pythonfunction(...) def ...(...):{ indentation ...}return(x) return x
真正需要适应的是 Python 的:
函数体同样依赖缩进:
def gc_content(seq): gc = seq.count(”G”) + seq.count(”C”) return gc / len(seq)
2. Parameter 与 Argument:函数怎样接收数据
这两个词以后看文档会不断出现,所以现在最好区分。
2.1 Parameter 是定义时的变量,argument 是调用时传进去的值
例如:
这里
是parameter。
调用:
这里:
是 argument。
所以:
定义 function:def gc_content(seq): ↑ parameter调用 function:gc_content(”ATGCGT”) ↑ argument
实际交流中大家有时不会严格区分,但看 Python 文档时知道这个差别很有帮助。
2.2 一个 function 可以有多个参数
多个参数用逗号分开,使函数能够接收多个输入。
例如我们在 Lesson 05/06 一直写:
seq = ”ATGCGT”k = 3kmers = [ seq[i:i + k] for i in range(len(seq) - k + 1)]
现在可以把它包装成:
def generate_kmers(seq, k): return [ seq[i:i + k] for i in range(len(seq) - k + 1) ]
调用:
generate_kmers(”ATGCGT”,3)
3. Positional、Keyword 与 Default Arguments
这是科研代码里很常见的一组参数设计。
3.1 Positional arguments:按照位置传值
函数:
def generate_kmers(seq, k): ...
调用:
generate_kmers(”ATGCGT”, 3)
Python 根据位置知道:
3.2 Keyword arguments:直接写参数名字
例如:
generate_kmers( seq=”ATGCGT”, k=3)
结果和:
generate_kmers(”ATGCGT”, 3)
相同。
但前者读起来更加明确:
对于参数很多的科研函数,这种写法尤其有价值。
例如未来你可能看到:
some_function( min_quality=30, min_length=100, remove_adapter=True)
比:
some_function(30, 100, True)
明显更容易知道这些数字和布尔值分别代表什么。
3.3 Default arguments:给参数一个默认值
课程最初就计划用:
def gc_content(seq, percentage=False): ...
作为例子。
我们现在把它真正实现出来:
def gc_content(seq, percentage=False): gc = seq.count(”G”) + seq.count(”C”) value = gc / len(seq) if percentage: return value * 100 return valuegc_content(”ATGCGT”)
gc_content(”ATGCGT”, percentage=True)
这里函数设计表达的是:
必须告诉我:seq可选告诉我:percentage如果不告诉我:percentage = False
这就是 default argument 最主要的用途。
所以可以先理解成:
def gc_content(seq, percentage=False):
其中:
seq→ requiredpercentage→ optional
因此:
不行,因为缺少 seq。
而:
可以。
4. return:Function 真正把什么交出去?
这是这一课最重要的概念之一。
4.1 return 和 print() 完全不是一回事
先看:
def gc_content(seq): gc = seq.count(”G”) + seq.count(”C”) print(gc / len(seq))gc_content(”ATGCGT”)
看起来似乎没问题。
但:
result = gc_content(”ATGCGT”)print(result)
因为函数根本没有return,它只是打印了一次结果。
正确写法:
def gc_content(seq): gc = seq.count(”G”) + seq.count(”C”) return gc / len(seq)result = gc_content(”ATGCGT”)print(result)
于是你还可以:
if result > 0.6: print(”High GC”)
或者:
或者:
这才是科研程序真正需要的。
4.2 return 会结束当前函数
例如:
def classify_length(seq): if len(seq) >= 100: return ”long” return ”short”
我们不一定要写:
def classify_length(seq): if len(seq) >= 100: return ”long” else: return ”short”
两者都正确。
前者在很多简单场景下会更加干净。
4.3 一个 function 可以返回多个结果
例如想同时得到:
GC count GC content
def gc_stats(seq): gc_count = seq.count(”G”) + seq.count(”C”) gc_content = gc_count / len(seq) return gc_count, gc_contentresult = gc_stats(”ATGCGT”)print(result)
更常见的写法是直接 unpack:
gc_count, gc_content = gc_stats(”ATGCGT”)print(gc_count, gc_content)
这就是为什么前面学习 tuple unpacking 并不是孤立语法。
5. Scope:函数里面的变量属于谁?
这一部分对 R 用户也非常值得注意。
5.1 Local variable:函数内部创建的变量
def gc_content(seq): gc_count = seq.count(”G”) + seq.count(”C”) gc = gc_count / len(seq) return gc
这里:
seq gc_count gc
都是函数内部使用的名字。
调用:
result = gc_content(”ATGCGT”)
之后:
result
存在。
但你不能指望在函数外面直接:
gc_count
因为:
gc_count
只在函数内部定义。
可以先把它理解成:
outsideresult ↑ │ return │┌───────────────┐│ function ││ ││ seq ││ gc_count ││ gc │└───────────────┘
函数内部拥有自己的工作空间。
5.2 为什么我们通常不依赖 global variable?
看看这种写法:
seq = ”ATGCGT”def gc_content(): gc = seq.count(”G”) + seq.count(”C”) return gc / len(seq)
它确实可能运行。
但是这个 function 偷偷依赖外面的seq
所以:
gc_content()
从调用本身完全看不出它在处理什么数据。
如果以后:
seq = "GGGGGG"
函数的结果也跟着改变。
更清楚的设计是:
def gc_content(seq): gc = seq.count(”G”) + seq.count(”C”) return gc / len(seq)gc_content(”ATGCGT”)
这样 function 的依赖非常明确:
input↓parameter↓function↓return
对于科研代码尤其重要,因为它让代码:
更容易理解 更容易复用 更容易测试 更不容易产生隐藏状态
6. Function 与 mutable object:一个非常重要的联系
6.1 Function 可以修改传入的 mutable object
def add_gene(genes): genes.append(”TP53”)genes = [”EGFR”, ”KRAS”]add_gene(genes)print(genes)
为什么?
不是 function 神奇地访问了外面的变量。
真正发生的是:
genes ─────────────┐ ↓ [”EGFR”, ”KRAS”] ↑ │function parameter ┘
函数收到的 parameter 也指向那个 list object。
然后:
修改的是这个 mutable list 本身。
所以函数外面的那个对象也发生了变化。
这正是:
name → object+mutable+reference
在 function 中的实际意义。
6.2 如果不希望修改原 list,就创建新对象
def add_gene(genes, gene): new_genes = genes.copy() new_genes.append(gene) return new_genesgenes = [”EGFR”, ”KRAS”]new_genes = add_gene(genes, ”TP53”)print(genes)print(new_genes)
所以以后设计 function 时要有一个意识:
这个 function 是返回一个新的对象,还是修改传进来的原对象?
这不是语法问题,而是函数设计问题。
Default arguments 很方便,但下面这种写法最好不要养成习惯:
def add_read(read, reads=[]): reads.append(read) return reads
因为这个默认 list 可能在多次调用之间被重复使用,产生非常反直觉的结果。
如果需要这样的逻辑,更安全的写法通常是:
def add_read(read, reads=None): if reads is None: reads = [] reads.append(read) return reads
目前不需要深入背机制,只需要记住:
default argumentint / float / str / bool / None→ 一般很自然[] / {} / set()→ 不要直接拿来当默认值
7. Error handling:让科研函数遇到坏数据时明确失败
新版课程把异常处理放入 Lesson 07;原始课程设计也特别强调,科研脚本中应该主动处理错误,例如空序列应明确抛出 ValueError。
这里先掌握两个层次:
发现输入不合法→ raise调用可能失败的代码→ try / except
7.1 raise:主动告诉调用者“输入有问题”
我们原来的 GC function:
def gc_content(seq): gc = seq.count(”G”) + seq.count(”C”) return gc / len(seq)
最终会发生除以 0。
更好的函数应该主动检查:
def gc_content(seq): if len(seq) == 0: raise ValueError(”Sequence is empty”) gc = seq.count(”G”) + seq.count(”C”) return gc / len(seq)gc_content(””)
现在会明确告诉你:
ValueError: Sequence is empty
这比程序在其他地方产生一个莫名其妙的错误好得多。
def gc_content(seq): seq = seq.upper() if len(seq) == 0: raise ValueError(”Sequence is empty”) valid_bases = {”A”, ”T”, ”C”, ”G”} if not set(seq) <= valid_bases: raise ValueError(”Sequence contains invalid bases”) gc = seq.count(”G”) + seq.count(”C”) return gc / len(seq)gc_content(””)
7.2 try / except:决定出现错误后怎么办
try: gc = gc_content(””)except ValueError: print(”Cannot calculate GC content”)
逻辑是:
try↓尝试运行成功→ 正常继续出现 ValueError→ 进入 except
也可以获得错误信息:
try: gc = gc_content(””)except ValueError as e: print(e)
这是一个很重要的设计思维。
函数内部:
def gc_content(seq): if len(seq) == 0: raise ValueError(”Sequence is empty”)
意思是:
我这个 function 知道什么输入是不合法的。
调用函数的地方:
try: gc = gc_content(seq)except ValueError as e: print(e)
意思是:
我决定遇到这个错误以后程序该怎么办。
所以二者不是竞争关系,而是:
function↓发现错误↓raisecaller↓接收到错误↓try / except↓决定处理方式
8. 把前六课的代码真正封装成 Functions
现在我们不再只是学 function 语法,而是把之前的 sequence algorithms 收回来。
8.1 Reverse complement
我们之前已经会:
seq = ”ATGCGT”complement = { ”A”: ”T”, ”T”: ”A”, ”G”: ”C”, ”C”: ”G”,}revcomp = ””for base in seq[::-1]: revcomp += complement[base]revcomp
现在:
reverse_complement()接收 DNA sequence,返回它的 reverse complement。
def reverse_complement(seq): complement = { ”A”: ”T”, ”T”: ”A”, ”G”: ”C”, ”C”: ”G”, } revcomp = ”” for base in seq[::-1]: revcomp += complement[base] return revcompreverse_complement(”ATTGCC”)
8.2 k-mer generation
Lesson 05:
for i in range(len(seq) - k + 1): kmer = seq[i:i+k]
[ seq[i:i+k] for i in range(len(seq) - k +1)]
def kmer_generate(seq, k): res = [ seq[i:i+k] for i in range(len(seq) - k + 1) ] return reskmer_generate(”ATTCG”,3)
8.3 Motif search
def find_motif(seq, motif): res = [] for i in range(len(seq) - len(motif) + 1): if seq[i:i+len(motif)] == motif: res.append(i+1) return(res)find_motif( ”GATATATGCATATACTT”, ”ATAT”)
def find_motif(seq, motif): res = [ i+1 for i in range(len(seq) - len(motif) + 1) if seq[i:i+len(motif)] == motif ] return(res)find_motif( ”GATATATGCATATACTT”, ”ATAT”)
现在函数本身表达得非常清楚:
function: find_motifinput: seq motifoutput: positions
9. Function composition:让小函数彼此组合
这是 function 真正开始强大的地方。
假设:
def gc_content(seq): if len(seq) == 0: raise ValueError(”Sequence is empty”) gc = seq.count(”G”) + seq.count(”C”) return gc / len(seq)def classify_gc(seq, threshold=0.5): gc = gc_content(seq) if gc >= threshold: return ”high GC” return ”low GC”
注意:
内部调用:
于是我们开始形成:
small function ↓small function ↓larger workflow
这对于后面的:
read FASTA↓clean sequence↓generate k-mers↓count k-mers↓build graph
非常重要。
最终你很可能会有类似:
seqs = read_fasta(path)for seq_id, seq in seqs.items(): kmers = generate_kmers(seq, k) ...
到这里,function 就开始成为组织 genomics workflow 的基本单元。
10. 一个科研函数应该尽量做到什么?
刚开始不用追求复杂的软件工程规范,但可以先养成几个习惯。
一个 function 最好有一个明确任务
例如:
很明确:
算 GC content。
很明确:
产生 k-mers
很明确:
找 motif。
而如果一个函数叫:
里面同时:
读取文件清洗序列计算 GC生成 k-mer画图保存结果
以后很难复用,也很难 debug。
Input 和 output 尽量明确
好的函数调用:
kmers = generate_kmers(seq, k)
相比之下:
然后偷偷从很多 global variables 中获取数据,就很难知道发生了什么。
计算函数通常优先 return,而不是 print
例如:
def sequence_length(seq): return len(seq)
比:
def sequence_length(seq): print(len(seq))
更可复用。
因为返回结果之后:
length = sequence_length(seq)
你想:
printfilterstorecomparewrite to file
都可以。
11. 本课练习
这次练习我建议真正自己写,因为 function 非常需要通过“封装旧代码”建立感觉。
Exercise A|最基础的 function
写:
def count_base(seq, base): ...
例如:
count_base("ATGCGT", "G")
应该返回:2
要求:
· 使用两个 parameters:seq、base · 使用 return · 不要在 function 内部 print() 结果
然后回答:
seq / base 是 parameter 还是 argument?
"ATGCGT" / "G" 又是什么?
def count_base(seq, base): return seq.count(base)count_base(”ATGCGT”, ”G”)
seq和base是parameter,"ATGCGT" / "G"是argument
Exercise B|GC content
写:
def gc_content(seq, percentage=False): ...
要求:gc_content("ATGCGT"),返回:0.5
而:gc_content("ATGCGT", percentage=True),返回:50.0
并且:gc_content(""),应该:raise ValueError(...)
def gc_content(seq, percentage=False): if len(seq) == 0: raise ValueError(”Sequence is empty”) valid_bases = {”A”, ”T”, ”C”, ”G”} if not set(seq) <= valid_bases: raise ValueError(”Sequence contains invalid bases”) gc = seq.count(”G”) + seq.count(”C”) gc_content = gc / len(seq) if percentage: return gc_content * 100 return gc_contentprint(gc_content(”ATGCA”,False))print(gc_content(”ATGCA”,True))
Exercise C|k-mer function
把 Lesson 06 的:
[ seq[i:i + k] for i in range(len(seq) - k + 1) ]
包装成:
def generate_kmers(seq, k): ...
给定:
seq = "CAATCCAAC" k = 5
自己预测:
应该返回多少个 k-mer?
然后运行检查。
进一步思考:
如果 k > len(seq),你希望 function 返回 [],还是主动 raise ValueError?
这里暂时没有唯一答案。
重要的是开始意识到:
函数不仅要决定怎么算,还要决定什么输入属于合法输入。
def generate_kmers(seq, k): if k > len(seq): raise ValueError(”kmer cannot be longer than sequence length”) kmer = [ seq[i:i+k] for i in range(len(seq) - k + 1) ] return kmergenerate_kmers(”CAATCCAAC”,5)
Exercise D|Motif function
写:
def find_motif(seq, motif): ...
给定:
seq = "GATATATGCATATACTT" motif = "ATAT"
要求返回:[2, 4, 10]
注意:return positions
而不是:逐个 print position
def find_motif(seq, motif): positions = [ i + 1 for i in range(len(seq) - len(motif) + 1) if seq[i:i+len(motif)] == motif ] return positionsfind_motif(seq = ”GATATATGCATATACTT”, motif = ”ATAT”)
Exercise E|理解 mutable + function
先不要运行:
def add_read(reads, read): reads.append(read)reads = [”ATGC”, ”GGAA”]add_read(reads, ”CCGT”)
回答最终:reads 是什么?
然后解释:
为什么 function 内部没有 return reads,外面的 reads 仍然发生了变化?
这一题实际上同时考:
function+reference+mutable
reads = ["ATGC", "GGAA","CCGT"]
Function 可以修改传入的 mutable object
Exercise F|return 还是 print?
比较:
def f1(seq): print(len(seq))
和:
def f2(seq): return len(seq)
然后:
x = f1(”ATGC”)y = f2(”ATGC”)
不运行,判断:
屏幕上会显示什么?
x 是什么?
y 是什么?
x=f1("ATGC")显示4,y=f2("ATGC")无任何显示。
x是none,y是4