Python面试编程题精选¶
一句话说明: 生信面试的编程环节,考的不是LeetCode Hard,而是你能不能用Python处理文件、解析序列、操作数据——本篇精选基础题+生信专项题+数据处理题+进阶题,每道都给解题思路和完整代码。
目录¶
1. 生信面试编程题类型概览¶
| 类型 | 考察点 | 频率 | 难度 |
|---|---|---|---|
| 字符串处理 | 基本功,DNA序列操作的基础 | 高 | 低 |
| 文件IO | 实际工作天天用 | 高 | 低 |
| 数据处理 | Pandas/CSV/JSON操作 | 高 | 中 |
| 生信专项 | GC含量、互补序列、FASTA解析等 | 非常高 | 中 |
| 算法题 | 排序、查找、动态规划等 | 中 | 中-高 |
| 进阶概念 | 装饰器、生成器、上下文管理器 | 低 | 中 |
重点: 生信面试和互联网面试不同,很少考纯算法Hard题。重点是文件处理+生信序列操作+数据分析。
2. 基础题¶
2.1 反转字符串¶
题目: 不使用内置reverse函数,反转一个字符串。
解题思路: 用切片操作最简洁;手动实现的话用双指针或循环。
# ========== 方法1:切片(最Pythonic,面试推荐写这个) ==========
def reverse_string(s: str) -> str:
"""反转字符串,利用Python切片特性"""
return s[::-1] # [::-1] 表示从后往前取,步长为-1
# ========== 方法2:手动实现(展示基本功) ==========
def reverse_string_manual(s: str) -> str:
"""用循环手动反转字符串"""
chars = list(s) # 字符串不可变,先转成列表
left, right = 0, len(chars) - 1 # 双指针,一头一尾
while left < right: # 两个指针还没碰面
chars[left], chars[right] = chars[right], chars[left] # 交换
left += 1 # 左指针右移
right -= 1 # 右指针左移
return ''.join(chars) # 列表拼回字符串
# 测试
print(reverse_string("ATCGATCG")) # 输出:GCTAGCTA
复杂度: 时间O(n),空间O(n)
2.2 列表去重(保持顺序)¶
题目: 给一个列表去重,并保持原来的顺序。
解题思路: 用set记录已出现的元素,遍历时跳过重复项。
# ========== 方法1:手动去重(保持顺序) ==========
def remove_duplicates(lst: list) -> list:
"""去重并保持原始顺序"""
seen = set() # 集合,记录已经出现过的元素
result = [] # 存放去重后的结果
for item in lst: # 遍历原列表
if item not in seen: # 如果没出现过
seen.add(item) # 标记为已出现
result.append(item) # 加入结果
return result
# ========== 方法2:dict.fromkeys(利用字典有序性,Python 3.7+) ==========
def remove_duplicates_v2(lst: list) -> list:
"""利用字典key不重复且有序的特性"""
return list(dict.fromkeys(lst)) # 字典的key自动去重,且保持插入顺序
# 测试
genes = ["TP53", "BRCA1", "TP53", "EGFR", "BRCA1", "MYC"]
print(remove_duplicates(genes)) # 输出:['TP53', 'BRCA1', 'EGFR', 'MYC']
复杂度: 时间O(n),空间O(n)
2.3 字典操作——统计词频¶
题目: 统计一个字符串中每个字符出现的次数。
解题思路: 遍历字符串,用字典计数。也可以用collections.Counter。
from collections import Counter # Python标准库的计数器
# ========== 方法1:手动计数 ==========
def count_chars(s: str) -> dict:
"""统计每个字符出现次数"""
freq = {} # 空字典
for char in s: # 遍历每个字符
freq[char] = freq.get(char, 0) + 1 # get(key, 默认值):不存在就返回0
return freq
# ========== 方法2:Counter(面试中说"我知道有Counter"即可) ==========
def count_chars_v2(s: str) -> dict:
"""用Counter一行搞定"""
return dict(Counter(s)) # Counter返回的是Counter对象,转成普通字典
# 测试
seq = "ATCGATCGATCG"
print(count_chars(seq)) # 输出:{'A': 3, 'T': 3, 'C': 3, 'G': 3}
复杂度: 时间O(n),空间O(k),k为不同字符数
2.4 文件读写¶
题目: 读取一个文本文件,统计行数、单词数、字符数。
解题思路: 用with语句打开文件,逐行读取统计。
def file_stats(filepath: str) -> dict:
"""统计文件的行数、单词数、字符数(类似wc命令)"""
lines = 0 # 行计数器
words = 0 # 单词计数器
chars = 0 # 字符计数器
with open(filepath, 'r') as f: # with确保文件正确关闭
for line in f: # 逐行读取(内存友好,大文件也能处理)
lines += 1 # 每读一行,行数+1
words += len(line.split()) # split()按空白分割,统计单词数
chars += len(line) # 统计字符数(含换行符)
return {
"lines": lines,
"words": words,
"chars": chars
}
# 测试
# stats = file_stats("sample.txt")
# print(f"行数: {stats['lines']}, 单词数: {stats['words']}, 字符数: {stats['chars']}")
复杂度: 时间O(n)(n为文件大小),空间O(1)(逐行读取不一次加载全文件)
3. 生信专项题¶
3.1 计算GC含量¶
题目: 给定一条DNA序列,计算GC含量(G和C碱基占总碱基的百分比)。
解题思路: 统计G和C的个数,除以序列总长度。注意处理大小写和非法字符。
def gc_content(seq: str) -> float:
"""
计算DNA序列的GC含量
GC含量 = (G的个数 + C的个数) / 序列总长度 × 100%
白话:GC含量高的区域热稳定性强(G-C之间有3个氢键,A-T只有2个)
"""
seq = seq.upper().strip() # 统一转大写,去除首尾空白
if len(seq) == 0: # 空序列检查
return 0.0
gc_count = seq.count('G') + seq.count('C') # 统计G和C的个数
return round(gc_count / len(seq) * 100, 2) # 保留2位小数
# 测试
print(gc_content("ATCGATCGATCG")) # 输出:50.0
print(gc_content("GGGGCCCC")) # 输出:100.0
print(gc_content("AAAATTTT")) # 输出:0.0
复杂度: 时间O(n),空间O(1)
3.2 反向互补序列¶
题目: 给定一条DNA序列,返回其反向互补序列。
解题思路: 先互补(A↔T, G↔C),再反转。DNA是双链反向平行的,所以反向互补序列代表另一条链。
def reverse_complement(seq: str) -> str:
"""
生成DNA反向互补序列
步骤:1. 每个碱基取互补(A↔T, G↔C)
2. 反转整个序列
白话:DNA双链像拉链,两条链方向相反且碱基互补
"""
# 互补碱基对照表
complement = {
'A': 'T', 'T': 'A', # A和T互补
'G': 'C', 'C': 'G', # G和C互补
'a': 't', 't': 'a', # 兼容小写
'g': 'c', 'c': 'g',
'N': 'N', 'n': 'n' # N表示未知碱基,互补还是N
}
seq = seq.strip() # 去除首尾空白
comp_seq = ''.join(complement.get(base, 'N') # 逐个碱基取互补
for base in seq) # get(key, 默认值):未知碱基返回N
return comp_seq[::-1] # 反转互补后的序列
# 更简洁的写法(用str.maketrans)
def reverse_complement_v2(seq: str) -> str:
"""使用maketrans的简洁写法"""
table = str.maketrans("ATGCatgc", "TACGtacg") # 创建互补转换表
return seq.translate(table)[::-1] # 先互补再反转
# 测试
print(reverse_complement("ATCGATCG")) # 输出:CGATCGAT
print(reverse_complement("AATTGGCC")) # 输出:GGCCAATT
复杂度: 时间O(n),空间O(n)
3.3 解析FASTA文件¶
题目: 读取FASTA格式文件,返回序列名和序列内容的字典。
解题思路: FASTA格式中>开头的行是序列名,后面的行(直到下一个>或文件末尾)是序列内容。
def parse_fasta(filepath: str) -> dict:
"""
解析FASTA文件,返回 {序列名: 序列} 的字典
FASTA格式示例:
>seq1 description
ATCGATCG
ATCGATCG
>seq2 description
GGCCAATT
"""
sequences = {} # 存储结果:{序列名: 序列}
current_name = None # 当前正在读取的序列名
current_seq = [] # 当前序列的片段列表(最后join拼接)
with open(filepath, 'r') as f:
for line in f:
line = line.strip() # 去除换行符和空白
if not line: # 跳过空行
continue
if line.startswith('>'): # 以>开头 = 新序列的标题行
if current_name: # 如果之前已经有序列在处理
sequences[current_name] = ''.join(current_seq) # 保存上一条
current_name = line[1:].split()[0] # 取>后面的第一个词作为序列名
current_seq = [] # 重置序列片段列表
else:
current_seq.append(line) # 序列内容行,追加到片段列表
# 别忘了保存最后一条序列!(常见bug:漏掉最后一条)
if current_name:
sequences[current_name] = ''.join(current_seq)
return sequences
# 测试
# seqs = parse_fasta("test.fasta")
# for name, seq in seqs.items():
# print(f">{name}\n长度: {len(seq)}\nGC含量: {gc_content(seq)}%")
复杂度: 时间O(n)(n为文件大小),空间O(n)
3.4 统计碱基频率¶
题目: 统计DNA序列中每种碱基的频率和占比。
解题思路: 在词频统计基础上,增加百分比计算和可视化输出。
def base_frequency(seq: str) -> dict:
"""
统计DNA序列中各碱基的数量和频率
返回格式:{'A': {'count': 30, 'freq': 0.25}, ...}
"""
seq = seq.upper().replace('\n', '').replace(' ', '') # 清理序列
total = len(seq) # 序列总长度
if total == 0: # 空序列检查
return {}
result = {}
for base in ['A', 'T', 'G', 'C', 'N']: # 遍历5种可能的碱基
count = seq.count(base) # 统计该碱基出现次数
result[base] = {
'count': count, # 绝对数量
'freq': round(count / total, 4) # 相对频率(保留4位小数)
}
return result
# 漂亮输出
def print_base_freq(seq: str):
"""格式化打印碱基频率表"""
freq = base_frequency(seq)
print(f"序列长度: {len(seq.upper().strip())}")
print(f"{'碱基':<6}{'数量':<10}{'频率':<10}{'柱状图'}")
print("-" * 50)
for base, info in freq.items():
bar = '#' * int(info['freq'] * 50) # 用#画简易柱状图
print(f"{base:<6}{info['count']:<10}{info['freq']:<10}{bar}")
# 测试
print_base_freq("ATCGATCGATCGATCGATCG" * 5)
复杂度: 时间O(n),空间O(1)
3.5 找开放阅读框(ORF)¶
题目: 在DNA序列中找到所有可能的ORF(以ATG开始,以TAA/TAG/TGA结束的区域)。
解题思路: 在3个阅读框中分别查找起始密码子ATG和终止密码子。
def find_orfs(seq: str, min_length: int = 100) -> list:
"""
查找DNA序列中所有开放阅读框(ORF)
ORF = 从ATG(起始密码子)到TAA/TAG/TGA(终止密码子)的区域
白话:ORF就是基因可能编码蛋白质的那一段
参数:
seq: DNA序列
min_length: 最短ORF长度(核苷酸数),默认100
返回:
ORF列表,每个元素包含起始位置、结束位置、长度、序列
"""
seq = seq.upper() # 统一大写
stop_codons = {'TAA', 'TAG', 'TGA'} # 三种终止密码子
orfs = [] # 存储找到的ORF
# 遍历3个阅读框(frame 0, 1, 2)
for frame in range(3): # 从位置0、1、2分别开始
i = frame # 当前位置
while i < len(seq) - 2: # 确保至少还有3个碱基(一个密码子)
codon = seq[i:i+3] # 取3个碱基作为一个密码子
if codon == 'ATG': # 找到起始密码子
# 从ATG开始,往后找终止密码子
for j in range(i + 3, len(seq) - 2, 3): # 每次跳3个碱基
next_codon = seq[j:j+3]
if next_codon in stop_codons: # 找到终止密码子
orf_seq = seq[i:j+3] # ORF序列(含起始和终止)
if len(orf_seq) >= min_length: # 长度过滤
orfs.append({
'start': i + 1, # 起始位置(1-based)
'end': j + 3, # 结束位置
'length': len(orf_seq), # 核苷酸长度
'frame': frame + 1, # 阅读框(1-based)
'sequence': orf_seq # ORF序列
})
break # 找到终止就停(最短ORF策略)
i += 3 # 按密码子步进
return sorted(orfs, key=lambda x: x['length'], reverse=True) # 按长度降序排列
# 测试
test_seq = "AAATGCCCGGGAAATTTCCCGGGTAAATGAAATTTCCCGGGTGA"
orfs = find_orfs(test_seq, min_length=9)
for orf in orfs:
print(f"Frame {orf['frame']}: 位置 {orf['start']}-{orf['end']}, "
f"长度 {orf['length']}nt, 序列: {orf['sequence']}")
复杂度: 时间O(n),空间O(k)(k为ORF数量)
3.6 计算N50¶
题目: 给定一组contig长度,计算N50值。
解题思路: N50是衡量基因组组装质量的指标——把所有contig按长度从长到短排列,累加长度直到超过总长度的50%,此时的contig长度就是N50。
def calculate_n50(lengths: list) -> int:
"""
计算N50
白话:N50就像"中位数的升级版"——
想象把所有序列排成一条线,从最长的开始铺,
铺到超过总长度一半时,你手里那条序列的长度就是N50。
N50越大,说明组装质量越好(长序列越多)。
参数:
lengths: contig长度列表,如 [1000, 500, 300, 200, 100]
返回:
N50值(整数)
"""
if not lengths: # 空列表检查
return 0
sorted_lengths = sorted(lengths, reverse=True) # 从长到短排序
total = sum(sorted_lengths) # 所有contig的总长度
half_total = total / 2 # 总长度的一半
cumulative = 0 # 累加长度
for length in sorted_lengths: # 从最长的开始累加
cumulative += length # 累加当前contig长度
if cumulative >= half_total: # 累加超过总长度的一半
return length # 当前contig长度就是N50
return sorted_lengths[-1] # 理论上不会走到这里
# 同理可计算N90、L50等
def calculate_nx(lengths: list, x: int = 50) -> int:
"""通用版:计算Nx(x可以是50、75、90等)"""
sorted_lengths = sorted(lengths, reverse=True)
total = sum(sorted_lengths)
threshold = total * x / 100 # x%的阈值
cumulative = 0
for length in sorted_lengths:
cumulative += length
if cumulative >= threshold:
return length
return sorted_lengths[-1]
# 测试
contigs = [10000, 8000, 5000, 3000, 2000, 1000, 500, 200]
print(f"N50: {calculate_n50(contigs)}") # 输出:N50: 5000
print(f"N90: {calculate_nx(contigs, 90)}") # 输出:N90: 1000
print(f"总长: {sum(contigs)}") # 总长: 29700
print(f"contig数: {len(contigs)}") # contig数: 8
复杂度: 时间O(n log n)(排序),空间O(n)
4. 数据处理题¶
4.1 CSV文件处理¶
题目: 读取一个CSV文件,按指定列排序并输出前N行。
import csv
def process_csv(filepath: str, sort_col: str, top_n: int = 10) -> list:
"""
读取CSV文件,按指定列排序,返回前N行
适用场景:处理差异分析结果表,按p-value排序找显著基因
"""
rows = []
with open(filepath, 'r') as f:
reader = csv.DictReader(f) # DictReader把每行读成字典
for row in reader:
rows.append(row)
# 按指定列排序(转float处理数值列)
rows.sort(key=lambda x: float(x.get(sort_col, 0)))
return rows[:top_n] # 返回前N行
# 实际场景:处理DESeq2差异分析结果
# top_genes = process_csv("deseq2_results.csv", sort_col="padj", top_n=20)
# for gene in top_genes:
# print(f"{gene['gene_name']}: padj={gene['padj']}, log2FC={gene['log2FoldChange']}")
4.2 JSON解析¶
题目: 解析嵌套JSON数据,提取指定字段。
import json
def parse_json_report(filepath: str) -> dict:
"""
解析fastp的JSON报告,提取关键质控指标
白话:fastp跑完会输出一个JSON文件,里面包含质控前后的统计信息
"""
with open(filepath, 'r') as f:
data = json.load(f) # 把JSON文件读成Python字典
# 提取关键信息(fastp JSON结构)
summary = {
'total_reads_before': data['summary']['before_filtering']['total_reads'],
'total_reads_after': data['summary']['after_filtering']['total_reads'],
'q20_rate_before': data['summary']['before_filtering']['q20_rate'],
'q20_rate_after': data['summary']['after_filtering']['q20_rate'],
'gc_content': data['summary']['after_filtering']['gc_content'],
}
# 计算过滤率
before = summary['total_reads_before']
after = summary['total_reads_after']
summary['filter_rate'] = round((1 - after / before) * 100, 2) # 被过滤掉的比例
return summary
# 测试
# report = parse_json_report("sample_fastp.json")
# print(f"过滤前reads: {report['total_reads_before']:,}")
# print(f"过滤后reads: {report['total_reads_after']:,}")
# print(f"过滤率: {report['filter_rate']}%")
4.3 正则表达式提取¶
题目: 从文本中提取所有的基因ID(如ENSG00000141510格式)。
import re
def extract_gene_ids(text: str) -> list:
"""
用正则表达式提取Ensembl基因ID
Ensembl ID格式:ENSG + 11位数字
白话:正则表达式就像"文字搜索的超级查找功能"
"""
pattern = r'ENSG\d{11}' # ENSG后面跟11位数字
# r'' 表示原始字符串(不转义\)
# \d 匹配数字
# {11} 恰好11位
gene_ids = re.findall(pattern, text) # findall返回所有匹配的列表
return list(set(gene_ids)) # 去重
# 更多常用正则示例
def extract_patterns_demo():
"""常见生信正则表达式示例"""
text = "Sample: SRR12345678, Gene: TP53 (ENSG00000141510), Chr: chr17:7668421-7687490"
# 提取SRA编号
sra = re.findall(r'SRR\d+', text) # SRR + 任意位数字
print(f"SRA编号: {sra}") # ['SRR12345678']
# 提取基因组坐标
coord = re.findall(r'chr\w+:\d+-\d+', text) # chr + 染色体号:起始-结束
print(f"坐标: {coord}") # ['chr17:7668421-7687490']
# 提取基因名(大写字母+数字)
genes = re.findall(r'\b[A-Z][A-Z0-9]{1,}\b', text) # \b是词边界
print(f"基因名: {genes}") # ['SRR12345678', 'TP53', 'ENSG00000141510', 'Chr']
extract_patterns_demo()
4.4 Pandas操作¶
题目: 用Pandas读取物种丰度表,进行筛选、分组统计、合并。
import pandas as pd
def abundance_analysis(filepath: str):
"""
用Pandas分析物种丰度表
白话:Pandas就是Python版的Excel,处理表格数据的神器
"""
# ===== 1. 读取数据 =====
df = pd.read_csv(filepath, sep='\t') # 读取TSV文件(制表符分隔)
# ===== 2. 基本信息 =====
print(f"行数: {len(df)}, 列数: {len(df.columns)}")
print(f"列名: {list(df.columns)}")
print(df.head()) # 显示前5行
# ===== 3. 筛选:只保留丰度>0.01的物种 =====
df_filtered = df[df['abundance'] > 0.01] # 布尔索引筛选
print(f"丰度>1%的物种数: {len(df_filtered)}")
# ===== 4. 分组统计:按分组(疾病/健康)计算平均丰度 =====
group_mean = df.groupby('group')['abundance'].mean() # 按group列分组求均值
print(f"各组平均丰度:\n{group_mean}")
# ===== 5. 排序:按丰度降序 =====
df_sorted = df.sort_values('abundance', ascending=False) # 降序排列
top10 = df_sorted.head(10) # 取前10
print(f"丰度最高的10个物种:\n{top10[['species', 'abundance']]}")
# ===== 6. 添加新列 =====
df['log_abundance'] = df['abundance'].apply( # apply对每个值执行函数
lambda x: round(pd.np.log10(x + 1e-6), 4) # log10转换(加小值避免log(0))
)
# ===== 7. 导出 =====
df_filtered.to_csv('filtered_abundance.tsv', sep='\t', index=False)
return df_filtered
# 测试
# result = abundance_analysis("species_abundance.tsv")
5. 进阶题¶
5.1 装饰器(Decorator)¶
题目: 写一个装饰器,计算函数运行时间。
解题思路: 装饰器本质是一个"函数的包装器",在不修改原函数代码的情况下添加功能。
import time
from functools import wraps # wraps保留原函数的名字和文档字符串
def timer(func):
"""
计时装饰器:自动打印函数的运行时间
白话:就像给函数套了一层"计时外壳"
"""
@wraps(func) # 保留原函数的__name__和__doc__
def wrapper(*args, **kwargs): # *args/**kwargs接受任意参数
start = time.time() # 记录开始时间
result = func(*args, **kwargs) # 执行原函数
elapsed = time.time() - start # 计算耗时
print(f"[{func.__name__}] 运行时间: {elapsed:.2f}秒")
return result # 返回原函数的返回值
return wrapper
# 使用装饰器
@timer # 等价于 parse_fasta = timer(parse_fasta)
def parse_fasta_timed(filepath):
"""解析FASTA文件(带计时)"""
sequences = {}
current_name = None
current_seq = []
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
if line.startswith('>'):
if current_name:
sequences[current_name] = ''.join(current_seq)
current_name = line[1:].split()[0]
current_seq = []
elif line:
current_seq.append(line)
if current_name:
sequences[current_name] = ''.join(current_seq)
return sequences
# 调用时自动打印运行时间
# seqs = parse_fasta_timed("big_file.fasta")
# 输出:[parse_fasta_timed] 运行时间: 2.35秒
5.2 生成器(Generator)¶
题目: 写一个生成器,逐条读取FASTA文件中的序列(内存友好)。
解题思路: 生成器用yield返回数据,不一次性加载全部内容到内存。
def fasta_reader(filepath: str):
"""
FASTA文件的生成器版本
白话:普通函数一次性把所有序列都读进内存;
生成器是"读一条用一条",特别适合处理几个GB的大文件
yield 和 return 的区别:
- return:函数结束,返回所有结果
- yield:暂停函数,返回一个结果,下次调用继续
"""
current_name = None
current_seq = []
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
if line.startswith('>'):
if current_name: # 遇到新序列时
yield current_name, ''.join(current_seq) # yield返回上一条
current_name = line[1:].split()[0]
current_seq = []
elif line:
current_seq.append(line)
if current_name: # 别忘了最后一条
yield current_name, ''.join(current_seq)
# 使用生成器
# for name, seq in fasta_reader("huge_file.fasta"): # 内存只需存一条序列
# if gc_content(seq) > 60: # 找GC含量>60%的序列
# print(f">{name} GC={gc_content(seq)}%")
5.3 上下文管理器(Context Manager)¶
题目: 写一个上下文管理器,自动记录代码块的运行时间。
解题思路: 上下文管理器就是with语句背后的机制,用于管理资源的获取和释放。
import time
from contextlib import contextmanager # 简化上下文管理器的创建
# ========== 方法1:用类实现 ==========
class Timer:
"""
计时上下文管理器(类实现)
白话:with Timer()就像一个自动计时的秒表——进入with时按下开始,离开时按下停止
"""
def __enter__(self): # 进入with块时执行
self.start = time.time() # 记录开始时间
return self # 返回自身(可以在with块中使用)
def __exit__(self, exc_type, exc_val, exc_tb): # 离开with块时执行
self.elapsed = time.time() - self.start # 计算耗时
print(f"耗时: {self.elapsed:.2f}秒")
return False # False = 不吞掉异常
# ========== 方法2:用装饰器实现(更简洁) ==========
@contextmanager
def timer_context(label: str = "代码块"):
"""计时上下文管理器(装饰器实现)"""
start = time.time()
yield # yield之前 = __enter__,之后 = __exit__
elapsed = time.time() - start
print(f"[{label}] 耗时: {elapsed:.2f}秒")
# 使用
# with Timer() as t:
# seqs = parse_fasta("big_file.fasta")
# 输出:耗时: 2.35秒
# with timer_context("FASTA解析"):
# seqs = parse_fasta("big_file.fasta")
# 输出:[FASTA解析] 耗时: 2.35秒
5.4 多线程处理¶
题目: 用多线程/多进程并行处理多个样本。
from concurrent.futures import ProcessPoolExecutor, as_completed
import os
def process_sample(sample_name: str) -> dict:
"""
处理单个样本的函数(模拟耗时操作)
实际场景:每个样本独立运行fastp/Kraken2等
"""
# 模拟耗时操作
import time
time.sleep(1)
return {
'sample': sample_name,
'reads': 1000000,
'gc_content': 45.2
}
def parallel_processing(sample_list: list, max_workers: int = 4) -> list:
"""
多进程并行处理样本
白话:一个人洗100个盘子太慢,找4个人同时洗
注意:
- CPU密集型任务用 ProcessPoolExecutor(多进程)
- IO密集型任务用 ThreadPoolExecutor(多线程)
- Python的GIL限制了多线程的CPU利用率,所以生信任务通常用多进程
"""
results = []
# 创建进程池,max_workers=同时跑几个进程
with ProcessPoolExecutor(max_workers=max_workers) as executor:
# 提交所有任务
future_to_sample = {
executor.submit(process_sample, sample): sample # 提交任务
for sample in sample_list
}
# 收集结果(as_completed:哪个先完成就先处理哪个)
for future in as_completed(future_to_sample):
sample = future_to_sample[future]
try:
result = future.result() # 获取结果
results.append(result)
print(f"完成: {sample}")
except Exception as e:
print(f"失败: {sample}, 错误: {e}")
return results
# 测试
# samples = [f"sample_{i:03d}" for i in range(1, 21)] # 20个样本
# results = parallel_processing(samples, max_workers=4)
# print(f"完成 {len(results)}/{len(samples)} 个样本")
复杂度提示: 并行化的效率取决于CPU核心数和IO瓶颈。4核心理想加速比是4倍,实际通常2-3倍。
6. 面试答题技巧¶
6.1 答题流程(黄金四步)¶
1. 先确认题意("您是要我实现...对吗?")
→ 避免做错方向
2. 说出解题思路("我的思路是...")
→ 让面试官看到你的思维过程
3. 边说边写代码(不要沉默写代码)
→ 面试考的不只是结果,还有沟通能力
4. 写完测试("让我用这个例子测一下...")
→ 展示你有测试意识
6.2 写代码时的注意事项¶
| 做 | 不做 |
|---|---|
| 先写函数签名和注释 | 上来就写循环 |
| 考虑边界情况(空输入、异常值) | 只考虑正常情况 |
| 用有意义的变量名 | 用a、b、c、x |
| 写完后口述测试用例 | 写完就说"完了" |
| 不确定时说"我先用暴力法,再优化" | 卡住了不说话 |
6.3 不会做时的应对¶
- 不要沉默:说出你想到的部分思路
- 可以问提示:"这道题是不是可以用哈希表?"
- 退而求其次:"我先写一个暴力解法,可以吗?"
- 坦诚:"这个我不太熟,但我的思路是..."
7. 速查表¶
Python常用内置函数¶
| 函数 | 用途 | 示例 |
|---|---|---|
len() | 长度 | len("ATCG") → 4 |
sorted() | 排序 | sorted([3,1,2]) → [1,2,3] |
enumerate() | 带索引遍历 | for i, x in enumerate(lst) |
zip() | 并行遍历 | for a, b in zip(lst1, lst2) |
map() | 映射 | list(map(str.upper, seqs)) |
filter() | 过滤 | list(filter(lambda x: x>0, lst)) |
sum() / max() / min() | 聚合 | sum([1,2,3]) → 6 |
str.split() | 分割 | "A,B,C".split(",") → ['A','B','C'] |
str.join() | 拼接 | ",".join(['A','B']) → "A,B" |
str.strip() | 去空白 | " hello ".strip() → "hello" |
生信编程题必背公式¶
| 公式 | 代码 |
|---|---|
| GC含量 | (seq.count('G') + seq.count('C')) / len(seq) |
| 反向互补 | seq.translate(str.maketrans("ATGC","TACG"))[::-1] |
| N50 | 排序后累加到≥50%总长 |
| 碱基频率 | Counter(seq) |
| FASTA解析 | >开头=序列名,其余=序列 |
复杂度速记¶
| 操作 | 时间复杂度 |
|---|---|
| 列表访问/赋值 | O(1) |
| 列表append | O(1)均摊 |
| 列表查找(in) | O(n) |
| 字典查找/插入 | O(1)均摊 |
| 集合查找/插入 | O(1)均摊 |
| 排序 | O(n log n) |
| 字符串拼接(join) | O(n) |
8. 延伸资源¶
| 资源 | 说明 |
|---|---|
| Rosalind | 经典生信编程练习平台,强烈推荐 |
| LeetCode | 算法题练习(生信面试一般不考Hard) |
| Biopython教程 | Python生信工具库,很多题用它更简洁 |
| 牛客网-生信面经 | 查看真实面试题 |
| 《Python编程:从入门到实践》 | 基础不牢时翻一翻 |
| Real Python | 高质量Python教程网站 |
记住: 生信编程面试的核心不是刷LeetCode,而是:(1) 能处理文件(FASTA/CSV/JSON),(2) 能操作序列(GC/互补/ORF),(3) 能用Pandas做数据分析,(4) 代码写得清晰有注释。把本篇的生信专项题练熟,面试编程环节基本稳了。