68. 面向对象编程 OOP 基础¶
一句话说明: OOP(Object-Oriented Programming)是把代码按"对象"来组织的编程方式,就像现实世界里把东西分类管理一样,让代码更好维护、更好复用。
1. 什么是 OOP¶
白话解释:
想象你在整理房间。函数式编程像"把所有东西扔一个大箱子",需要什么自己翻。OOP 像"衣服放衣柜、书放书架、碗放厨房"——每个"对象"都有自己的属性(数据)和方法(行为)。
传统写法(面条式代码):
gene_name = "BRCA1"
gene_length = 81189
print(gene_name + " 长度是 " + str(gene_length))
OOP写法:
gene = Gene("BRCA1", 81189) # 创建一个Gene对象
gene.show_info() # 调用对象的方法
OOP 的核心思想: 把数据(属性)和操作数据的函数(方法)打包在一起,形成"对象"。
2. 四大特性¶
2.1 封装(Encapsulation)¶
白话: 把东西装进盒子里,只留几个按钮给外面用。你不需要知道电视机里面怎么工作的,按遥控器就行。
class Gene:
"""基因类 —— 封装基因的基本信息"""
def __init__(self, name, sequence):
self.name = name # 公开属性:基因名,外部可直接访问
self._organism = "Human" # 受保护属性:约定不要从外部随意访问(单下划线)
self.__sequence = sequence # 私有属性:外部不能直接访问(双下划线)
def get_sequence(self):
"""提供一个安全的方式来获取序列(getter方法)"""
return self.__sequence # 通过方法间接访问私有属性
def get_length(self):
"""计算序列长度"""
return len(self.__sequence) # 内部可以自由访问私有属性
# 使用
gene = Gene("BRCA1", "ATCGATCG")
print(gene.name) # ✅ 可以访问:BRCA1
print(gene.get_sequence()) # ✅ 通过方法访问:ATCGATCG
# print(gene.__sequence) # ❌ 报错!外部不能直接访问私有属性
2.2 继承(Inheritance)¶
白话: 儿子继承父亲的财产,还可以自己再挣更多。子类继承父类的属性和方法,还可以添加自己的。
class Sequence:
"""父类:通用序列"""
def __init__(self, seq_id, sequence):
self.seq_id = seq_id # 序列ID
self.sequence = sequence # 序列字符串
def get_length(self):
"""返回序列长度"""
return len(self.sequence)
class DNASequence(Sequence):
"""子类:DNA序列,继承自Sequence"""
def __init__(self, seq_id, sequence):
super().__init__(seq_id, sequence) # 调用父类的__init__
self.seq_type = "DNA" # 子类自己的新属性
def gc_content(self):
"""计算GC含量 —— 这是DNA特有的方法"""
gc = self.sequence.upper().count('G') + self.sequence.upper().count('C')
return gc / len(self.sequence) # GC碱基数 / 总长度
def complement(self):
"""返回互补链"""
comp_map = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G'}
return ''.join(comp_map.get(base, 'N') for base in self.sequence.upper())
class RNASequence(Sequence):
"""子类:RNA序列"""
def __init__(self, seq_id, sequence):
super().__init__(seq_id, sequence)
self.seq_type = "RNA"
# 使用
dna = DNASequence("gene_001", "ATCGATCG")
print(dna.get_length()) # 8 —— 继承自父类的方法
print(dna.gc_content()) # 0.5 —— 子类自己的方法
print(dna.complement()) # TAGCTAGC
2.3 多态(Polymorphism)¶
白话: 同一个动作,不同的人做出来效果不同。比如"说话",猫是"喵",狗是"汪"——同一个方法名,不同对象有不同的表现。
class Sequence:
"""序列基类"""
def describe(self):
return "我是一个序列"
class DNASequence(Sequence):
"""DNA序列"""
def describe(self):
return "我是DNA序列,由ATCG组成" # 重写(override)父类方法
class ProteinSequence(Sequence):
"""蛋白质序列"""
def describe(self):
return "我是蛋白质序列,由20种氨基酸组成"
# 多态的威力:同一个函数,传入不同对象,行为不同
def print_info(seq_obj):
"""接受任何Sequence子类的对象"""
print(seq_obj.describe()) # 自动调用各自的describe方法
sequences = [DNASequence(), ProteinSequence()]
for seq in sequences:
print_info(seq)
# 输出:
# 我是DNA序列,由ATCG组成
# 我是蛋白质序列,由20种氨基酸组成
2.4 抽象(Abstraction)¶
白话: 画一张设计图纸,规定"房子必须有门和窗",但不规定门窗长什么样。抽象类定义接口规范,子类必须实现。
from abc import ABC, abstractmethod # abc = Abstract Base Class
class SequenceAnalyzer(ABC):
"""抽象类:定义序列分析器的接口规范"""
@abstractmethod
def parse(self, filepath):
"""子类必须实现这个方法:解析文件"""
pass # 抽象方法没有具体实现
@abstractmethod
def analyze(self):
"""子类必须实现这个方法:执行分析"""
pass
class FastaAnalyzer(SequenceAnalyzer):
"""具体类:FASTA文件分析器"""
def parse(self, filepath):
"""实现parse方法"""
print(f"正在解析FASTA文件: {filepath}")
def analyze(self):
"""实现analyze方法"""
print("正在分析FASTA序列...")
# analyzer = SequenceAnalyzer() # ❌ 报错!抽象类不能实例化
analyzer = FastaAnalyzer() # ✅ 具体类可以实例化
analyzer.parse("genes.fasta") # 正在解析FASTA文件: genes.fasta
3. 类和对象¶
3.1 基本概念¶
3.2 核心魔术方法¶
class Gene:
"""基因类 —— 演示常用魔术方法"""
def __init__(self, name, sequence, chromosome=None):
"""构造方法:创建对象时自动调用
白话:新生儿出生时填的"出生信息表"
"""
self.name = name # 基因名
self.sequence = sequence # 序列
self.chromosome = chromosome # 染色体位置(可选参数)
def __str__(self):
"""给人看的字符串表示:print()时调用
白话:你的"自我介绍"
"""
return f"Gene({self.name}, len={len(self.sequence)})"
def __repr__(self):
"""给开发者看的字符串表示:在交互式终端直接输入对象时显示
白话:你的"身份证信息",要能用这个字符串重建对象
"""
return f"Gene('{self.name}', '{self.sequence}', '{self.chromosome}')"
# 使用
gene = Gene("TP53", "ATCGATCG", "chr17")
print(gene) # Gene(TP53, len=8) ← 调用__str__
gene # Gene('TP53', 'ATCGATCG', 'chr17') ← 调用__repr__(交互式)
3.3 self 是什么¶
class Gene:
def __init__(self, name): # self = 当前正在创建/操作的那个对象
self.name = name # "把name存到这个对象身上"
def show(self): # self自动传入,调用时不用写
print(self.name)
gene1 = Gene("BRCA1") # self → gene1 这个对象
gene2 = Gene("TP53") # self → gene2 这个对象
gene1.show() # 等同于 Gene.show(gene1),输出 BRCA1
gene2.show() # 等同于 Gene.show(gene2),输出 TP53
4. 属性和方法¶
class Gene:
"""演示三种属性和三种方法"""
# ---- 类属性(所有对象共享,像"物种共同特征")----
kingdom = "Animalia" # 所有Gene对象共享这个值
gene_count = 0 # 计数器:记录创建了多少个Gene对象
def __init__(self, name, sequence):
# ---- 实例属性(每个对象独有,像"个人特征")----
self.name = name
self.sequence = sequence
Gene.gene_count += 1 # 每创建一个对象,计数器+1
# ---- 实例方法(操作具体对象的数据)----
def gc_content(self):
"""计算这个基因的GC含量"""
seq = self.sequence.upper()
return (seq.count('G') + seq.count('C')) / len(seq)
# ---- 类方法(操作类本身,用@classmethod装饰器)----
@classmethod
def get_count(cls):
"""返回已创建的Gene对象数量
cls = 类本身(不是具体对象)
"""
return cls.gene_count
# ---- 静态方法(和类相关但不需要访问类或对象的数据)----
@staticmethod
def is_valid_dna(sequence):
"""检查序列是否是合法的DNA序列
不需要self或cls,只是一个"归类"到Gene下的工具函数
"""
return all(base in 'ATCGatcg' for base in sequence)
# 使用
g1 = Gene("BRCA1", "ATCGATCG")
g2 = Gene("TP53", "GCGCATAT")
print(g1.gc_content()) # 0.5 —— 实例方法
print(Gene.get_count()) # 2 —— 类方法
print(Gene.is_valid_dna("ATCG")) # True —— 静态方法
print(Gene.is_valid_dna("ATCX")) # False
5. 常用魔术方法¶
class GeneList:
"""基因列表类 —— 演示常用魔术方法"""
def __init__(self, genes=None):
self._genes = genes or [] # 内部存储基因列表
def add(self, gene):
"""添加一个基因"""
self._genes.append(gene)
def __len__(self):
"""让len()函数可以作用于GeneList对象"""
return len(self._genes)
def __getitem__(self, index):
"""让对象支持 genelist[0] 这样的下标访问"""
return self._genes[index]
def __contains__(self, gene_name):
"""让对象支持 'BRCA1' in genelist 这样的判断"""
return any(g.name == gene_name for g in self._genes)
def __iter__(self):
"""让对象支持 for gene in genelist 这样的遍历"""
return iter(self._genes)
def __eq__(self, other):
"""让对象支持 == 比较"""
if not isinstance(other, GeneList):
return False
return self._genes == other._genes
def __lt__(self, other):
"""让对象支持 < 比较(按基因数量)"""
return len(self._genes) < len(other._genes)
# 使用
gl = GeneList()
gl.add(Gene("BRCA1", "ATCG"))
gl.add(Gene("TP53", "GCGC"))
print(len(gl)) # 2 ← __len__
print(gl[0].name) # BRCA1 ← __getitem__
print("BRCA1" in gl) # True ← __contains__
for gene in gl: # ← __iter__
print(gene.name)
6. 生信中的 OOP 应用¶
6.1 Gene 类¶
class Gene:
"""生信中常用的基因类"""
def __init__(self, gene_id, name, sequence, chromosome=None, start=None, end=None):
self.gene_id = gene_id # 基因ID,如 ENSG00000141510
self.name = name # 基因名,如 TP53
self.sequence = sequence.upper() # DNA序列(统一转大写)
self.chromosome = chromosome # 染色体,如 chr17
self.start = start # 起始位置
self.end = end # 终止位置
def gc_content(self):
"""计算GC含量"""
g = self.sequence.count('G') # 统计G的数量
c = self.sequence.count('C') # 统计C的数量
return round((g + c) / len(self.sequence), 4) # 保留4位小数
def transcribe(self):
"""DNA → RNA(T替换为U)"""
return self.sequence.replace('T', 'U')
def complement(self):
"""返回互补链"""
comp = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G'}
return ''.join(comp[b] for b in self.sequence)
def reverse_complement(self):
"""返回反向互补链(生信最常用!)"""
return self.complement()[::-1] # 先互补再反转
def find_motif(self, motif):
"""查找motif在序列中的所有位置(0-based)"""
positions = [] # 存储找到的位置
start = 0
while True:
pos = self.sequence.find(motif.upper(), start)
if pos == -1: # 没找到了
break
positions.append(pos)
start = pos + 1 # 从下一个位置继续找
return positions
def __len__(self):
return len(self.sequence)
def __str__(self):
return f"Gene({self.name}, chr={self.chromosome}, len={len(self)})"
def __repr__(self):
return f"Gene('{self.gene_id}', '{self.name}', seq_len={len(self)})"
# 使用示例
tp53 = Gene("ENSG00000141510", "TP53", "ATCGATCGCCGCATAT", "chr17", 7668402, 7687550)
print(tp53) # Gene(TP53, chr=chr17, len=16)
print(f"GC含量: {tp53.gc_content()}") # 0.5
print(f"RNA: {tp53.transcribe()}") # AUCGAUCGCCGCAUAU
print(f"反向互补: {tp53.reverse_complement()}") # ATATGCGGCGATCGAT
print(f"ATG位置: {tp53.find_motif('ATCG')}") # [0, 4]
6.2 Pipeline 类¶
import time
class BioinfoStep:
"""流程中的一个步骤"""
def __init__(self, name, command, description=""):
self.name = name # 步骤名称
self.command = command # 要执行的命令
self.description = description
self.status = "pending" # pending/running/done/failed
self.start_time = None
self.end_time = None
def run(self):
"""执行这个步骤"""
self.status = "running"
self.start_time = time.time()
print(f" [运行中] {self.name}: {self.command}")
# 实际项目中这里会用subprocess.run(self.command)
time.sleep(0.1) # 模拟运行时间
self.status = "done"
self.end_time = time.time()
def elapsed(self):
"""计算运行耗时"""
if self.start_time and self.end_time:
return round(self.end_time - self.start_time, 2)
return 0
class Pipeline:
"""生信分析流程管理器"""
def __init__(self, name):
self.name = name
self.steps = [] # 按顺序存储所有步骤
def add_step(self, name, command, description=""):
"""添加一个分析步骤"""
step = BioinfoStep(name, command, description)
self.steps.append(step)
return self # 返回self支持链式调用
def run_all(self):
"""按顺序执行所有步骤"""
print(f"=== 开始流程: {self.name} ===")
for i, step in enumerate(self.steps, 1):
print(f"\n步骤 {i}/{len(self.steps)}:")
step.run()
print(f" [完成] 耗时 {step.elapsed()}s")
print(f"\n=== 流程 {self.name} 全部完成 ===")
def summary(self):
"""打印流程摘要"""
print(f"\n{'步骤名':<20} {'状态':<10} {'耗时(s)':<10}")
print("-" * 40)
for step in self.steps:
print(f"{step.name:<20} {step.status:<10} {step.elapsed():<10}")
# 使用示例:宏基因组分析流程
pipeline = Pipeline("宏基因组分析")
pipeline.add_step("质控", "fastp -i raw.fq -o clean.fq", "去除低质量reads")
pipeline.add_step("去宿主", "bowtie2 -x human -U clean.fq | samtools view -f 4", "去除人类基因组")
pipeline.add_step("组装", "megahit -r filtered.fq -o assembly/", "宏基因组组装")
pipeline.add_step("基因预测", "prodigal -i contigs.fa -o genes.gff", "预测ORF")
pipeline.add_step("功能注释", "eggnog-mapper -i genes.faa -o annotation", "功能注释")
pipeline.run_all()
pipeline.summary()
7. OOP vs 函数式编程对比¶
| 维度 | OOP | 函数式编程 |
|---|---|---|
| 核心思想 | 围绕"对象"组织代码 | 围绕"函数"组织代码 |
| 数据管理 | 数据和方法绑定在对象里 | 数据和函数分离 |
| 状态 | 对象有状态(可变) | 尽量无状态(不可变) |
| 适合场景 | 大型项目、GUI、游戏、复杂实体 | 数据处理、数学计算、管道流程 |
| 生信应用 | 设计Gene/Sample/Pipeline类 | 写分析脚本、数据转换 |
| Python风格 | class Gene: ... | def gc_content(seq): ... |
| 学习曲线 | 概念较多(类/继承/多态) | 概念较少但思维转变大 |
实际建议: Python 支持混合编程。小脚本用函数式,复杂项目用 OOP,生信中两者常混合使用。
8. 常见面试题¶
题1:类属性 vs 实例属性¶
# 问:以下代码输出什么?
class Counter:
count = 0 # 类属性
def __init__(self):
Counter.count += 1 # 修改的是类属性
c1 = Counter()
c2 = Counter()
c3 = Counter()
print(Counter.count) # 答:3(类属性被所有实例共享)
print(c1.count) # 答:3(通过实例也能访问类属性)
题2:方法解析顺序(MRO)¶
# 问:多重继承时,Python怎么决定调用哪个父类的方法?
class A:
def who(self):
return "A"
class B(A):
def who(self):
return "B"
class C(A):
def who(self):
return "C"
class D(B, C): # 多重继承
pass
d = D()
print(d.who()) # 答:B
# MRO顺序:D → B → C → A(C3线性化算法)
print(D.__mro__) # 查看完整MRO链
题3:property 装饰器¶
# 问:如何让属性有验证逻辑?
class Gene:
def __init__(self, name, sequence):
self.name = name
self.sequence = sequence # 触发setter验证
@property
def sequence(self):
"""getter:获取序列时调用"""
return self._sequence
@sequence.setter
def sequence(self, value):
"""setter:设置序列时自动验证"""
if not all(b in 'ATCGatcg' for b in value):
raise ValueError(f"非法DNA序列: {value}")
self._sequence = value.upper()
gene = Gene("TP53", "ATCG") # ✅ 合法
# gene = Gene("TP53", "ATCX") # ❌ ValueError: 非法DNA序列: ATCX
题4:__slots__ 优化内存¶
# 问:处理百万个对象时如何节省内存?
class GeneSlim:
__slots__ = ['name', 'length'] # 限制只能有这两个属性,不创建__dict__
def __init__(self, name, length):
self.name = name
self.length = length
# 对比:普通类每个对象约200字节,用__slots__约100字节
# 处理100万基因时,内存从200MB降到100MB
题5:设计模式 —— 单例模式¶
# 问:如何确保一个数据库连接类只有一个实例?
class DatabaseConnection:
_instance = None # 类属性,存储唯一实例
def __new__(cls, *args, **kwargs):
"""控制对象创建过程"""
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance # 总是返回同一个实例
def __init__(self, host="localhost"):
self.host = host
db1 = DatabaseConnection("server1")
db2 = DatabaseConnection("server2")
print(db1 is db2) # True —— 它们是同一个对象
9. 速查表¶
| 概念 | 语法 | 说明 |
|---|---|---|
| 定义类 | class Gene: | 类名用大驼峰 |
| 构造方法 | __init__(self, ...) | 创建对象时自动调用 |
| 实例属性 | self.name = name | 每个对象独有 |
| 类属性 | count = 0(类体内) | 所有对象共享 |
| 实例方法 | def method(self): | 操作具体对象 |
| 类方法 | @classmethod def m(cls): | 操作类本身 |
| 静态方法 | @staticmethod def m(): | 工具函数 |
| 继承 | class DNA(Sequence): | 子类继承父类 |
| 调用父类 | super().__init__() | 调用父类方法 |
| 抽象类 | class X(ABC): + @abstractmethod | 定义接口规范 |
| 私有属性 | self.__attr | 双下划线,名称改写 |
| property | @property / @x.setter | 属性验证 |
| 打印对象 | __str__ / __repr__ | 字符串表示 |
| 长度 | __len__ | 支持 len(obj) |
| 下标 | __getitem__ | 支持 obj[i] |
| 比较 | __eq__ / __lt__ | 支持 == / < |
| 节省内存 | __slots__ | 限制属性列表 |
| 多重继承顺序 | ClassName.__mro__ | 查看方法解析顺序 |
10. 延伸资源¶
| 资源 | 类型 | 说明 |
|---|---|---|
| Python官方教程 - Classes | 官方文档 | Python 3.14 最新类教程 |
| Real Python - OOP | 教程 | 通俗易懂的OOP入门 |
| 《流畅的Python》第11-14章 | 书籍 | 深入理解Python对象模型 |
| Python设计模式 | 网站 | 23种设计模式Python实现 |
| Biopython Source Code | 源码 | 学习生信领域的OOP实践 |
面试提示: 面试官不会问你背定义,而是看你能不能用OOP思维解决实际问题。比如"设计一个类来管理FASTQ文件"——这时候把封装、继承、方法设计讲清楚,比背四大特性有用得多。