3. 自定义 Expectation 开发环境搭建:项目结构、模板代码、测试框架
说实话,很多人在 Great Expectations 里卡住,不是因为概念不懂,而是因为不知道怎么开始写第一个自定义 Expectation。我见过不少团队,文档翻了好几遍,最后还是在项目结构上栽了跟头。
这一章,我们就来把这个「开始」理清楚。我会带你搭一个干净、可复用的开发环境。你跟着走一遍,后面写插件就会顺手很多。
3.1 项目结构:别乱放,按规矩来
Great Expectations 对自定义插件的加载路径有严格要求。说白了,它只会在几个固定的目录里找你的代码。放错了,它就当没看见。
我个人习惯用这样的结构:
custom_expectations/
├── great_expectations.yml # 项目级配置
├── expectations/
│ ├── __init__.py
│ ├── expect_column_values_to_be_positive.py
│ └── expect_column_values_to_be_in_range.py
├── plugins/
│ └── custom_metrics.py # 可选:自定义 Metric
├── tests/
│ ├── __init__.py
│ ├── test_expect_column_values_to_be_positive.py
│ └── conftest.py # 测试夹具
├── data/
│ └── sample.csv # 测试用数据
└── requirements-dev.txt
这里有几个关键点:
- expectations/ 目录是核心。每个文件放一个 Expectation 类。文件名和类名最好对应,方便查找。
- plugins/ 目录放辅助逻辑。比如你写了一个复杂的统计函数,不想塞进 Expectation 类里,就放这里。
- tests/ 目录必须和 expectations/ 保持镜像。我见过有人把所有测试写在一个文件里,结果项目大了根本跑不动。
plugins_directory: plugins/,这样 Great Expectations 启动时会自动加载 plugins 目录下的模块。我曾经忘了配这个,调试了半小时才发现。
3.2 模板代码:从官方脚手架开始
写自定义 Expectation 最怕从零开始。好在 Great Expectations 官方提供了一个 CLI 命令,能帮你生成模板。
运行这个:
great_expectations expectation new
它会问你几个问题:Expectation 类型、列名、参数等。然后生成一个完整的 .py 文件。你打开看看,结构是这样的:
from great_expectations.expectations.expectation import ColumnMapExpectation
from great_expectations.expectations.util import render_evaluation_parameter_string
class ExpectColumnValuesToBePositive(ColumnMapExpectation):
# 1. 元数据
library_metadata = {
"maturity": "experimental",
"tags": ["custom", "numeric"],
"contributors": ["your_name"],
}
# 2. 默认参数
default_kwarg_values = {
"mostly": 1.0,
"result_format": "BASIC",
}
# 3. 核心逻辑
def _validate(
self,
configuration: ExpectationConfiguration,
metrics: dict,
runtime_configuration: dict,
execution_environment: ExecutionEnvironment,
):
# 这里写你的验证逻辑
pass
# 4. 指标映射
metric_dependencies = ["column_values.nonnull.count"]
# 5. 成功条件
success_keys = ("mostly",)
嗯,这里要注意:_validate 方法是你的主战场。所有逻辑都写在这里。它接收一个 metrics 字典,里面是 Great Expectations 计算好的统计值。你只需要判断这些值是否满足条件,然后返回一个字典。
返回格式必须是这样:
{
"success": True/False,
"result": {
"observed_value": 0.95,
"details": {...}
}
}
为什么 success 和 result 要分开?因为 Great Expectations 的渲染引擎会根据 result 里的内容生成漂亮的 HTML 报告。你想想看,如果只返回一个布尔值,报告里就啥也看不到了。
3.3 测试框架:别等集成测试才发现问题
我刚开始写自定义 Expectation 时,习惯直接跑 great_expectations suite new 来验证。结果每次都要等几分钟,特别烦。后来我学乖了,用 pytest 写单元测试,几秒钟就能跑一轮。
测试框架的核心是 conftest.py。它负责创建测试用的 DataContext 和 DataFrame。这是我的标准模板:
# tests/conftest.py
import pytest
import pandas as pd
from great_expectations.data_context import BaseDataContext
from great_expectations.data_context.types.base import DataContextConfig, InMemoryStoreBackendDefaults
@pytest.fixture
def in_memory_context():
"""创建一个内存中的 DataContext,不依赖文件系统"""
project_config = DataContextConfig(
store_backend_defaults=InMemoryStoreBackendDefaults()
)
return BaseDataContext(project_config)
@pytest.fixture
def sample_df():
"""生成测试用的 DataFrame"""
return pd.DataFrame({
"values": [1, 2, 3, -1, 5, 0, 10],
"names": ["a", "b", "c", "d", "e", "f", "g"],
})
然后测试文件里这样写:
# tests/test_expect_column_values_to_be_positive.py
import pandas as pd
from expectations.expect_column_values_to_be_positive import ExpectColumnValuesToBePositive
class TestExpectColumnValuesToBePositive:
def test_positive_values(self, in_memory_context, sample_df):
# 只取正数部分
df = sample_df[sample_df["values"] > 0]
result = ExpectColumnValuesToBePositive().validate(
df,
result_format="COMPLETE"
)
assert result["success"] is True
def test_negative_values(self, in_memory_context, sample_df):
# 包含负数
result = ExpectColumnValuesToBePositive().validate(
sample_df,
result_format="COMPLETE"
)
assert result["success"] is False
assert result["result"]["observed_value"] == 4/7 # 4个正数 / 7个总数
result_format。结果 Great Expectations 默认只返回 BASIC 格式,observed_value 是空的。后来我查了半天文档才发现。所以测试时一定要显式指定 result_format="COMPLETE"。
3.4 知识体系总览
下面这张图把整个开发环境的核心关系画出来了。你看一眼,心里就有数了。
3.5 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Great Expectations 找不到自定义 Expectation | 插件目录未注册或路径不对 | 检查 great_expectations.yml 中的 plugins_directory 配置 |
| 测试中 observed_value 为空 | 未指定 result_format 或使用了 BASIC 模式 | 测试时显式传入 result_format="COMPLETE" |
| _validate 方法返回格式错误 | 缺少 success 或 result 字段 | 严格按照 {success: bool, result: dict} 格式返回 |
| pytest 报模块找不到 | PYTHONPATH 未包含项目根目录 | 在 conftest.py 中 sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) |
核心要点: 开发环境搭建的本质是「约定大于配置」。Great Expectations 对目录结构、类命名、方法签名都有约定。你遵守这些约定,它就能自动发现你的插件。你打破约定,它就会让你花大量时间调试。所以,从模板开始,不要自己造轮子。