AI辅助代码重构:我把10年老代码从3小时改写到15分钟的血泪史

30秒速览

  • Python 2.7老代码迁移别硬刚,Claude 3能省80%时间
  • 类型提示批量生成记得加numpy特殊处理
  • 函数拆分要先做AST分析再喂给AI
  • 向量化优化可能用3倍内存换15倍速度
  • 所有AI生成代码必须检查# PRESERVE标记

接手一个烂摊子:2013年的Python 2.7代码库还在生产环境跑

上个月接手了公司一个老项目——为连锁超市做的库存预测系统。打开代码库我差点吐血:Python 2.7写的,没有类型提示,函数动不动就300行,还有大量全局变量。最离谱的是有个叫magic_calculate()的函数,注释写着”不要动这里,动了会崩”。

老板给我的任务是两周内完成Python 3迁移和性能优化。我估算了一下:

任务 手工耗时
Python 2→3语法转换 8小时
添加类型提示 20小时
函数拆分重构 30小时+

这还没算测试时间。正当我准备通宵时,突然想起最近在玩的Claude 3 Opus…

Claude 3 + pylint:我的自动化重构流水线

我设计了一个三阶段流水线:

  1. 2to3做基础语法转换
  2. 用Claude批量添加类型提示
  3. 人工复核关键业务逻辑

核心代码如下(真实项目代码脱敏后):

# 批量类型提示生成器
def auto_type_hint(code: str) -> str:
    prompt = f"""请为以下Python代码添加类型提示,保持原有逻辑不变:
    
    {code}
    
    要求:
    1. 使用Python 3.10+语法
    2. 对复杂类型使用typing模块
    3. 对可能为None的值用Optional标注"""
    
    response = claude_client.generate(
        prompt=prompt,
        max_tokens=4000,
        temperature=0.2  # 低随机性保证稳定性
    )
    return response.code

这个方案有个坑:Claude有时会给numpy.ndarray错误地标注为List[float]。我加了后处理检查:

# 类型修正后处理
def fix_type_hints(code: str) -> str:
    patterns = [
        (r"List[float]", "np.ndarray"),  # numpy数组修正
        (r"Dict[str, Any]", "dict[str, Any]")  # 新语法替换
    ]
    for old, new in patterns:
        code = re.sub(old, new, code)
    return code

函数拆分的艺术:AI不是万能的

最头疼的是处理那些超长函数。我试过直接让AI拆,结果生成了一堆无法运行的代码。后来摸索出有效套路:

  1. 先用ast模块解析函数结构
  2. 识别代码块边界(比如连续5行以上操作同一变量)
  3. 让AI针对特定代码块生成独立函数

示例:原函数片段

def process_inventory(items):
    # 200行意大利面条代码...
    for item in items:
        if item['type'] == 'FROZEN':
            item['weight'] *= 1.1
            if item['expire_date'] < today:
                item['status'] = 'DISCARD'
        elif item['type'] == 'FRESH':
            # 更多嵌套判断...

优化后:

def handle_frozen_item(item: InventoryItem) -> None:
    """处理冷冻商品专用逻辑"""
    item.weight *= 1.1
    if item.expire_date < datetime.now().date():
        item.status = InventoryStatus.DISCARD

def process_inventory(items: list[InventoryItem]) -> None:
    """处理库存主流程"""
    for item in items:
        match item.type:
            case ItemType.FROZEN:
                handle_frozen_item(item)
            case ItemType.FRESH:
                handle_fresh_item(item)

这个改造让单元测试覆盖率从35%提升到82%,因为现在可以单独测试每个处理函数了。

性能优化:AI建议的骚操作让我惊掉下巴

原代码有个计算商品相似度的函数,用双重循环实现,3000个商品要跑12秒。我让Claude优化,它给出了一个我完全没想到的方案:

# 优化前
def calculate_similarity(items):
    scores = []
    for i in range(len(items)):
        for j in range(i+1, len(items)):
            sim = some_heavy_calculation(items[i], items[j])
            scores.append((i, j, sim))
    return scores

# 优化后(使用numpy向量化)
def calculate_similarity(items: np.ndarray) -> np.ndarray:
    """向量化相似度计算"""
    # 利用广播机制一次性计算所有组合
    diff = items[:, None] - items[None, :]  # 形状(n,n,feature_dim)
    distances = np.linalg.norm(diff, axis=2)
    return 1 / (1 + distances)

这个改动把运行时间从12秒降到了0.8秒,但内存占用涨了3倍。在超市场景下完全可以接受,因为服务器有128G内存。

那些让我熬夜的坑

不是所有AI建议都能直接用。最坑的一次是AI把这段代码:

try:
    import simplejson as json
except ImportError:
    import json

“优化”成了:

import json

结果上线后因为性能问题直接崩了——原来老代码特意用simplejson处理大文件。这个教训让我在所有AI生成代码里都加了# PRESERVE标记:

try:
    import simplejson as json  # PRESERVE: 性能关键
except ImportError:
    import json

现在我的重构流程强制包含以下检查项:

  • 所有# PRESERVE注释是否被保留
  • 第三方库导入是否一致
  • 魔术数字是否被正确替换为常量

成果:从地狱到天堂的蜕变

最终数据对比:

指标 重构前 重构后
代码行数 8,742 6,215
平均函数长度 48行 16行
类型覆盖率 0% 92%
测试覆盖率 35% 82%
冷启动时间 3.2秒 1.5秒

最让我得意的是,原本预估60小时的工作,实际只用了15小时(其中AI辅助12小时,人工复核3小时)。现在这个系统每天处理300+门店的库存预测,再没出现过半夜告警。

重构第一战:解剖”魔法函数”

那个被注释警告的magic_calculate()函数简直是个黑洞——287行代码里嵌套了11层if-else,还混着正则表达式和数据库查询。当我第一次用PyCharm的Control Flow分析工具可视化时,出来的图活像一团意大利面。

最致命的发现是在第143行:

# 历史遗留魔法数字(供应商API限制)
if len(result) > 73: 
    result = result[:73] + chr(ord(result[-1]) + 1)

这个诡异的73字符限制后来在Slack上问老员工才知道,2015年合作方API有缓存bug,他们的临时解决方案居然是手动修改最后一个字符的ASCII码!而现在的API文档里明确写着:”请求体长度限制:8192 bytes”。

AI辅助拆解过程

  1. Git blame定位到2016年的提交记录,发现这个函数最初只有40行
  2. 让Copilot生成函数职责分析报告,发现实际包含:
    • 数据清洗(带5种异常处理)
    • 第三方API调用
    • 本地缓存管理
    • 业务逻辑校验
  3. Bard建议的SOLID原则拆分方案,最终拆解成:
    class InventoryCalculator:
        def __init__(self, api_client):
            self.api_client = api_client
            
        def normalize_data(self, raw_data: dict) -> list: ...
        
        def call_vendor_api(self, clean_data: list) -> dict: ...
        
        def apply_business_rules(self, api_response: dict) -> dict: ...

类型提示的蝴蝶效应

当我给第一个函数加上-> dict[str, list[float]]返回值类型提示时,PyCharm立即在调用处标出17个红色波浪线。原来这个函数在不同场景下会返回:

返回类型 调用模块 潜在风险
dict 报表生成 缺少key时崩溃
list 数据看板 长度不一致导致UI错位
None 批处理任务 静默失败

这时我才理解为什么代码里到处都是if isinstance(result, dict) and 'items' in result这样的防御性编程。通过mypy--strict模式,我们发现了整个系统最核心的数据类型边界模糊问题。

联合类型解决方案

最终采用typing.Union配合@overload装饰器:

from typing import overload, Union

@overload
def process_inventory(store_id: int) -> dict[str, list[float]]: ...
    
@overload 
def process_inventory(store_id: str) -> list[float]: ...

def process_inventory(store_id: Union[int, str]) -> Union[dict[str, list[float]], list[float]]:
    # 实际实现
    ...

这个改动导致42个测试用例需要更新,但意外发现了3个隐藏的边界条件bug——其中一个是当门店ID带字母后缀时,会错误地把月销量乘以12。

性能优化的认知颠覆

原以为把for循环改成列表推导式就能提升性能,直到用py-spy做了火焰图分析:

优化前: 97%时间消耗在validate_stock()

真相: 这个函数里调用的datetime.strptime()占用了86%的CPU时间

根本原因: 每次校验都重复解析相同的日期字符串(”YYYY-MM-DD”格式)

最终的解决方案既不是异步也不是缓存,而是简单的日期格式预处理

# 优化前(单次执行0.8ms)
def validate_stock(record):
    expiry = datetime.strptime(record['expiry'], '%Y-%m-%d')
    return expiry > datetime.now()

# 优化后(提速240倍)
DATE_CACHE = {}

def preprocess_dates(data):
    for record in data:
        if record['expiry'] not in DATE_CACHE:
            DATE_CACHE[record['expiry']] = datetime.strptime(record['expiry'], '%Y-%m-%d')

def validate_stock(record):
    return DATE_CACHE[record['expiry']] > datetime.now()

这个改动让批量处理的吞吐量从1200条/分钟提升到28万条/分钟,而代码量反而减少了——因为移除了大量重复的日期解析逻辑。

测试覆盖的黑暗森林

当我看到test_main.py里写着”跳过:太慢”的@unittest.skip装饰器时,就知道要出大事。这个被跳过的测试用例涉及:

  • 多线程环境下修改全局配置
  • 临时文件清理
  • MySQL事务回滚

pytest-xdist并行运行测试时,出现了经典的时间旅行问题:

测试A在11:00:00修改了config.TIMEOUT = 10
测试B在11:00:01读取到修改后的值
但理论上测试B应该看到默认值30

最终我们用pytest.fixture重构了所有测试固件:

@pytest.fixture(autouse=True)
def reset_globals():
    original = config.TIMEOUT
    yield
    config.TIMEOUT = original

这个过程中AI工具帮了大忙——ChatGPT通过分析测试失败时序图,准确指出了3处需要加threading.Lock的资源竞争点。

那些年我们踩过的全局变量坑

当我第一次看到代码里密密麻麻的global关键字时,血压直接飙升到180。这个库存预测系统里竟然有47个全局变量,最夸张的是有个叫current_magic_number的变量,在8个不同文件中被修改。你能想象半夜收到报警,发现是澳洲分店的库存同步把这个变量改成了None,导致亚洲区预测全部崩掉的绝望吗?

重构时我发明了一套”全局变量剿灭三步法”:

  1. 先用grep -r "global" .地毯式搜索
  2. 给每个全局变量建立迁移档案,记录所有读写点
  3. 用类封装成单例模式,比如把global config改成ConfigManager.instance()
# 改造前
global cache
cache = {}

def update_item(item_id):
    global cache
    cache[item_id] = datetime.now()

# 改造后
class CacheManager:
    _instance = None
    
    @classmethod
    def instance(cls):
        if not cls._instance:
            cls._instance = cls()
        return cls._instance
    
    def __init__(self):
        self._cache = {}
    
    def update_item(self, item_id):
        self._cache[item_id] = datetime.now()

魔法函数现形记

那个著名的magic_calculate()函数简直就是个黑洞——传参是3个布尔值和2个整数,返回值时而字典时而列表,中间还夹杂着sys._getframe()这样的黑魔法。为了搞懂它的逻辑,我不得不动用终极武器:

  • pdb下断点跟踪了3小时
  • 画出了27种参数组合的流程图
  • 发现它其实是在模拟Excel的某类特殊计算

最终拆解出来的核心算法让我哭笑不得:

# 原始魔法函数的核心逻辑
def _real_calculation(base, modifier):
    return (base * 1.8 + 32) if modifier % 2 else (base / 0.45)

原来是在华氏度和公斤/磅之间做单位转换!只是被层层包装后完全看不出原貌。我把它重构成:

class UnitConverter:
    @staticmethod
    def fahrenheit_from_celsius(c):
        return c * 1.8 + 32
    
    @staticmethod 
    def pounds_from_kg(kg):
        return kg / 0.45

类型提示带来的惊喜

当我给那个300行的calculate_inventory()函数加上类型提示时,PyCharm直接报出17个潜在错误。最精彩的是发现了一个隐藏5年的边界条件bug:

# 原代码
def adjust_stock(items):  # items是什么结构?只有上帝知道
    for item in items:
        # ... 50行后
        warehouse = item[3][2]  # 猜猜这个魔法数字是什么?
        
# 重构后
from typing import TypedDict

class Warehouse(TypedDict):
    id: str
    location: tuple[float, float]

class InventoryItem(TypedDict):
    sku: str
    quantity: int
    warehouses: list[Warehouse]

def adjust_stock(items: list[InventoryItem]) -> None:
    for item in items:
        # 现在IDE能自动补全warehouse.location了!

更绝的是,当我用mypy做静态检查时,发现有个函数返回Union[Dict, str],而调用方永远只处理字典情况。原来字符串返回值是给某个早已下线的移动端用的——这种技术债就该果断砍掉。

发表评论