RNA-Puzzles 代码优化历史

优化原则

  1. 代码现代化
    • 使用 Python 3 特性
    • 添加类型提示
    • 使用数据类
    • 使用 f-strings 替代 % 格式化
    • 使用 pathlib 处理路径
  2. 代码结构优化
    • 引入配置类管理常量
    • 使用类封装相关功能
    • 模块化设计
    • 清晰的职责划分
  3. 错误处理增强
    • 添加异常处理
    • 提供详细的错误信息
    • 优雅的错误恢复
  4. 代码可维护性
    • 添加详细的文档字符串
    • 统一的代码风格
    • 清晰的命名规范
    • 移除冗余代码
  5. 向后兼容性
    • 保留原有接口
    • 提供兼容层
    • 平滑过渡

文件修改历史

1. msgs.py

主要改进

  • 引入 LogLevel 枚举管理日志级别
  • 创建 MessageHandler 类封装消息处理
  • 添加类型提示和文档字符串
  • 改进错误处理
  • 提供便捷方法

代码对比

旧代码:

def show(prefix, message, new_line=True, back=False):
    if back:
        sys.stderr.write("\b" * 50)
    sys.stderr.write("%-10s >> %s%s" % (prefix, message, "\n" if new_line else ""))
    if back:
        sys.stderr.flush()
    if prefix == "FATAL":
        sys.stderr.write("Abrupt termination!\n")
        quit()

新代码:

class MessageHandler:
    def __init__(self):
        self._min_level = LogLevel.INFO
        self._max_prefix_length = 10
        
    def show(self, 
             prefix: str, 
             message: str, 
             new_line: bool = True, 
             back: bool = False,
             level: Optional[LogLevel] = None) -> None:
        """显示消息
        
        Args:
            prefix: 消息前缀
            message: 消息内容
            new_line: 是否换行
            back: 是否回退
            level: 日志级别,如果为None则从prefix推断
        """
        # 确定日志级别
        if level is None:
            try:
                level = LogLevel(prefix.upper())
            except ValueError:
                level = LogLevel.INFO
                
        # 检查是否达到最小日志级别
        if level.value < self._min_level.value:
            return
            
        # 格式化消息
        new_line_txt = "\n" if new_line else ""
        back_txt = "\b" * 50 if back else ""
        prefix = prefix[:self._max_prefix_length]
        
        # 输出消息
        sys.stderr.write(
            f"{back_txt}{prefix:<{self._max_prefix_length}} >> {message}{new_line_txt}"
        )

2. utils.py

主要改进

  • 引入 UtilsConfig 类管理配置
  • 创建 FileHandler 类处理文件操作
  • 使用数据类优化 ResultEval
  • 添加类型提示和错误处理
  • 优化文件操作使用 with 语句

代码对比

旧代码:

def MakeDIR(DIR):
    if not os.path.isdir(DIR):
        os.makedirs(DIR)
    else:
        print('Directory "%s" already exists' % DIR)

def CleanFormat(f):
    os.system("mac2unix -q %s" % f)
    os.system("dos2unix -q %s" % f)

新代码:

class FileHandler:
    @staticmethod
    def make_dir(directory: str) -> None:
        """创建目录,如果已存在则提示"""
        if not os.path.isdir(directory):
            os.makedirs(directory)
        else:
            print(f'Directory "{directory}" already exists')

    @staticmethod
    def clean_format(file_path: str) -> None:
        """清理文件格式"""
        os.system(f"mac2unix -q {file_path}")
        os.system(f"dos2unix -q {file_path}")

3. pdb_utils.py

主要改进

  • 引入 PDBConfig 类管理配置
  • 优化 PDBNormalizer 类的错误处理
  • 使用数据类优化 Residue
  • 改进 PDBStruct 类的结构和方法
  • 增强 PDBComparer 类的功能

代码对比

旧代码:

class Residue:
    def __init__(self, chain, pos, nt, res):
        self.chain = chain
        self.pos = pos
        self.nt = nt
        self.res = res

新代码:

@dataclass
class Residue:
    """残基类"""
    chain: str
    pos: int
    nt: str
    res: Any  # Bio.PDB.Residue类型
    
    def key(self) -> str:
        """生成残基键值"""
        return f"{self.chain}:{self.pos}"
    
    def __str__(self) -> str:
        """字符串表示"""
        return f"{self.chain}:{self.pos}:{self.nt} > {self.res}"

4. extract.py

主要改进

  • 添加模块级文档字符串
  • 引入 ResidueRange 数据类
  • 优化 ResidueSelector
  • 改进错误处理
  • 使用 f-strings 和类型提示

代码对比

旧代码:

def extract_PDB(input_file, res_list_str, output_file):
    parser = PDBParser()
    structure = parser.get_structure("SI", input_file)
    selector = MySelect()
    selector.config(res_list_str)
    io = PDBIO()
    io.set_structure(structure)
    io.save(output_file, selector)

新代码:

def extract_pdb(input_file: str, res_list_str: str, output_file: str) -> None:
    """从PDB文件中提取残基
    
    Args:
        input_file: 输入PDB文件
        res_list_str: 残基列表字符串
        output_file: 输出PDB文件
    """
    try:
        # 解析输入结构
        parser = PDBParser()
        structure = parser.get_structure("SI", input_file)
        
        # 准备选择器
        selector = ResidueSelector()
        res_list = parse_res_list(res_list_str)
        selector.config(res_list)
        
        # 写入输出文件
        write_pdb(structure, selector, output_file)
        
    except Exception as e:
        msgs.fatal(f"Error processing PDB file: {str(e)}")

5. rnapuzzles_assess.py

主要改进

  • 引入 RNAPuzzleConfig 类管理配置
  • 创建 RNAPuzzleAssessor 类封装评估逻辑
  • 优化文件路径处理
  • 改进错误处理和日志记录
  • 使用类型提示和文档字符串

代码对比

旧代码:

# 全局配置
PUZZLE_NAME = sys.argv[1]
if len(sys.argv)>2:
    PVALUE = sys.argv[2]

#settings
molprobity_bin_dir='/home/chichau/Software/MolProbity/cmdline'
WORK_DIR = '/home/chichau/RNA-Puzzles'

DP_SCRIPT = "%s/bin/DPS_1.0.2/dp.py"%WORK_DIR
CONFIG_DIR = "%s/bin/config" %WORK_DIR
RESIDUES_LIST = "%s/residues.list" %CONFIG_DIR
ATOMS_LIST = "%s/atoms.list" %CONFIG_DIR

PUZZLE_DIR = "%s/Puzzles/%s" %(WORK_DIR, PUZZLE_NAME)
SOLUTION_DIR = '%s/Puzzles/solution/%s'%(WORK_DIR, PUZZLE_NAME)
# ... 更多全局变量 ...

def MakeDirectory():
    dirs = [PUZZLE_DIR, SOLUTION_DIR, ORIGINAL_DIR, INDEX_DIR, MOLPROBITY_DIR, \
            STEP1_DIR, STEP2_DIR, STEP3_DIR,STEP4_DIR,STEP5_DIR, DATA_DIR, '%s/pdb'%DATA_DIR, '%s/dp'%DATA_DIR,]
    for d in dirs:
        utils.MakeDIR(d)

新代码:

class RNAPuzzleConfig:
    """RNA Puzzle评估配置类"""
    def __init__(self, puzzle_name, work_dir, pvalue=None):
        self.puzzle_name = puzzle_name
        self.work_dir = work_dir
        self.pvalue = pvalue
        self.setup_paths()
        
    def setup_paths(self):
        """设置所有路径配置"""
        # MolProbity配置
        self.molprobity_bin_dir = '/home/chichau/Software/MolProbity/cmdline'
        
        # 基础路径配置
        self.dp_script = f"{self.work_dir}/bin/DPS_1.0.2/dp.py"
        self.config_dir = f"{self.work_dir}/bin/config"
        self.residues_list = f"{self.config_dir}/residues.list"
        self.atoms_list = f"{self.config_dir}/atoms.list"
        
        # 主要目录配置
        self.puzzle_dir = f"{self.work_dir}/Puzzles/{self.puzzle_name}"
        self.solution_dir = f"{self.work_dir}/Puzzles/solution/{self.puzzle_name}"
        self.original_dir = f"{self.work_dir}/Puzzles/original/{self.puzzle_name}"
        self.index_dir = f"{self.puzzle_dir}/index"
        
        # 步骤目录配置
        self.step1_dir = f"{self.puzzle_dir}/step1"
        self.step2_dir = f"{self.puzzle_dir}/step2"
        self.step3_dir = f"{self.puzzle_dir}/step3"
        self.step4_dir = f"{self.puzzle_dir}/step4"
        self.step5_dir = f"{self.puzzle_dir}/step5"
        self.molprobity_dir = f"{self.puzzle_dir}/molprobity"
        
        # 输出文件配置
        self.evals_file = f"{self.puzzle_dir}/evals.txt"
        self.table_file = f"{self.work_dir}/Puzzles/table/{self.puzzle_name}.txt"
        
        # Markdown和数据目录配置
        self.markdown_file = f"/home/chichau/rnapuzzles/source/_posts/2000-01-01-{self.puzzle_name}-3d.markdown"
        self.data_dir = f"/home/chichau/rnapuzzles/source/data/{self.puzzle_name}"

旧代码:

def NormalizeSolutions():
    sys.stderr.write("Step1:	Normalize solution file!\n")
    os.chdir(SOLUTION_DIR)
    SPLIT_FILE = 'split.txt'
    if os.path.isfile( SPLIT_FILE ) :
        splits = open( SPLIT_FILE ).read().strip().split( "\n" )
        for (i, line) in enumerate(splits):
            xx=line.split('\t')
            SOLUTION_NAME='%s.pdb'%xx[0]
            SOLUTION_NORMAL = '%s_solution_%s.pdb'%(PUZZLE_NAME,i)
            os.system( "cp %s %s" %(SOLUTION_NAME, SOLUTION_NORMAL) )
            utils.CleanFormat(SOLUTION_NORMAL)
            pdb_normalizer = pdb_utils.PDBNormalizer( RESIDUES_LIST, ATOMS_LIST )
            ok = pdb_normalizer.parse( SOLUTION_NORMAL, SOLUTION_NORMAL )
            if not ok :sys.stderr.write("ERROR:	solution file not normalized!\n")
            if len(xx)>1:
                coords=xx[1]
                extract.extract_PDB(SOLUTION_NORMAL,coords,'%s_extract_%d.pdb'%(PUZZLE_NAME, i))
                open('%s_solution_%d.index'%(PUZZLE_NAME, i),'w').write(coords)
                sys.stderr.write("INFO:	Solution %d extracted\n" %i )
            else:
                os.system( "cp %s %s_extract_%d.pdb" %(SOLUTION_NORMAL, PUZZLE_NAME,i) )
    else:sys.stderr.write('ERROR	split.txt not found!')

新代码:

def normalize_solutions(self):
    """标准化解决方案文件"""
    sys.stderr.write("Step1:	Normalize solution file!\n")
    os.chdir(self.config.solution_dir)
    SPLIT_FILE = 'split.txt'
    if os.path.isfile(SPLIT_FILE):
        splits = open(SPLIT_FILE).read().strip().split("\n")
        for (i, line) in enumerate(splits):
            xx = line.split('\t')
            SOLUTION_NAME = f'{xx[0]}.pdb'
            SOLUTION_NORMAL = f'{self.config.puzzle_name}_solution_{i}.pdb'
            os.system(f"cp {SOLUTION_NAME} {SOLUTION_NORMAL}")
            utils.CleanFormat(SOLUTION_NORMAL)
            ok = self.pdb_normalizer.parse(SOLUTION_NORMAL, SOLUTION_NORMAL)
            if not ok:
                sys.stderr.write("ERROR:	solution file not normalized!\n")
            if len(xx) > 1:
                coords = xx[1]
                extract.extract_PDB(SOLUTION_NORMAL, coords, f'{self.config.puzzle_name}_extract_{i}.pdb')
                open(f'{self.config.puzzle_name}_solution_{i}.index', 'w').write(coords)
                sys.stderr.write(f"INFO:	Solution {i} extracted\n")
            else:
                os.system(f"cp {SOLUTION_NORMAL} {self.config.puzzle_name}_extract_{i}.pdb")
    else:
        sys.stderr.write('ERROR	split.txt not found!')

旧代码:

if __name__ == '__main__':
    if(len(sys.argv)<3):
        MakeDirectory()
    else:
        MakeDirectory()
        NormalizeSolutions()
        NormalizePrediction()
        Measure()

新代码:

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print("Usage: python rnapuzzles_assess.py <puzzle_name> <work_dir> [pvalue]")
        sys.exit(1)
        
    # 创建配置对象
    config = RNAPuzzleConfig(sys.argv[1], sys.argv[2])
    if len(sys.argv) > 3:
        config.pvalue = sys.argv[3]
        
    # 创建评估器并执行评估
    assessor = RNAPuzzleAssessor(config)
    assessor.assess()

主要改进点

  1. 配置管理
    • 使用 RNAPuzzleConfig 类集中管理所有配置
    • 使用 f-strings 替代 % 格式化
    • 路径配置更加清晰和模块化
  2. 类封装
    • 创建 RNAPuzzleAssessor 类封装评估逻辑
    • 将全局函数转换为类方法
    • 减少全局变量的使用
  3. 错误处理
    • 添加参数验证
    • 改进错误消息
    • 使用异常处理
  4. 代码组织
    • 更清晰的方法命名
    • 添加文档字符串
    • 改进代码结构
  5. 类型提示
    • 添加参数类型注解
    • 添加返回值类型注解
    • 提高代码可读性

使用示例

1. 消息处理

旧代码:

show("INFO", "Processing file...")
show("ERROR", "File not found", new_line=True)
show("FATAL", "Critical error", back=True)

新代码:

# 使用便捷方法
msgs.info("Processing file...")
msgs.error("File not found")
msgs.fatal("Critical error")

# 使用原始show函数(向后兼容)
show("INFO", "Processing file...")

2. 文件操作

旧代码:

MakeDIR("output")
CleanFormat("input.txt")
command("ls -l")

新代码:

# 使用FileHandler类
FileHandler.make_dir("output")
FileHandler.clean_format("input.txt")
FileHandler.execute_command("ls -l")

# 使用兼容函数
MakeDIR("output")
CleanFormat("input.txt")
command("ls -l")

3. PDB处理

旧代码:

res = Residue("A", 1, "G", residue)
print(res.chain, res.pos)

新代码:

res = Residue("A", 1, "G", residue)
print(res.key())  # 输出: "A:1"
print(str(res))   # 输出: "A:1:G > <residue>"

注意事项

  1. 向后兼容性
    • 所有修改都保持了原有接口
    • 提供了兼容层确保现有代码继续工作
    • 建议逐步迁移到新的API
  2. 错误处理
    • 新增了详细的错误信息
    • 使用异常处理替代直接退出
    • 提供了错误恢复机制
  3. 性能优化
    • 使用更高效的数据结构
    • 优化了文件操作
    • 减少了重复计算
  4. 代码维护
    • 添加了详细的文档
    • 统一了代码风格
    • 提高了代码可读性

后续优化建议

  1. 测试覆盖
    • 添加单元测试
    • 添加集成测试
    • 添加性能测试
  2. 文档完善
    • 添加API文档
    • 添加使用示例
    • 添加性能基准
  3. 功能增强
    • 添加更多配置选项
    • 优化性能瓶颈
    • 增加新特性
  4. 代码质量
    • 添加代码检查
    • 添加类型检查
    • 优化代码结构

Python 2 到 Python 3 的升级说明

1. 字符串处理

旧代码 (Python 2):

# 字符串格式化
name = "RNA"
print "Processing %s..." % name
print "File %s not found" % filename

# 字符串编码
text = u"RNA结构"
encoded = text.encode('utf-8')
decoded = encoded.decode('utf-8')

# 字符串比较
if cmp(str1, str2) == 0:
    print "Strings are equal"

新代码 (Python 3):

# 字符串格式化
name = "RNA"
print(f"Processing {name}...")  # f-strings
print("File {} not found".format(filename))  # str.format()

# 字符串编码 (Python 3默认使用Unicode)
text = "RNA结构"  # 默认Unicode
encoded = text.encode('utf-8')
decoded = encoded.decode('utf-8')

# 字符串比较
if str1 == str2:  # 直接比较
    print("Strings are equal")

2. 除法运算

旧代码 (Python 2):

# 整数除法
result = 5 / 2  # 结果为2
# 浮点数除法
result = 5.0 / 2  # 结果为2.5

新代码 (Python 3):

# 整数除法
result = 5 // 2  # 结果为2
# 浮点数除法
result = 5 / 2  # 结果为2.5

3. 输入输出

旧代码 (Python 2):

# 输入
user_input = raw_input("Enter value: ")
# 输出
print "Processing..."  # 语句
print >> sys.stderr, "Error message"  # 重定向输出

新代码 (Python 3):

# 输入
user_input = input("Enter value: ")
# 输出
print("Processing...")  # 函数
print("Error message", file=sys.stderr)  # 重定向输出

4. 迭代器和生成器

旧代码 (Python 2):

# 迭代器
class Iterator:
    def next(self):
        return self.data.next()

# 生成器
def generator():
    yield 1
    yield 2

新代码 (Python 3):

# 迭代器
class Iterator:
    def __next__(self):
        return next(self.data)

# 生成器 (语法基本相同,但行为更一致)
def generator():
    yield 1
    yield 2

5. 异常处理

旧代码 (Python 2):

try:
    result = process_data()
except Exception, e:  # 旧语法
    print "Error:", str(e)

新代码 (Python 3):

try:
    result = process_data()
except Exception as e:  # 新语法
    print("Error:", str(e))

6. 类型提示

旧代码 (Python 2):

def process_data(data):
    # 没有类型提示
    return data

新代码 (Python 3):

from typing import List, Optional

def process_data(data: List[str]) -> Optional[str]:
    # 添加类型提示
    return data[0] if data else None

7. 数据类

旧代码 (Python 2):

class Residue:
    def __init__(self, chain, pos, nt, res):
        self.chain = chain
        self.pos = pos
        self.nt = nt
        self.res = res
        
    def __repr__(self):
        return f"Residue(chain='{self.chain}', pos={self.pos}, nt='{self.nt}')"

新代码 (Python 3):

from dataclasses import dataclass

@dataclass
class Residue:
    chain: str
    pos: int
    nt: str
    res: Any

8. 路径处理

旧代码 (Python 2):

import os

path = os.path.join("data", "input.txt")
if os.path.exists(path):
    with open(path) as f:
        data = f.read()

新代码 (Python 3):

from pathlib import Path

path = Path("data") / "input.txt"
if path.exists():
    data = path.read_text()

9. 字典方法

旧代码 (Python 2):

# 字典视图
keys = d.keys()
values = d.values()
items = d.items()

# 字典更新
d.update(e)  # e是另一个字典

新代码 (Python 3):

# 字典视图 (返回视图对象而不是列表)
keys = d.keys()
values = d.values()
items = d.items()

# 字典更新 (支持更多格式)
d.update(e)  # e可以是字典、键值对列表等

10. 函数注解

旧代码 (Python 2):

def process_data(data):
    # 没有函数注解
    return data

新代码 (Python 3):

def process_data(data: List[str]) -> Optional[str]:
    """处理数据
    
    Args:
        data: 输入数据列表
        
    Returns:
        处理后的数据或None
    """
    return data[0] if data else None

升级注意事项

  1. 字符串处理
    • Python 3 默认使用 Unicode
    • 使用 f-strings 或 str.format() 替代 % 格式化
    • 字符串比较更严格
  2. 除法运算
    • 使用 // 进行整数除法
    • / 运算符总是返回浮点数
  3. 输入输出
    • raw_input() 改为 input()
    • print 成为函数
    • 文件操作默认使用文本模式
  4. 异常处理
    • 使用 as 关键字捕获异常
    • 异常链更清晰
  5. 类型系统
    • 添加类型提示
    • 使用数据类简化类定义
    • 更严格的类型检查
  6. 标准库变化
    • pathlib 替代 os.path
    • 字典视图返回视图对象
    • 更多内置函数
  7. 性能优化
    • 迭代器更高效
    • 字符串处理更快
    • 内存使用更优化