Py学习  »  Python

别再用 utils.py 了!这 3 个设计模式让你的 Python 代码更清晰

数据STUDIO • 4 月前 • 135 次点击  


你是不是也有一个长达千行的 utils.py

打开你的项目,搜索一下 utils.py 这个文件。是不是找到了?而且点开一看,里面塞满了各种“可能有用的”函数——日期格式化、字符串处理、数据验证、API封装……

刚开始时,你觉得这很聪明:“看,我多会复用代码!”但几个月后,这个文件变成了没人敢动的“雷区”。新同事看不懂,老同事不敢改,大家都宁愿自己重写一遍,也不愿去那里面翻找“合适”的函数。

今天,我们就来聊聊这个几乎每个Python项目都会掉入的陷阱——滥用工具函数,并分享3个更优雅的替代方案。

为什么 utils.py 会变成代码的“垃圾场”?

我们先看一个典型的场景:

# utils.py (已经长到800行了)
def format_date(date_str):
    """格式化日期"""
    # 一大堆复杂的逻辑...
    pass

def validate_email(email):
    """验证邮箱"""
     # 业务特有的验证规则...
    pass

def calculate_discount(price, user):
    """计算折扣"""
    if user.is_vip:
        return price * 0.8# VIP 8折
    elif user.is_new:
        return price * 0.9# 新用户9折
    return price

def process_order_data(order):
    """处理订单数据"""
    # 混合了数据清洗和业务逻辑...
    pass

看起来很正常,对吧?但这里隐藏着几个致命问题:

问题1:上下文丢失

当你调用 utils.validate_email() 时,你完全不知道这个验证规则是针对用户注册的,还是邮件订阅的,或者是联系表单的。不同的场景可能需要不同的验证规则,但工具函数把它们全混在一起了。

问题2:紧耦合而不自知

calculate_discount 看起来是个“通用”函数,但实际上它硬编码了业务规则(VIP打8折,新用户打9折)。当促销策略变化时,你很难知道哪些地方在用这个函数,改起来战战兢兢。

问题3:变成事实上的“业务逻辑垃圾场”

因为“不知道放哪就放utils吧”,这个文件逐渐积累了大量本应属于具体模块的业务逻辑。结果就是:看似复用,实则混乱

更好的模式1

让行为靠近它所属的领域

核心思想:把函数放到它真正属于的模块里,用清晰的命名告诉读者“我是干什么的”。

重构前

# 糟糕:上下文完全丢失
from utils import format_date, validate_input

# 这两个函数到底在做什么?很难从名字看出来
result1 = format_date(some_date)
result2 = validate_input(some_input)

重构后

# 清晰:每个函数都有明确的归属
from billing.dates import format_invoice_date
from auth.validators import validate_login_credentials

# 现在一眼就知道这些函数是干什么的
invoice_date = format_invoice_date(order_date)
is_valid = validate_login_credentials(username, password)

实际代码示例

# 不要这样做:
# utils/helpers.py
def get_user_stats(user):
    """获取用户统计信息"""
    orders = Order.objects.filter(user=user)
    total_spent = sum(order.amount for order in orders)
    last_order_date = orders.last().date if orders elseNone
    return {
        'order_count': len(orders),
        'total_spent': total_spent,
        'last_order_date': last_order_date
    }

# 应该这样做:
# users/models.py 或 users/services.py
class UserStatsService:
    """专门处理用户统计相关逻辑"""
    
    def __init__(self, user):
        self.user = user
    
    def get_stats(self):
        orders = Order.objects.filter(user=self.user)
        return {
            'order_count': self._count_orders(orders),
            'total_spent': self._calculate_total_spent(orders),
            'last_order_date': self._get_last_order_date(orders)
        }
    
    def _count_orders(self, orders):
        return len(orders)
    
    def _calculate_total_spent(self, orders):
        return sum(order.amount for order in orders)
    
    def _get_last_order_date(self, orders):
        return orders.last().date if orders elseNone

# 使用起来更清晰
from users.services import UserStatsService

user_stats = UserStatsService(current_user).get_stats()

更好的模式2

当数据需要行为时,使用类方法或属性

核心思想:如果某个函数主要是操作某个特定类型的数据,那么它应该属于那个数据类。

重构前

# utils.py
def is_order_refundable(order):
    """检查订单是否可以退款"""
    if order.status != 'completed':
        returnFalse
    if order.created_at < datetime.now() - timedelta(days=30):
        returnFalse
    if order.has_refund_request:
        returnFalse
    returnTrue

# 使用时
from utils import is_order_refundable
if is_order_refundable(order):
    process_refund(order)

重构后

# orders/models.py
class Order:
    def __init__(self, status, created_at, has_refund_request=False):
        self.status = status
        self.created_at = created_at
        self.has_refund_request = has_refund_request
    
    @property
    def is_refundable(self):
        """检查订单是否可以退款"""
        if self.status != 'completed':
            returnFalse
        if self.created_at < datetime.now() - timedelta(days=30):
            returnFalse
        if self.has_refund_request:
            returnFalse
        returnTrue

# 使用时 - 读起来就像英语句子一样自然!
if order.is_refundable:
    order.process_refund()

这个模式的优势

  1. 高内聚:与订单相关的逻辑都放在订单类里
  2. 易发现:新开发者只需要查看  Order 类就能找到相关功能
  3. 易测试:可以单独测试 Order 类的各种行为
  4. 易维护:修改退款规则时,你知道只需要改一个地方

更好的模式3

使用小而专的模块, 而不是大而全的工具箱

核心思想:Python的模块系统非常轻量,不要害怕创建小的、专注的模块。

不推荐的结构

project/
├── utils.py        # 2000行,什么都有
├── helpers.py     # 又开了个新坑...
└── common.py      # 第三个垃圾场

推荐的结构

project/
├── core/
│   ├── validators.py   # 专门做数据验证
│   ├── formatters.py   # 专门做数据格式化
│   └── exceptions.py   # 自定义异常
├── billing/
│   ├── calculator.py   # 计算相关
│   └── formatters.py   # 账单特定格式化
├── users/
│   ├── validators.py   # 用户相关验证
│   └── services.py     # 用户相关服务
└── utils/              # 如果有的话,只放真正通用的
    ├── date_utils.py   # 纯日期工具
    └── string_utils.py # 纯字符串工具

实际示例 - 验证逻辑的分层组织

# 不要把所有验证都塞进一个文件
# utils/validation.py ❌

# 而是按领域和层级分开
# core/validators.py - 真正通用的验证器
def validate_email_format(email: str) -> bool:
    """验证邮箱格式(纯技术验证)"""
    import re
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

# users/validators.py - 用户领域的特定验证
class UserValidator:
    """用户相关的业务验证"""
    
    def __init__(self, user_repository):
        self.user_repository = user_repository
    
    def validate_for_registration(self, email: str, username: str):
        """注册专用的验证"""
        errors = []
        
        # 格式验证(调用通用验证器)
        ifnot validate_email_format(email):
            errors.append("邮箱格式不正确")
        
        # 业务规则验证
         if self.user_repository.email_exists(email):
            errors.append("邮箱已被注册")
        
        if len(username) 3:
            errors.append("用户名至少3个字符")
        
        return errors

# orders/validators.py - 订单领域的特定验证
class OrderValidator:
    """订单相关的业务验证"""
    
    def validate_for_checkout(self, order):
        """结账前的验证"""
        errors = []
        
        ifnot order.items:
            errors.append("订单不能为空")
        
        if order.total_amount <= 0:
            errors.append("订单金额必须大于0")
        
        # 更复杂的业务规则...
        if order.customer.has_unpaid_orders():
            errors.append("您有待支付的订单,请先支付")
        
        return errors

那么,什么时候可以用工具函数?

并不是说完全不能用工具函数,真正通用的、无状态的、与业务无关的函数还是可以放在工具模块里的:

# utils/date_utils.py - 这个可以存在
def days_between(date1, date2):
    """计算两个日期之间的天数差(纯函数)"""
    return abs((date2 - date1).days)

def format_duration(seconds):
    """格式化时间间隔(纯函数)"""
    hours = seconds // 3600
    minutes = (seconds % 3600) // 60
    returnf"{hours}{minutes}m"

# utils/string_utils.py - 这个也可以
def truncate(text, length, suffix="..."):
    """截断字符串(纯函数)"""
    if len(text) <= length:
        return text
     return text[:length - len(suffix)] + suffix

判断标准:如果把这个函数拿到另一个完全不同的项目里,它还能正常工作,那它可能真的是个工具函数。

一个简单的决策流程图

下次当你想要写工具函数时,试试这个决策流程:

开始
  ↓
这个函数主要是操作某个特定对象吗?
  ↓
是 → 做成对象的方法或属性(模式2)

  ↓
这个函数属于某个特定的业务领域吗?
  ↓
是 → 放到对应领域的模块中(模式1)

  ↓
这个函数真的完全通用、无状态、无业务逻辑吗?
  ↓
是 → 放到小而专的工具模块中(模式3)

  ↓
重新思考:这个函数真的需要存在吗?

写在最后

工具函数就像是编程中的“速食面”——快速、方便,但长期吃下去会营养不良。它们让今天的代码写得快一点,却让明天的维护难很多。

好的代码设计就像好的城市规划:相关的东西放在一起,每条路都有明确的目的地,每个建筑都有清晰的用途utils.py 之所以会变成垃圾场,就是因为我们用它来回避那个更难的问题:“这个逻辑到底属于哪里?”

记住这三个原则:

  1. 让行为靠近它所属的数据
  2. 用清晰的模块结构代替模糊的工具箱
  3. 真正通用的才叫工具,业务相关的都有归属

从今天开始,打开你的项目,找出那个臃肿的 utils.py,开始给它“瘦身”吧。你会惊讶地发现,代码的可读性和可维护性能提升这么多。

你在项目中还遇到过哪些代码组织的“陷阱”?或者你有什么更好的代码组织经验?欢迎在评论区分享交流!


🏴‍☠️宝藏级🏴‍☠️ 原创公众号『数据STUDIO』内容超级硬核。公众号以Python为核心语言,垂直于数据科学领域,包括可戳👉 PythonMySQL 数据分析数据可视化机器学习与数据挖掘爬虫 等,从入门到进阶!

长按👇关注- 数据STUDIO -设为星标,干货速递

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/193269