本章示範如何透過繼承抽象基類來新增語言解析器。這是一個完整的實作範例,展示了前面學到的 ABC、工廠模式和型別提示等概念。

前置知識

建議先閱讀:

場景說明

假設 Hook 系統需要支援新的配置格式(例如 TOML),我們需要:

  1. 建立繼承自 BaseParserTomlParser 類別
  2. 實作所有抽象方法
  3. 註冊到工廠
  4. 撰寫測試

步驟 1:了解基類介面

首先檢視抽象基類的定義:

 1# parsers/base.py
 2from abc import ABC, abstractmethod
 3from typing import Optional
 4
 5class BaseParser(ABC):
 6    """解析器抽象基類"""
 7
 8    def __init__(self, encoding: str = "utf-8"):
 9        self.encoding = encoding
10
11    @abstractmethod
12    def parse(self, content: str) -> dict:
13        """
14        解析內容
15
16        Args:
17            content: 要解析的字串
18
19        Returns:
20            dict: 解析後的字典
21
22        Raises:
23            ParseError: 解析失敗時拋出
24        """
25        pass
26
27    @abstractmethod
28    def validate(self, content: str) -> bool:
29        """
30        驗證內容格式是否正確
31
32        Args:
33            content: 要驗證的字串
34
35        Returns:
36            bool: 格式正確返回 True
37        """
38        pass
39
40    @property
41    @abstractmethod
42    def file_extensions(self) -> list[str]:
43        """支援的檔案副檔名列表"""
44        pass
45
46    # 共用方法(不是抽象的)
47    def parse_file(self, path: str) -> dict:
48        """解析檔案"""
49        from pathlib import Path
50        content = Path(path).read_text(encoding=self.encoding)
51        return self.parse(content)

步驟 2:實作新解析器

 1# parsers/toml_parser.py
 2"""
 3TOML 解析器
 4
 5支援 TOML 格式的配置檔案解析。
 6"""
 7
 8from typing import Optional
 9from .base import BaseParser
10
11# 嘗試導入 toml 模組
12try:
13    import tomllib  # Python 3.11+ 內建
14except ImportError:
15    try:
16        import toml as tomllib  # 第三方套件
17    except ImportError:
18        tomllib = None
19
20class TomlParser(BaseParser):
21    """
22    TOML 格式解析器
23
24    支援 .toml 檔案的解析。
25
26    Example:
27        parser = TomlParser()
28        config = parser.parse_file("config.toml")
29    """
30
31    def __init__(self, encoding: str = "utf-8"):
32        """
33        初始化 TOML 解析器
34
35        Args:
36            encoding: 檔案編碼
37
38        Raises:
39            ImportError: 如果 toml 模組不可用
40        """
41        if tomllib is None:
42            raise ImportError(
43                "TOML parser requires 'tomllib' (Python 3.11+) "
44                "or 'toml' package. Install with: pip install toml"
45            )
46        super().__init__(encoding)
47
48    def parse(self, content: str) -> dict:
49        """
50        解析 TOML 內容
51
52        Args:
53            content: TOML 格式的字串
54
55        Returns:
56            dict: 解析後的字典
57
58        Raises:
59            ValueError: 如果 TOML 格式錯誤
60        """
61        try:
62            # Python 3.11+ 的 tomllib 需要 bytes
63            if hasattr(tomllib, 'loads'):
64                return tomllib.loads(content)
65            else:
66                # tomllib (3.11+) 只接受 bytes
67                return tomllib.load(content.encode(self.encoding))
68        except Exception as e:
69            raise ValueError(f"Failed to parse TOML: {e}") from e
70
71    def validate(self, content: str) -> bool:
72        """
73        驗證 TOML 格式
74
75        Args:
76            content: 要驗證的字串
77
78        Returns:
79            bool: 格式正確返回 True
80        """
81        try:
82            self.parse(content)
83            return True
84        except ValueError:
85            return False
86
87    @property
88    def file_extensions(self) -> list[str]:
89        """支援的副檔名"""
90        return [".toml"]

步驟 3:註冊到工廠

 1# parsers/__init__.py
 2"""
 3解析器模組
 4
 5提供多種格式的檔案解析功能。
 6"""
 7
 8from .base import BaseParser
 9from .factory import ParserFactory
10from .json_parser import JsonParser
11from .yaml_parser import YamlParser
12
13# 註冊內建解析器
14ParserFactory.register("json", [".json"])(JsonParser)
15ParserFactory.register("yaml", [".yaml", ".yml"])(YamlParser)
16
17# 嘗試註冊 TOML 解析器(可選依賴)
18try:
19    from .toml_parser import TomlParser
20    ParserFactory.register("toml", [".toml"])(TomlParser)
21except ImportError:
22    pass  # TOML 支援不可用
23
24__all__ = [
25    "BaseParser",
26    "ParserFactory",
27    "JsonParser",
28    "YamlParser",
29]

或者使用裝飾器直接在類別定義時註冊:

1# parsers/toml_parser.py
2from .factory import ParserFactory
3from .base import BaseParser
4
5@ParserFactory.register("toml", [".toml"])
6class TomlParser(BaseParser):
7    """TOML 解析器"""
8    # ... 實作 ...

步驟 4:撰寫測試

 1# tests/test_toml_parser.py
 2"""
 3TOML 解析器測試
 4"""
 5
 6import unittest
 7from unittest.mock import patch
 8
 9# 跳過測試如果 toml 不可用
10try:
11    from parsers.toml_parser import TomlParser
12    HAS_TOML = True
13except ImportError:
14    HAS_TOML = False
15
16@unittest.skipUnless(HAS_TOML, "TOML support not available")
17class TestTomlParser(unittest.TestCase):
18    """測試 TomlParser 類別"""
19
20    def setUp(self):
21        """測試前準備"""
22        self.parser = TomlParser()
23
24    def test_parse_simple_toml(self):
25        """測試解析簡單的 TOML"""
26        content = """
27        [server]
28        host = "localhost"
29        port = 8080
30        """
31        result = self.parser.parse(content)
32
33        self.assertEqual(result["server"]["host"], "localhost")
34        self.assertEqual(result["server"]["port"], 8080)
35
36    def test_parse_nested_toml(self):
37        """測試解析巢狀的 TOML"""
38        content = """
39        [database]
40        host = "localhost"
41
42        [database.connection]
43        timeout = 30
44        retries = 3
45        """
46        result = self.parser.parse(content)
47
48        self.assertEqual(result["database"]["host"], "localhost")
49        self.assertEqual(result["database"]["connection"]["timeout"], 30)
50
51    def test_validate_valid_toml(self):
52        """測試驗證有效的 TOML"""
53        content = '[section]\nkey = "value"'
54        self.assertTrue(self.parser.validate(content))
55
56    def test_validate_invalid_toml(self):
57        """測試驗證無效的 TOML"""
58        content = "this is not valid TOML ["
59        self.assertFalse(self.parser.validate(content))
60
61    def test_parse_invalid_raises_error(self):
62        """測試解析無效 TOML 時拋出錯誤"""
63        content = "invalid [ toml"
64        with self.assertRaises(ValueError):
65            self.parser.parse(content)
66
67    def test_file_extensions(self):
68        """測試檔案副檔名"""
69        self.assertIn(".toml", self.parser.file_extensions)
70
71@unittest.skipUnless(HAS_TOML, "TOML support not available")
72class TestTomlParserFactory(unittest.TestCase):
73    """測試 TOML 解析器與工廠的整合"""
74
75    def test_factory_creates_toml_parser(self):
76        """測試工廠可以建立 TOML 解析器"""
77        from parsers import ParserFactory
78
79        parser = ParserFactory.create("toml")
80        self.assertIsInstance(parser, TomlParser)
81
82    def test_factory_creates_from_file(self):
83        """測試工廠根據副檔名建立解析器"""
84        from parsers import ParserFactory
85
86        parser = ParserFactory.create_from_file("config.toml")
87        self.assertIsInstance(parser, TomlParser)
88
89if __name__ == "__main__":
90    unittest.main()

步驟 5:更新文件

在 README 或相關文件中記錄新功能:

 1## 支援的配置格式
 2
 3- JSON (.json) - 內建支援
 4- YAML (.yaml, .yml) - 需要 PyYAML
 5- TOML (.toml) - 需要 Python 3.11+ 或 toml 套件
 6
 7### 安裝 TOML 支援
 8
 9```bash
10# Python 3.11+ 內建支援
11# 或安裝第三方套件
12pip install toml
13```

使用範例

1from parsers import ParserFactory
2
3# 自動選擇解析器
4parser = ParserFactory.create_from_file("config.toml")
5config = parser.parse_file("config.toml")
 1
 2## 完整檢查清單
 3
 4新增解析器時的檢查項目
 5
 6- [ ] 繼承 `BaseParser`
 7- [ ] 實作所有 `@abstractmethod`
 8  - [ ] `parse(content: str) -> dict`
 9  - [ ] `validate(content: str) -> bool`
10  - [ ] `file_extensions` 屬性
11- [ ] 處理可選依賴
12- [ ] 註冊到 `ParserFactory`
13- [ ] 撰寫單元測試
14  - [ ] 正常解析
15  - [ ] 錯誤處理
16  - [ ] 驗證功能
17  - [ ] 工廠整合
18- [ ] 更新文件
19- [ ] 更新 `__all__` 匯出
20
21## 常見問題
22
23### Q: 如果依賴套件不可用怎麼辦?
24
25 `__init__` 中檢查並提供清楚的錯誤訊息
26
27```python
28def __init__(self):
29    if required_module is None:
30        raise ImportError(
31            "TomlParser requires 'toml' package. "
32            "Install with: pip install toml"
33        )

Q: 如何處理不同版本的 API?

1try:
2    import tomllib  # Python 3.11+
3    def parse_toml(content: str) -> dict:
4        return tomllib.loads(content)
5except ImportError:
6    import toml
7    def parse_toml(content: str) -> dict:
8        return toml.loads(content)

Q: 解析錯誤應該拋出什麼異常?

建議轉換為標準異常並保留原始資訊:

1try:
2    return toml.loads(content)
3except toml.TomlDecodeError as e:
4    raise ValueError(f"Invalid TOML: {e}") from e

思考題

  1. 為什麼要將原始異常作為 from e 傳遞?
  2. 如何設計讓解析器支援串流處理(大檔案)?
  3. 如果要支援解析器鏈(先解密再解析),應該如何設計?

延伸閱讀(進階系列)


上一章:如何擴展共用模組 回到首頁:Python 維護工程師實戰指南