生信面试 Python 编程题精选¶
一句话概述:生信面试中 Python 编程题集中在序列处理、文件解析、数据分析和算法实现四个方向,掌握字符串操作、文件I/O、pandas 和 Biopython 就能应对大部分面试。
核心知识点速查表¶
| 概念 | 白话解释 |
|---|---|
| 字符串操作 | DNA序列本质就是字符串,各种切片、翻转、替换 |
| 字典(dict) | 键值对存储,如 |
| 列表推导式 | 一行代码生成列表的简洁写法 |
| 文件I/O | 读写FASTA/FASTQ/VCF等生信文件 |
| pandas | 数据分析核心库(类似Excel但更强大) |
| Biopython | 生物信息专用Python库 |
| 正则表达式 | 用模式匹配文本(re模块) |
| defaultdict | 带默认值的字典,统计计数超好用 |
一、DNA 序列操作(最高频)¶
题1:反向互补序列¶
# ========== 反向互补序列(面试必考) ==========
def reverse_complement(seq):
"""
返回DNA序列的反向互补链
A↔T, C↔G
"""
complement = { # 互补碱基字典
'A': 'T', 'T': 'A',
'C': 'G', 'G': 'C',
'N': 'N' # N保持不变
}
# 反向([::-1]) + 互补(complement.get)
return ''.join(
complement.get(base, 'N') # 获取每个碱基的互补
for base in reversed(seq.upper()) # 反向遍历,统一大写
)
# 更简洁的写法(面试加分)
def reverse_complement_v2(seq):
"""使用 str.maketrans 的高效写法"""
table = str.maketrans('ATCGatcg', 'TAGCtagc') # 创建转换表
return seq.translate(table)[::-1] # 互补后反转
# 测试
print(reverse_complement("ATCGATCG")) # CGATCGAT
print(reverse_complement("AATTCCGG")) # CCGGAATT
题2:GC含量计算¶
# ========== GC含量计算 ==========
def gc_content(seq):
"""计算DNA序列的GC含量(百分比)"""
seq = seq.upper() # 统一大写
gc = seq.count('G') + seq.count('C') # 统计G和C数量
total = len(seq) # 序列总长度
if total == 0: # 防止除以0
return 0.0
return round(gc / total * 100, 2) # 百分比,保留2位小数
# 滑动窗口GC含量(进阶版)
def sliding_gc(seq, window=100, step=10):
"""用滑动窗口计算GC含量变化"""
results = [] # 存储结果
for i in range(0, len(seq) - window + 1, step): # 滑动窗口
sub = seq[i:i+window] # 截取窗口序列
gc = gc_content(sub) # 计算GC含量
results.append((i, gc)) # 保存位置和GC值
return results
print(gc_content("ATCGATCG")) # 50.0
print(gc_content("AAATTT")) # 0.0
print(gc_content("GGGCCC")) # 100.0
题3:DNA转RNA转蛋白质¶
# ========== 中心法则:DNA→RNA→蛋白质 ==========
def transcribe(dna):
"""DNA转录为mRNA(T→U)"""
return dna.upper().replace('T', 'U') # 把T替换为U
def translate(rna):
"""mRNA翻译为蛋白质序列"""
codon_table = { # 密码子表
'UUU': 'F', 'UUC': 'F', 'UUA': 'L', 'UUG': 'L',
'CUU': 'L', 'CUC': 'L', 'CUA': 'L', 'CUG': 'L',
'AUU': 'I', 'AUC': 'I', 'AUA': 'I', 'AUG': 'M',
'GUU': 'V', 'GUC': 'V', 'GUA': 'V', 'GUG': 'V',
'UCU': 'S', 'UCC': 'S', 'UCA': 'S', 'UCG': 'S',
'CCU': 'P', 'CCC': 'P', 'CCA': 'P', 'CCG': 'P',
'ACU': 'T', 'ACC': 'T', 'ACA': 'T', 'ACG': 'T',
'GCU': 'A', 'GCC': 'A', 'GCA': 'A', 'GCG': 'A',
'UAU': 'Y', 'UAC': 'Y', 'UAA': '*', 'UAG': '*',
'CAU': 'H', 'CAC': 'H', 'CAA': 'Q', 'CAG': 'Q',
'AAU': 'N', 'AAC': 'N', 'AAA': 'K', 'AAG': 'K',
'GAU': 'D', 'GAC': 'D', 'GAA': 'E', 'GAG': 'E',
'UGU': 'C', 'UGC': 'C', 'UGA': '*', 'UGG': 'W',
'CGU': 'R', 'CGC': 'R', 'CGA': 'R', 'CGG': 'R',
'AGU': 'S', 'AGC': 'S', 'AGA': 'R', 'AGG': 'R',
'GGU': 'G', 'GGC': 'G', 'GGA': 'G', 'GGG': 'G',
}
protein = [] # 存储氨基酸
for i in range(0, len(rna) - 2, 3): # 每3个碱基一组
codon = rna[i:i+3] # 截取密码子
aa = codon_table.get(codon, 'X') # 查密码子表,未知用X
if aa == '*': # 遇到终止密码子
break
protein.append(aa) # 添加氨基酸
return ''.join(protein) # 拼接成蛋白质序列
# 测试
dna = "ATGGCTAAATGA" # 编码 M A K (stop)
rna = transcribe(dna) # AUGGCUAAAU GA
print(f"RNA: {rna}")
print(f"Protein: {translate(rna)}") # MAK
二、文件解析(必考)¶
题4:解析FASTA文件¶
# ========== 解析FASTA文件(不用Biopython) ==========
def parse_fasta(filepath):
"""
解析FASTA文件,返回 {序列名: 序列} 字典
FASTA格式:>header\nSEQUENCE...
"""
sequences = {} # 存储结果
current_name = None # 当前序列名
current_seq = [] # 当前序列(列表拼接比字符串快)
with open(filepath, 'r') as f: # 打开文件
for line in f: # 逐行读取
line = line.strip() # 去除首尾空白
if not line: # 跳过空行
continue
if line.startswith('>'): # header行
if current_name: # 保存上一条序列
sequences[current_name] = ''.join(current_seq)
current_name = line[1:].split()[0] # 取>后第一个词作为名字
current_seq = [] # 重置序列
else:
current_seq.append(line) # 累加序列行
if current_name: # 保存最后一条序列
sequences[current_name] = ''.join(current_seq)
return sequences
# 使用
seqs = parse_fasta("sequences.fasta")
for name, seq in seqs.items():
print(f"{name}: length={len(seq)}, GC={gc_content(seq)}%")
题5:解析FASTQ并统计质量¶
# ========== 解析FASTQ文件并统计碱基质量 ==========
def parse_fastq(filepath):
"""
解析FASTQ文件,返回 reads 列表
FASTQ格式:@header / 序列 / + / 质量
"""
reads = [] # 存储所有reads
with open(filepath, 'r') as f:
while True:
header = f.readline().strip() # 第1行:@header
if not header: # 文件末尾
break
seq = f.readline().strip() # 第2行:序列
plus = f.readline().strip() # 第3行:+
qual = f.readline().strip() # 第4行:质量字符串
reads.append({
'name': header[1:], # 去掉@
'seq': seq,
'qual': qual,
})
return reads
def avg_quality(qual_string):
"""
计算平均碱基质量分数
质量字符的ASCII码 - 33 = Phred质量值(Illumina 1.8+)
"""
scores = [ord(c) - 33 for c in qual_string] # ASCII转Phred分数
return sum(scores) / len(scores) # 平均值
# 使用
reads = parse_fastq("sample.fastq")
print(f"Total reads: {len(reads)}")
low_quality = sum(1 for r in reads if avg_quality(r['qual']) < 20)
print(f"Low quality reads (Q<20): {low_quality}")
三、pandas 数据分析(高频)¶
题6:处理差异表达结果¶
# ========== pandas处理DESeq2结果 ==========
import pandas as pd
# 读取差异表达结果
df = pd.read_csv("deseq2_results.csv") # 读取CSV
print(df.shape) # 查看维度(行数, 列数)
print(df.head()) # 查看前5行
print(df.describe()) # 统计摘要
# 筛选显著差异基因
# 条件:|log2FoldChange| > 1 且 padj < 0.05
sig = df[
(abs(df['log2FoldChange']) > 1) & # 绝对值>1
(df['padj'] < 0.05) # 调整后p值<0.05
].copy() # .copy() 避免SettingWithCopyWarning
print(f"显著差异基因数: {len(sig)}")
# 分类:上调 vs 下调
up = sig[sig['log2FoldChange'] > 0] # 上调基因
down = sig[sig['log2FoldChange'] < 0] # 下调基因
print(f"上调: {len(up)}, 下调: {len(down)}")
# 排序:按padj排序,找最显著的基因
top_genes = sig.nsmallest(20, 'padj') # padj最小的20个
print(top_genes[['gene', 'log2FoldChange', 'padj']])
# 保存结果
up.to_csv("upregulated_genes.csv", index=False) # 保存上调基因
down.to_csv("downregulated_genes.csv", index=False) # 保存下调基因
题7:合并多个数据表¶
# ========== 合并多个样本的表达量数据 ==========
import pandas as pd
import glob
# 读取多个样本的计数文件并合并
files = glob.glob("counts/*.txt") # 获取所有计数文件
print(f"找到 {len(files)} 个样本文件")
merged = None # 初始化合并结果
for f in sorted(files):
sample = f.split('/')[-1].replace('.txt', '') # 从文件名提取样本名
data = pd.read_csv(f, sep='\t',
names=['gene', sample]) # 两列:基因名, 计数
if merged is None:
merged = data # 第一个文件
else:
merged = pd.merge(merged, data,
on='gene', how='outer') # 按基因名合并
merged = merged.fillna(0) # 缺失值填0
merged = merged.set_index('gene') # 基因名设为索引
print(f"合并结果: {merged.shape[0]} genes × {merged.shape[1]} samples")
# 过滤低表达基因(至少在一个样本中有10个以上reads)
expressed = merged[merged.max(axis=1) >= 10] # 行最大值>=10
print(f"过滤后: {len(expressed)} genes")
四、算法题(偶尔考)¶
题8:找最长公共子序列(LCS)¶
# ========== 两条序列的最长公共子序列 ==========
def longest_common_subsequence(seq1, seq2):
"""
动态规划求最长公共子序列
生信应用:序列比对的基础
"""
m, n = len(seq1), len(seq2) # 两条序列长度
dp = [[0] * (n + 1) for _ in range(m + 1)] # DP矩阵(m+1 × n+1)
# 填充DP矩阵
for i in range(1, m + 1): # 遍历seq1
for j in range(1, n + 1): # 遍历seq2
if seq1[i-1] == seq2[j-1]: # 字符匹配
dp[i][j] = dp[i-1][j-1] + 1 # 左上角+1
else:
dp[i][j] = max(dp[i-1][j], # 取上方和左方的最大值
dp[i][j-1])
# 回溯找到LCS序列
lcs = []
i, j = m, n
while i > 0 and j > 0:
if seq1[i-1] == seq2[j-1]: # 匹配
lcs.append(seq1[i-1])
i -= 1
j -= 1
elif dp[i-1][j] > dp[i][j-1]: # 上方更大
i -= 1
else: # 左方更大
j -= 1
return ''.join(reversed(lcs)) # 反转得到LCS
print(longest_common_subsequence("ATCGTAC", "ATGTAC")) # ATGTAC
题9:K-mer频率统计¶
# ========== K-mer频率统计 ==========
from collections import Counter
def kmer_count(seq, k=3):
"""
统计序列中所有k-mer的出现频率
K-mer:长度为k的连续子序列
应用:基因组组装、物种分类
"""
seq = seq.upper() # 统一大写
kmers = [] # 存储所有k-mer
for i in range(len(seq) - k + 1): # 滑动窗口
kmer = seq[i:i+k] # 截取k-mer
kmers.append(kmer)
counts = Counter(kmers) # 统计频率
return dict(counts.most_common()) # 按频率排序
# 测试
seq = "ATCGATCGATCG"
result = kmer_count(seq, k=3)
for kmer, count in sorted(result.items(), key=lambda x: -x[1]):
print(f"{kmer}: {count}") # 打印每个k-mer的频率
五、实用脚本题¶
题10:批量重命名文件¶
# ========== 批量重命名测序文件 ==========
import os
import re
def rename_fastq_files(directory, pattern, replacement):
"""
批量重命名FASTQ文件
如:SRR12345_1.fastq.gz → Sample1_R1.fastq.gz
"""
renamed = 0 # 计数
for filename in os.listdir(directory): # 遍历目录
if not filename.endswith(('.fastq.gz', '.fq.gz')):
continue # 跳过非fastq文件
new_name = re.sub(pattern, replacement, filename) # 正则替换
if new_name != filename: # 名字有变化
old_path = os.path.join(directory, filename)
new_path = os.path.join(directory, new_name)
os.rename(old_path, new_path) # 重命名
print(f" {filename} → {new_name}")
renamed += 1
print(f"共重命名 {renamed} 个文件")
# 示例:SRR编号改为样本名
# rename_fastq_files("./data", r"SRR\d+", "SampleX")
题11:VCF文件变异统计¶
# ========== VCF文件变异类型统计 ==========
from collections import defaultdict
def vcf_stats(vcf_file):
"""统计VCF文件中的变异类型"""
stats = defaultdict(int) # 默认值为0的字典
chrom_count = defaultdict(int) # 每条染色体变异数
with open(vcf_file, 'r') as f:
for line in f:
if line.startswith('#'): # 跳过注释行
continue
fields = line.strip().split('\t') # tab分割
chrom = fields[0] # 染色体
ref = fields[3] # 参考等位基因
alt = fields[4] # 变异等位基因
chrom_count[chrom] += 1 # 统计染色体
# 判断变异类型
if len(ref) == 1 and len(alt) == 1: # SNP
stats['SNP'] += 1
stats[f'{ref}>{alt}'] += 1 # 具体的碱基替换类型
elif len(ref) > len(alt): # 缺失
stats['DEL'] += 1
elif len(ref) < len(alt): # 插入
stats['INS'] += 1
else: # 复杂变异
stats['COMPLEX'] += 1
print("=== 变异类型统计 ===")
for vtype, count in sorted(stats.items(),
key=lambda x: -x[1]):
print(f" {vtype}: {count}")
print("\n=== 染色体分布 ===")
for chrom, count in sorted(chrom_count.items()):
print(f" {chrom}: {count}")
return stats
# vcf_stats("variants.vcf")
六、常见报错与解决¶
| 问题 | 原因 | 解决方法 |
|---|---|---|
| UnicodeDecodeError | 文件编码不是UTF-8 | open(f, encoding='latin-1') |
| MemoryError | 文件太大一次读入内存 | 逐行读取或用pandas的chunksize |
| KeyError | 字典中没有这个键 | 用 .get(key, default) |
| IndexError | 列表越界 | 检查长度,加 if len(list) > n |
| SettingWithCopyWarning | pandas切片赋值 | 用 .copy() 或 .loc[] |
| FileNotFoundError | 路径错误 | 用 os.path.exists() 先检查 |
七、面试高频问题¶
Q1:Python 列表和字典的时间复杂度?
列表:索引O(1),搜索O(n),末尾添加O(1),中间插入O(n)。 字典:查找O(1),插入O(1),删除O(1)(基于哈希表)。 所以需要频繁查找时用字典,需要有序遍历时用列表。
Q2:如何处理大文件(比如50GB的FASTQ)?
(1) 逐行读取而非一次性读入(
for line in open(f));(2) 用生成器(yield)代替列表;(3) 用pandas的chunksize分块读取;(4) 用多进程并行处理。
Q3:Biopython 的 SeqIO 怎么用?
from Bio import SeqIOfor record in SeqIO.parse("file.fasta", "fasta"):print(record.id, len(record.seq))支持fasta/fastq/genbank等多种格式,parse返回迭代器不占内存。
Q4:Python 2 和 Python 3 的主要区别?
(1) print变成函数
print();(2) 整数除法3/2=1.5(Python2是1);(3) 字符串默认Unicode;(4) range返回迭代器不是列表。现在只用Python 3。
八、速查表¶
# === 生信 Python 面试速查 ===
# DNA操作
rev_comp = seq.translate(str.maketrans('ATCG','TAGC'))[::-1] # 反向互补
gc = (seq.count('G')+seq.count('C'))/len(seq)*100 # GC含量
# 文件解析
with open(f) as fh: # 安全打开文件
for line in fh: # 逐行读取(省内存)
line = line.strip() # 去除换行符
# pandas常用
df = pd.read_csv("f.csv") # 读CSV
df = pd.read_csv("f.tsv", sep="\t") # 读TSV
df[df['col'] > 0] # 条件筛选
df.groupby('col').mean() # 分组统计
df.merge(df2, on='key') # 合并表格
df.to_csv("out.csv", index=False) # 保存
# collections 好用工具
from collections import Counter, defaultdict
Counter("ATCGATCG") # 字符频率统计
d = defaultdict(list) # 默认值为空列表的字典
# 正则表达式
import re
re.findall(r'ATG(?:[ATCG]{3})*?(?:TAA|TAG|TGA)', seq) # 找ORF
参考资料:Biopython Tutorial 2024、Python for Biologists (Martin Jones)、Rosalind 生信编程练习平台