社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

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

数据STUDIO • 6 月前 • 200 次点击  


你是不是也有一个长达千行的 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