【设计模式】Python模板方法模式:从入门到实战

Python模板方法模式:从入门到实战

前言

上一篇聊了策略模式,核心是"算法可以换着用"。这篇来讲模板方法模式,它俩经常被放在一起比较,因为都是处理"多种实现方式"的问题,但思路完全不同。

模板方法模式的核心思想是:算法的骨架是固定的,但某些步骤可以让子类自己去实现。打个比方,泡茶和泡咖啡的流程差不多------烧水、冲泡、倒进杯子、加调料,但具体冲泡什么、加什么调料是不一样的。模板方法就是把这个"流程"固定下来,具体细节让子类去填。

说实话,这个模式在 Python 里用得比 Java 少一些,因为 Python 有更灵活的方式来实现类似效果。但理解它还是很有必要的,毕竟很多框架源码里都能看到它的影子。

🏠个人主页:你的主页


文章目录


一、模板方法模式概念

模板方法模式是一种行为设计模式,说白了就是:父类定义做事的步骤,子类负责填充具体内容

还是用泡饮料的例子,不管你泡茶还是泡咖啡,步骤都差不多:

复制代码
1. 把水烧开
2. 用热水冲泡(茶叶 or 咖啡粉)
3. 把饮料倒进杯子
4. 加点调料(柠檬 or 糖和牛奶)

这个流程是固定的,但第2步和第4步的具体内容不一样。模板方法模式就是把这个流程定义在父类里,然后让子类去实现那些"不一样"的部分。

模板方法模式通常包含这几个角色:

抽象类 :定义算法骨架,包含模板方法和一些抽象方法
模板方法 :定义算法的步骤顺序,通常是 final 的(Python 里没有 final,靠约定)
抽象方法 :由子类实现的具体步骤
钩子方法:可选的,子类可以选择性重写,通常有默认实现

钩子方法这个概念挺有意思的,它给子类提供了一个"插手"的机会,但不是强制的。比如"要不要加调料"这个决定,可以做成钩子方法让子类自己决定。


二、不使用模板方法模式的痛点

假设我们要做一个数据导出功能,支持导出 CSV、JSON、Excel 格式。不用模板方法的话,可能会写成这样:

python 复制代码
class CSVExporter:
    def export(self, data, filename):
        # 1. 验证数据
        if not data:
            raise ValueError("数据不能为空")
        print("验证数据通过")
      
        # 2. 预处理数据
        processed = []
        for item in data:
            processed.append({k: str(v) for k, v in item.items()})
        print("数据预处理完成")
      
        # 3. 转换格式(CSV特有逻辑)
        import csv
        with open(f"{filename}.csv", 'w', newline='', encoding='utf-8') as f:
            if processed:
                writer = csv.DictWriter(f, fieldnames=processed[0].keys())
                writer.writeheader()
                writer.writerows(processed)
        print("CSV文件生成完成")
      
        # 4. 后处理
        print(f"导出完成:{filename}.csv")
        return f"{filename}.csv"


class JSONExporter:
    def export(self, data, filename):
        # 1. 验证数据(重复代码)
        if not data:
            raise ValueError("数据不能为空")
        print("验证数据通过")
      
        # 2. 预处理数据(重复代码)
        processed = []
        for item in data:
            processed.append({k: str(v) for k, v in item.items()})
        print("数据预处理完成")
      
        # 3. 转换格式(JSON特有逻辑)
        import json
        with open(f"{filename}.json", 'w', encoding='utf-8') as f:
            json.dump(processed, f, ensure_ascii=False, indent=2)
        print("JSON文件生成完成")
      
        # 4. 后处理(重复代码)
        print(f"导出完成:{filename}.json")
        return f"{filename}.json"


class ExcelExporter:
    def export(self, data, filename):
        # 又是一堆重复的验证、预处理代码...
        # 只有转换格式那部分不一样
        pass

问题很明显:

大量重复代码。验证数据、预处理、后处理的逻辑几乎一模一样,复制粘贴了好几遍。哪天要改验证逻辑,得改好几个地方。

流程不统一。万一某个开发者写 XMLExporter 的时候忘了验证数据呢?或者打乱了步骤顺序?没有一个强制的流程约束。

维护成本高。假设要加一个"记录日志"的步骤,你得改所有 Exporter 类。

这种情况就很适合用模板方法模式来重构。


三、Python实现模板方法模式

3.1 经典的抽象基类实现

最标准的实现方式,用 ABC 定义抽象类:

python 复制代码
from abc import ABC, abstractmethod
from typing import List, Dict, Any


class DataExporter(ABC):
    """数据导出器的抽象基类"""
  
    def export(self, data: List[Dict], filename: str) -> str:
        """
        模板方法:定义导出流程
        这个方法定义了算法骨架,子类不应该重写它
        """
        # 步骤1:验证
        self._validate(data)
      
        # 步骤2:预处理
        processed_data = self._preprocess(data)
      
        # 步骤3:钩子 - 导出前的准备(可选)
        if self._before_export():
            print("执行导出前的准备工作...")
      
        # 步骤4:实际导出(子类必须实现)
        result = self._do_export(processed_data, filename)
      
        # 步骤5:后处理
        self._postprocess(result)
      
        return result
  
    def _validate(self, data: List[Dict]) -> None:
        """验证数据 - 通用逻辑,子类一般不用重写"""
        if not data:
            raise ValueError("导出数据不能为空")
        if not isinstance(data, list):
            raise TypeError("数据必须是列表类型")
        print(f"✓ 数据验证通过,共 {len(data)} 条记录")
  
    def _preprocess(self, data: List[Dict]) -> List[Dict]:
        """预处理数据 - 通用逻辑,子类可以重写"""
        # 默认实现:把所有值转成字符串
        processed = []
        for item in data:
            processed.append({k: str(v) if v is not None else '' for k, v in item.items()})
        print("✓ 数据预处理完成")
        return processed
  
    def _before_export(self) -> bool:
        """
        钩子方法:导出前的准备
        返回 True 表示需要执行准备工作,默认不需要
        子类可以重写这个方法
        """
        return False
  
    @abstractmethod
    def _do_export(self, data: List[Dict], filename: str) -> str:
        """
        抽象方法:实际的导出逻辑
        这是每种格式不一样的地方,子类必须实现
        """
        pass
  
    def _postprocess(self, filepath: str) -> None:
        """后处理 - 通用逻辑"""
        print(f"✓ 导出完成:{filepath}")


class CSVExporter(DataExporter):
    """CSV导出器"""
  
    def _do_export(self, data: List[Dict], filename: str) -> str:
        import csv
        filepath = f"{filename}.csv"
      
        with open(filepath, 'w', newline='', encoding='utf-8') as f:
            if data:
                writer = csv.DictWriter(f, fieldnames=data[0].keys())
                writer.writeheader()
                writer.writerows(data)
      
        print(f"  → CSV格式转换完成")
        return filepath


class JSONExporter(DataExporter):
    """JSON导出器"""
  
    def __init__(self, pretty: bool = True):
        self.pretty = pretty
  
    def _do_export(self, data: List[Dict], filename: str) -> str:
        import json
        filepath = f"{filename}.json"
      
        indent = 2 if self.pretty else None
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=indent)
      
        print(f"  → JSON格式转换完成")
        return filepath


class ExcelExporter(DataExporter):
    """Excel导出器"""
  
    def _before_export(self) -> bool:
        """Excel导出前检查 openpyxl 是否安装"""
        try:
            import openpyxl
            return True
        except ImportError:
            print("  ! 警告:openpyxl 未安装,将使用 csv 作为备选")
            return False
  
    def _do_export(self, data: List[Dict], filename: str) -> str:
        try:
            import openpyxl
            from openpyxl import Workbook
          
            filepath = f"{filename}.xlsx"
            wb = Workbook()
            ws = wb.active
          
            # 写表头
            if data:
                headers = list(data[0].keys())
                ws.append(headers)
                # 写数据
                for item in data:
                    ws.append([item.get(h, '') for h in headers])
          
            wb.save(filepath)
            print(f"  → Excel格式转换完成")
            return filepath
          
        except ImportError:
            # 降级为CSV
            return CSVExporter()._do_export(data, filename)

使用起来很简单:

python 复制代码
# 测试数据
data = [
    {"name": "张三", "age": 25, "city": "北京"},
    {"name": "李四", "age": 30, "city": "上海"},
    {"name": "王五", "age": 28, "city": "广州"},
]

# 用不同的导出器,流程完全一样
csv_exporter = CSVExporter()
csv_exporter.export(data, "users")

json_exporter = JSONExporter(pretty=True)
json_exporter.export(data, "users")

excel_exporter = ExcelExporter()
excel_exporter.export(data, "users")

输出类似这样:

复制代码
✓ 数据验证通过,共 3 条记录
✓ 数据预处理完成
  → CSV格式转换完成
✓ 导出完成:users.csv

这样一来,流程被固定住了,想加新的导出格式只需要继承 DataExporter 并实现 _do_export 方法就行。

3.2 使用 __init_subclass__ 的现代写法

Python 3.6+ 提供了一个挺酷的特性,可以在子类创建时做一些检查:

python 复制代码
class DataProcessor:
    """使用 __init_subclass__ 的模板类"""
  
    required_methods = ['process_item']
  
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        # 检查子类是否实现了必要的方法
        for method in cls.required_methods:
            if not callable(getattr(cls, method, None)):
                raise TypeError(f"子类 {cls.__name__} 必须实现 {method} 方法")
  
    def process(self, items: list) -> list:
        """模板方法"""
        results = []
      
        for item in items:
            # 前置处理
            prepared = self.prepare_item(item)
            # 核心处理(子类实现)
            processed = self.process_item(prepared)
            # 后置处理
            final = self.finalize_item(processed)
            results.append(final)
      
        return results
  
    def prepare_item(self, item):
        """默认的前置处理"""
        return item
  
    def process_item(self, item):
        """子类必须实现"""
        raise NotImplementedError
  
    def finalize_item(self, item):
        """默认的后置处理"""
        return item


# 正确的子类
class UpperCaseProcessor(DataProcessor):
    def process_item(self, item):
        return item.upper() if isinstance(item, str) else str(item).upper()


# 这个会报错,因为没实现 process_item
# class BrokenProcessor(DataProcessor):
#     pass  # TypeError: 子类 BrokenProcessor 必须实现 process_item 方法

这种方式比 ABC 的好处是:错误在类定义时就能发现,而不是等到调用时才报错。

3.3 函数式的替代方案

有时候觉得为了一个流程专门搞继承体系太重了,可以用高阶函数来实现类似效果:

python 复制代码
from typing import Callable, List, Any, TypeVar

T = TypeVar('T')


def create_pipeline(
    validate: Callable[[List[T]], None] = None,
    preprocess: Callable[[List[T]], List[T]] = None,
    process: Callable[[List[T]], Any] = None,
    postprocess: Callable[[Any], Any] = None
) -> Callable[[List[T]], Any]:
    """
    创建一个处理管道
    这其实是模板方法的函数式实现
    """
    def default_validate(data):
        if not data:
            raise ValueError("数据不能为空")
  
    def default_preprocess(data):
        return data
  
    def default_postprocess(result):
        return result
  
    # 使用默认值
    _validate = validate or default_validate
    _preprocess = preprocess or default_preprocess
    _postprocess = postprocess or default_postprocess
  
    def pipeline(data: List[T]) -> Any:
        _validate(data)
        processed = _preprocess(data)
      
        if process is None:
            raise ValueError("必须提供 process 函数")
      
        result = process(processed)
        return _postprocess(result)
  
    return pipeline


# 使用
def to_json(data):
    import json
    return json.dumps(data, ensure_ascii=False)

def add_timestamp(result):
    from datetime import datetime
    return {"data": result, "timestamp": datetime.now().isoformat()}

# 创建一个JSON导出管道
json_pipeline = create_pipeline(
    process=to_json,
    postprocess=add_timestamp
)

result = json_pipeline([{"name": "test"}])
print(result)

这种方式更加灵活,适合那种步骤简单、不需要复杂继承关系的场景。


四、模板方法 vs 策略模式

这俩模式经常被拿来比较,因为它们都是处理"变化"的。但思路完全不一样:

4.1 核心区别

方面 模板方法模式 策略模式
关注点 算法的骨架(流程) 算法的整体替换
实现方式 继承 组合
变化的粒度 算法中的某些步骤 整个算法
控制反转 父类控制流程,子类实现细节 客户端选择策略
扩展方式 继承父类 实现策略接口

打个比方:

模板方法像是填空题,框架给你出好了,你只需要填几个空。比如"做菜的步骤是:洗菜→切菜→___→装盘",你只需要填"炒"还是"蒸"还是"煮"。

策略模式像是选择题,有几个完整的选项让你选。比如"去公司的方式:A.开车 B.地铁 C.骑车",每个选项都是完整的方案。

4.2 选择建议

用模板方法的场景:

  • 多个类有相同的算法流程,只是某些步骤不同
  • 需要控制子类的扩展点,不让子类乱来
  • 公共逻辑比较多,私有逻辑比较少

用策略模式的场景:

  • 算法之间差异很大,没什么公共流程
  • 需要在运行时动态切换算法
  • 不想用继承,更喜欢组合

4.3 两者配合使用

很多时候这俩是可以一起用的。比如一个数据处理框架:

python 复制代码
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Callable


# 策略接口:定义不同的处理策略
class ProcessStrategy(ABC):
    @abstractmethod
    def process(self, item: Dict) -> Dict:
        pass


class CleanStrategy(ProcessStrategy):
    """数据清洗策略"""
    def process(self, item: Dict) -> Dict:
        return {k: v.strip() if isinstance(v, str) else v for k, v in item.items()}


class EnrichStrategy(ProcessStrategy):
    """数据增强策略"""
    def process(self, item: Dict) -> Dict:
        item['processed'] = True
        item['source'] = 'pipeline'
        return item


# 模板方法:定义处理流程
class DataPipeline(ABC):
    """
    数据处理管道
    模板方法定义流程,策略模式处理具体逻辑
    """
  
    def __init__(self, strategy: ProcessStrategy = None):
        self.strategy = strategy or CleanStrategy()
  
    def set_strategy(self, strategy: ProcessStrategy):
        """运行时切换策略"""
        self.strategy = strategy
  
    def run(self, data: List[Dict]) -> List[Dict]:
        """模板方法:定义管道流程"""
        # 1. 加载数据(子类实现)
        loaded = self._load(data)
      
        # 2. 验证
        self._validate(loaded)
      
        # 3. 处理每条数据(使用策略)
        processed = []
        for item in loaded:
            result = self.strategy.process(item)
            processed.append(result)
      
        # 4. 保存(子类实现)
        self._save(processed)
      
        return processed
  
    def _validate(self, data: List[Dict]):
        """通用验证逻辑"""
        if not data:
            raise ValueError("数据为空")
        print(f"验证通过,共 {len(data)} 条")
  
    @abstractmethod
    def _load(self, data: List[Dict]) -> List[Dict]:
        """加载数据 - 子类实现"""
        pass
  
    @abstractmethod
    def _save(self, data: List[Dict]) -> None:
        """保存数据 - 子类实现"""
        pass


class FilePipeline(DataPipeline):
    """文件处理管道"""
  
    def __init__(self, output_file: str, strategy: ProcessStrategy = None):
        super().__init__(strategy)
        self.output_file = output_file
  
    def _load(self, data: List[Dict]) -> List[Dict]:
        print("从内存加载数据...")
        return data
  
    def _save(self, data: List[Dict]) -> None:
        import json
        with open(self.output_file, 'w') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
        print(f"数据已保存到 {self.output_file}")


class DatabasePipeline(DataPipeline):
    """数据库处理管道"""
  
    def __init__(self, table_name: str, strategy: ProcessStrategy = None):
        super().__init__(strategy)
        self.table_name = table_name
  
    def _load(self, data: List[Dict]) -> List[Dict]:
        print(f"准备写入表 {self.table_name}...")
        return data
  
    def _save(self, data: List[Dict]) -> None:
        # 模拟数据库操作
        print(f"已将 {len(data)} 条数据写入 {self.table_name}")

使用:

python 复制代码
data = [
    {"name": "  张三  ", "age": 25},
    {"name": "李四", "age": 30},
]

# 文件管道 + 清洗策略
file_pipeline = FilePipeline("output.json", strategy=CleanStrategy())
file_pipeline.run(data)

# 同一个管道,换个策略
file_pipeline.set_strategy(EnrichStrategy())
file_pipeline.run(data)

# 数据库管道
db_pipeline = DatabasePipeline("users", strategy=EnrichStrategy())
db_pipeline.run(data)

这样,模板方法 控制了"加载→验证→处理→保存"的流程,策略模式控制了"处理"这一步的具体逻辑。两者各司其职,代码既有章法又灵活。


五、实战:数据处理管道

来个更完整的例子,做一个通用的数据爬取和处理框架。这种场景特别适合模板方法,因为爬虫的流程基本是固定的。

5.1 定义爬虫基类

python 复制代码
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field
from datetime import datetime
import time
import random


@dataclass
class CrawlResult:
    """爬取结果"""
    success: bool
    data: List[Dict[str, Any]] = field(default_factory=list)
    error: Optional[str] = None
    stats: Dict[str, Any] = field(default_factory=dict)


class BaseCrawler(ABC):
    """
    爬虫基类 - 模板方法模式
    定义了爬取流程:初始化 → 获取URL列表 → 爬取 → 解析 → 存储 → 清理
    """
  
    def __init__(self, name: str = "BaseCrawler"):
        self.name = name
        self.stats = {
            "start_time": None,
            "end_time": None,
            "total_urls": 0,
            "success_count": 0,
            "fail_count": 0,
        }
  
    def crawl(self) -> CrawlResult:
        """
        模板方法:定义爬虫流程
        子类不应该重写这个方法
        """
        all_data = []
      
        try:
            # 1. 初始化
            print(f"\n{'='*50}")
            print(f"🚀 爬虫 [{self.name}] 开始运行")
            print(f"{'='*50}")
          
            self.stats["start_time"] = datetime.now()
            self._initialize()
          
            # 2. 获取要爬取的URL列表
            urls = self._get_urls()
            self.stats["total_urls"] = len(urls)
            print(f"📋 获取到 {len(urls)} 个URL")
          
            # 3. 逐个爬取和解析
            for i, url in enumerate(urls, 1):
                print(f"\n[{i}/{len(urls)}] 正在处理: {url}")
              
                try:
                    # 请求前的钩子
                    if self._before_request(url):
                        # 发起请求
                        response = self._fetch(url)
                      
                        # 解析数据
                        data = self._parse(response)
                      
                        if data:
                            all_data.extend(data if isinstance(data, list) else [data])
                            self.stats["success_count"] += 1
                            print(f"  ✓ 解析成功,获取 {len(data) if isinstance(data, list) else 1} 条数据")
                      
                        # 请求后的钩子
                        self._after_request(url, response)
                  
                    # 礼貌性延迟
                    self._delay()
                  
                except Exception as e:
                    self.stats["fail_count"] += 1
                    print(f"  ✗ 处理失败: {e}")
                    self._on_error(url, e)
          
            # 4. 存储数据
            if all_data:
                self._save(all_data)
          
            # 5. 清理工作
            self._cleanup()
          
            self.stats["end_time"] = datetime.now()
            duration = (self.stats["end_time"] - self.stats["start_time"]).total_seconds()
          
            print(f"\n{'='*50}")
            print(f"✅ 爬虫完成!耗时 {duration:.2f} 秒")
            print(f"   成功: {self.stats['success_count']}, 失败: {self.stats['fail_count']}")
            print(f"   共获取 {len(all_data)} 条数据")
            print(f"{'='*50}\n")
          
            return CrawlResult(
                success=True,
                data=all_data,
                stats=self.stats.copy()
            )
          
        except Exception as e:
            return CrawlResult(
                success=False,
                error=str(e),
                stats=self.stats.copy()
            )
  
    # ============ 子类必须实现的方法 ============
  
    @abstractmethod
    def _get_urls(self) -> List[str]:
        """获取要爬取的URL列表 - 子类必须实现"""
        pass
  
    @abstractmethod
    def _parse(self, response: Any) -> List[Dict[str, Any]]:
        """解析响应数据 - 子类必须实现"""
        pass
  
    # ============ 可选重写的方法(钩子) ============
  
    def _initialize(self) -> None:
        """初始化 - 可选重写"""
        print("📦 初始化...")
  
    def _fetch(self, url: str) -> Any:
        """
        发起请求 - 默认实现
        子类可以重写以使用不同的请求方式
        """
        # 这里简单模拟,实际项目中用 requests 或 aiohttp
        print(f"  → 请求: {url}")
        time.sleep(0.1)  # 模拟网络延迟
        return {"url": url, "html": f"<html>Mock content for {url}</html>"}
  
    def _save(self, data: List[Dict[str, Any]]) -> None:
        """存储数据 - 默认打印,子类可重写"""
        print(f"\n💾 保存 {len(data)} 条数据...")
        for item in data[:3]:  # 只打印前3条
            print(f"   {item}")
        if len(data) > 3:
            print(f"   ... 还有 {len(data) - 3} 条")
  
    def _cleanup(self) -> None:
        """清理工作 - 可选重写"""
        print("🧹 清理资源...")
  
    def _before_request(self, url: str) -> bool:
        """请求前的钩子,返回False可跳过该URL"""
        return True
  
    def _after_request(self, url: str, response: Any) -> None:
        """请求后的钩子"""
        pass
  
    def _on_error(self, url: str, error: Exception) -> None:
        """错误处理钩子"""
        pass
  
    def _delay(self) -> None:
        """请求间隔 - 默认随机延迟"""
        delay = random.uniform(0.5, 1.5)
        time.sleep(delay)

5.2 实现具体爬虫

python 复制代码
import json
from typing import List, Dict, Any


class NewsCrawler(BaseCrawler):
    """新闻爬虫"""
  
    def __init__(self, category: str = "tech"):
        super().__init__(name=f"NewsCrawler-{category}")
        self.category = category
        self.output_file = f"news_{category}.json"
  
    def _get_urls(self) -> List[str]:
        """生成新闻URL列表"""
        # 实际项目中这里可能是从种子页面解析出来的
        base_url = f"https://news.example.com/{self.category}"
        return [f"{base_url}/page/{i}" for i in range(1, 4)]
  
    def _parse(self, response: Any) -> List[Dict[str, Any]]:
        """解析新闻数据"""
        # 实际项目中这里用 BeautifulSoup 或 lxml 解析
        # 这里模拟解析结果
        url = response.get("url", "")
        return [
            {
                "title": f"新闻标题 - 来自 {url}",
                "category": self.category,
                "publish_time": datetime.now().isoformat(),
                "source_url": url,
            }
        ]
  
    def _save(self, data: List[Dict[str, Any]]) -> None:
        """保存到JSON文件"""
        with open(self.output_file, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"💾 数据已保存到 {self.output_file}")
  
    def _before_request(self, url: str) -> bool:
        """过滤某些URL"""
        # 比如跳过广告页面
        if "ad" in url or "sponsor" in url:
            print(f"  ⏭ 跳过广告页: {url}")
            return False
        return True


class ProductCrawler(BaseCrawler):
    """商品爬虫"""
  
    def __init__(self, keywords: List[str]):
        super().__init__(name="ProductCrawler")
        self.keywords = keywords
        self.products = []
  
    def _initialize(self) -> None:
        """初始化 - 可以在这里登录啥的"""
        print("📦 初始化商品爬虫...")
        print(f"   关键词: {', '.join(self.keywords)}")
        # 实际项目中这里可能要初始化session、登录等
  
    def _get_urls(self) -> List[str]:
        """根据关键词生成搜索URL"""
        urls = []
        for keyword in self.keywords:
            urls.append(f"https://shop.example.com/search?q={keyword}")
        return urls
  
    def _parse(self, response: Any) -> List[Dict[str, Any]]:
        """解析商品列表"""
        # 模拟解析
        return [
            {"name": "商品A", "price": 99.9, "url": response["url"]},
            {"name": "商品B", "price": 199.9, "url": response["url"]},
        ]
  
    def _save(self, data: List[Dict[str, Any]]) -> None:
        """保存到数据库(这里模拟)"""
        print(f"💾 准备写入数据库...")
        for item in data:
            print(f"   INSERT INTO products VALUES ('{item['name']}', {item['price']})")
  
    def _on_error(self, url: str, error: Exception) -> None:
        """错误处理 - 可以发告警"""
        print(f"  ⚠ 错误已记录,稍后重试: {url}")
        # 实际项目中这里可以发邮件、写日志、加入重试队列等

5.3 运行爬虫

python 复制代码
# 运行新闻爬虫
news_crawler = NewsCrawler(category="tech")
result = news_crawler.crawl()

print(f"爬取结果: 成功={result.success}, 数据条数={len(result.data)}")

# 运行商品爬虫
product_crawler = ProductCrawler(keywords=["手机", "耳机", "充电器"])
result = product_crawler.crawl()

这个例子展示了模板方法的威力:流程被固定住了(初始化→获取URL→爬取→解析→保存→清理),但每个爬虫可以有自己的实现。想加新爬虫只需要继承 BaseCrawler,实现 _get_urls_parse 两个方法就行。


六、使用场景与注意事项

6.1 适用场景

模板方法模式特别适合这些情况:

有固定流程但步骤实现不同。比如数据导入导出、文档生成、测试框架的 setup/teardown。

想限制子类的扩展点。只让子类重写特定方法,其他地方不能动。这在框架设计里很常见。

公共代码太多重复了。把公共逻辑放父类,差异化的部分让子类实现。

多个子类有相同的行为序列。比如不同类型的报表生成,都是"收集数据→处理数据→生成图表→导出文件"。

6.2 不适合的场景

当然也有不适合的时候:

如果算法之间差异太大,根本没什么公共流程,那强行抽模板反而别扭。这时候用策略模式更合适。

如果继承层级太深,或者子类太多,维护起来会头疼。Python 社区一般更推荐组合而非继承。

如果步骤需要动态组合,比如今天要A→B→C,明天要B→C→A,模板方法就搞不定了,得用其他模式(比如责任链)。

6.3 Python 特有的注意事项

关于"私有"方法 :Python 没有真正的私有方法,单下划线 _method 只是约定。如果担心子类乱重写,可以用双下划线 __method,但这样也有名称改写(name mangling)的问题,用之前想清楚。

钩子方法要有默认实现:让钩子方法返回合理的默认值,这样子类不重写也能正常工作。

考虑用 __init_subclass__:Python 3.6+ 可以在子类定义时做检查,比等到运行时报错体验好。

能用组合就别用继承:Python 社区有句话叫"扁平胜于嵌套"。如果继承层级超过2-3层,考虑换种实现方式。

6.4 与其他模式的关系

与工厂模式配合:工厂方法其实就是模板方法的一种特例,用来创建对象。

与策略模式互补:模板方法用继承改变部分实现,策略模式用组合改变整体实现。前面已经演示过两者配合使用了。

与钩子概念:模板方法里的钩子方法,本质上是给子类提供的扩展点。很多框架(Django、Flask)的中间件机制都有这个影子。


七、总结

模板方法模式的核心就一句话:父类定流程,子类填细节。这种"控制反转"在框架设计里特别常见,你用过 Django 的类视图、unittest 的 TestCase,其实都在用这个模式。

相比策略模式,模板方法更强调流程的统一性。策略模式是"我有几种方案你选一个",模板方法是"做事步骤就这些,具体怎么做你来定"。两者经常配合使用,模板方法控制大流程,策略模式处理某个步骤的变化,代码写起来会很清爽。

Python 实现模板方法虽然没有 Java 那么"仪式感",但灵活性更高。可以用 ABC 正经做,也可以用函数式的方式替代。关键是理解它的思想------把变化封装在特定的地方,让扩展变得可控。

最后提醒一句,设计模式是工具不是目的。如果你的场景简单,if-else 写几行就完事了,没必要非得套模式。模板方法最适合那种"流程固定、步骤多、变化点明确"的场景,用对地方才能发挥它的价值。


热门专栏推荐

等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持

文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论

希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读

如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力

相关推荐
再__努力1点3 小时前
LBP纹理特征提取:高鲁棒性的纹理特征算法
开发语言·人工智能·python·算法·计算机视觉
梅如你4 小时前
【网盘直享】最新DEM数据分享(全球/全国/分省12.5m/30m/90m/250m/1000m)
图像处理·人工智能·python·计算机视觉
superman超哥4 小时前
仓颉热点代码识别深度解析
开发语言·后端·python·c#·仓颉
摸鱼仙人~4 小时前
如何对量化前后的模型进行评估
python
overmind4 小时前
oeasy玩py111列表_排序_sort_比较大小
python
JH灰色4 小时前
【大模型】-modelscope魔搭
python
STLearner5 小时前
AAAI 2026 | 时空数据(Spatial-temporal)论文总结[上](时空预测,轨迹挖掘,自动驾驶等)
大数据·人工智能·python·深度学习·机器学习·数据挖掘·自动驾驶
知行合一。。。5 小时前
Python--02--流程控制语句
开发语言·python
码农小卡拉5 小时前
Java多线程:CompletableFuture使用详解(超详细)
java·开发语言·spring boot·python·spring·spring cloud