跳转至

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 基本概念

类(Class)= 设计图纸       →  class Gene:
对象(Object)= 按图纸造的实物  →  brca1 = Gene("BRCA1", "ATCG...")

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文件"——这时候把封装、继承、方法设计讲清楚,比背四大特性有用得多。