跳转至

Python面试编程题精选

一句话说明: 生信面试的编程环节,考的不是LeetCode Hard,而是你能不能用Python处理文件、解析序列、操作数据——本篇精选基础题+生信专项题+数据处理题+进阶题,每道都给解题思路和完整代码。


目录

  1. 生信面试编程题类型概览
  2. 基础题
  3. 生信专项题
  4. 数据处理题
  5. 进阶题
  6. 面试答题技巧
  7. 速查表
  8. 延伸资源

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)
列表appendO(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) 代码写得清晰有注释。把本篇的生信专项题练熟,面试编程环节基本稳了。