为 DataFusion 编写批量 UDF,并看清过滤下推的代价

DataFusion 已提供大量表达式和内置函数,但有些计算仍需要接入自己的逻辑。Python 自定义函数可以承担这项工作:标量函数处理每行的值,聚合函数合并一组输入,窗口函数则在窗口分区上计算。这里最容易混淆的一点是,标量 UDF 的语义虽然逐行独立,调用接口却一次接收整批 Arrow 数组。

本文完整译写 Apache DataFusion Python 官方 User-Defined Functions,核对日期为 2026 年 10 月 5 日。原页未署个人作者,归属 Apache Software Foundation 及 DataFusion 贡献者。文中保留示例、性能边界与扩展入口;发现的指南与当前源码差异单独注明。所有代码仅静态审阅,示例输出引用原文或明确标为推算,没有进行本地运行。

DataFusion 原生表达式可将可分析的过滤条件交给 Parquet 扫描并利用统计信息剪枝,Python UDF 内部规则对优化器不可见,通常需要读取候选数据后执行回调
原创技术图:未完纪技术编辑,依据 Apache DataFusion Python 文档绘制。示意表达式可见性与读取路径,不表示实测性能比例。

标量函数:一批数组进,一批数组出

调用 udf() 可以把 Python 函数定义为标量 UDF。函数接收一个或多个 PyArrow 数组,并返回一个数组;批内各行仍应按逐行逻辑计算。下面检查列 a 中哪些位置是空值:

import pyarrow
import datafusion
from datafusion import udf, col

def is_null(array: pyarrow.Array) -> pyarrow.Array:
    return array.is_null()

is_null_arr = udf(is_null, [pyarrow.int64()], pyarrow.bool_(), "stable")
ctx = datafusion.SessionContext()
batch = pyarrow.RecordBatch.from_arrays(
    [pyarrow.array([1, None, 3]), pyarrow.array([4, 5, 6])],
    names=["a", "b"],
)
df = ctx.create_dataframe([[batch]], name="batch_array")
df.select(col("a"), is_null_arr(col("a")).alias("is_null")).show()

原文展示的结果如下:空值位置返回 true,其他位置返回 false。

+---+---------+
| a | is_null |
+---+---------+
| 1 | false   |
|   | true    |
| 3 | false   |
+---+---------+

示例直接使用数组的 is_null()。PyArrow 还有许多 compute 函数;能用它们实现时,应优先让计算留在 Arrow 数据表示中,避免先逐项转成 Python 对象。原文强调,这能减少数组到 Python 值往返转换和相关复制开销。具体算子仍可能分配输出缓冲区,不能把这理解成所有 Arrow 运算都“零分配”。

如果目标操作没有对应的 Arrow compute 函数,就可能需要取出 Python 值、执行自己的计算,再构造输出数组。下面与前一例含义相同,但逐项调用 as_py():

import pyarrow
import datafusion
from datafusion import udf, col

def is_null(array: pyarrow.Array) -> pyarrow.Array:
    return pyarrow.array([value.as_py() is None for value in array])

is_null_arr = udf(is_null, [pyarrow.int64()], pyarrow.bool_(), "stable")
ctx = datafusion.SessionContext()
batch = pyarrow.RecordBatch.from_arrays(
    [pyarrow.array([1, None, 3]), pyarrow.array([4, 5, 6])],
    names=["a", "b"],
)
df = ctx.create_dataframe([[batch]], name="batch_array")
df.select(col("a"), is_null_arr(col("a")).alias("is_null")).show()

原文给出的输出与上一表一致,但执行路径不同。把 Arrow 值转成 Python 对象可能成为 DataFusion 工作负载中最慢的环节之一,因此应尽量减少。批量调用并不自动意味着批内计算已经向量化。

上面注册时传入的是 PyArrow DataType。需要控制元数据、输入或输出的可空性时,可改用 Field。函数的波动性也应真实反映语义:immutable 表示相同输入始终得到相同结果;stable 表示同一查询内稳定;volatile 则可能在各次调用间变化。不能为了优化随意把外部查询或随机计算声明成不可变。

如果仍需要自定义计算,又希望避免 Python 对象往返转换,可用 Rust 实现并暴露给 Python。原文链接了 DataFusion 的 Rust/Python UDF 比较文章。这是进一步阅读入口,不在本文建立一套不完整的 Rust 编译流程。

什么时候不应该写 UDF

适合 UDF 的,是确实无法用内置表达式树描述的逐行计算。如果筛选条件本来可以由列、常量和布尔运算表达,只是写成 Python 循环更顺手,UDF 可能让查询变慢。优化器看不到 UDF 内部的规则,因此会失去原生表达式可用的一些改写机会,尤其是向表提供器下推可分析的过滤条件。

以 Parquet 为例,原生条件可以借助文件尾部的最小值、最大值及其他统计信息排除不可能匹配的行组。类似机制也适用于声明支持过滤的其他表提供器。下面先创建一个小 Parquet 文件,然后比较两种写法的计划。这个例子关注计划结构,不是性能基准:

import tempfile, os
import pyarrow as pa
import pyarrow.parquet as pq
from datafusion import SessionContext, col, lit, udf

tmpdir = tempfile.mkdtemp()
parquet_path = os.path.join(tmpdir, "items.parquet")
pq.write_table(
    pa.table({
        "id": list(range(100)),
        "brand": ["A", "B", "C", "D"] * 25,
        "qty": [i * 10 for i in range(100)],
    }),
    parquet_path,
)
ctx = SessionContext()
items = ctx.read_parquet(parquet_path)

native_filtered = items.filter(
    (col("brand") == lit("A")) & (col("qty") >= lit(150))
)
print(native_filtered.execution_plan().display_indent())

原文计划包含 FilterExec、RepartitionExec 和 DataSourceExec。扫描节点上最值得关注的是三个字段:

  • predicate=brand@1 = A AND qty@2 >= 150:扫描器收到过滤条件。
  • pruning_predicate=… brand_min@0 <= A AND A <= brand_max@1 … qty_max@4 >= 150:可用行组统计信息排除不匹配的行组,原文中还带有空值计数判断。
  • required_guarantees=[brand in (A)]:在具备相应 Bloom filter 或字典信息时,可用于进一步跳过数据。

这些字段证明优化器能理解条件,但不保证任意文件一定少读。100 行的小文件可能只有一个行组;若统计范围同时覆盖 A 和 B、数量范围也与阈值重叠,就未必能剪掉它。实际收益取决于文件布局、统计信息、引擎版本与配置。

再把同样的规则包装为 Python UDF:

def brand_qty_filter(brand_arr: pa.Array, qty_arr: pa.Array) -> pa.Array:
    return pa.array([
        b.as_py() == "A" and q.as_py() >= 150
        for b, q in zip(brand_arr, qty_arr)
    ])

pred_udf = udf(
    brand_qty_filter, [pa.string(), pa.int64()], pa.bool_(), "stable",
)
udf_filtered = items.filter(pred_udf(col("brand"), col("qty")))
print(udf_filtered.execution_plan().display_indent())

原文展示的扫描节点仍可能带 predicate=brand_qty_filter(...),但没有 pruning_predicate 或 required_guarantees。所以“UDF 不能下推”更精确的含义是:优化器无法把函数内部规则变成可用于统计剪枝的原生条件;并非任何版本的扫描节点都绝不会出现 UDF 谓词。对于本例中的这条 UDF 规则,引擎需要读出候选行组并交给回调判断。

编辑核对:原始数据没有空值。如果品牌为 A 而数量为 null,原样回调会尝试比较 None >= 150,产生异常。生产版本必须定义空值语义并补充处理,不能仅凭无空值样例推定与 SQL 的三值逻辑完全等价。临时目录由 mkdtemp() 创建,示例未清理;应在数据读取和实际执行完成后再清理,避免惰性查询尚未执行就删除文件。

原文的完整执行计划对照

以下两段为原文展示的计划,依次对应原生表达式和 Python UDF;路径、并行分区数及格式属于该次上游示例,不是本稿运行所得。

FilterExec: brand@1 = A AND qty@2 >= 150
  RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1
    DataSourceExec: file_groups={1 group: [[tmp/tmpsywpupmx/items.parquet]]}, projection=[id, brand, qty], file_type=parquet, predicate=brand@1 = A AND qty@2 >= 150, pruning_predicate=brand_null_count@2 != row_count@3 AND brand_min@0 <= A AND A <= brand_max@1 AND qty_null_count@5 != row_count@3 AND qty_max@4 >= 150, required_guarantees=[brand in (A)]
FilterExec: brand_qty_filter(CAST(brand@1 AS Utf8), qty@2)
  RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1
    DataSourceExec: file_groups={1 group: [[tmp/tmpsywpupmx/items.parquet]]}, projection=[id, brand, qty], file_type=parquet, predicate=brand_qty_filter(CAST(brand@1 AS Utf8), qty@2)

多组业务规则仍然可以组合成表达式

如果条件是“满足任意一组品牌规则即可”,可先为每组构建一个 AND 子句,再用 | 合成 OR。下面保留原文规则:

from functools import reduce
from operator import or_
from datafusion import col, lit, functions as f

buckets = {
    "Brand#12": {"containers": ["SM CASE", "SM BOX"], "min_qty": 1, "max_size": 5},
    "Brand#23": {"containers": ["MED BAG", "MED BOX"], "min_qty": 10, "max_size": 10},
}

def bucket_clause(brand, spec):
    return (
        (col("brand") == lit(brand))
        & f.in_list(col("container"), [lit(c) for c in spec["containers"]])
        & (col("quantity") >= lit(spec["min_qty"]))
        & (col("quantity") <= lit(spec["min_qty"] + 10))
        & (col("size") >= lit(1))
        & (col("size") <= lit(spec["max_size"]))
    )

predicate = reduce(or_, (bucket_clause(b, s) for b, s in buckets.items()))
df = df.filter(predicate)

这是表达式拼接片段,不可直接接在前面的两列 df 或 items 后运行:输入需包含 brand、container、quantity、size。原文的 buckets 非空;若配置允许空规则集,应先决定它表示“保留全部”还是“不保留任何行”,再提供相应恒真或恒假初值,避免 reduce() 在空序列上失败。

聚合函数:把更新、状态和合并分开

udaf() 用于注册自定义聚合函数,需要实现 Accumulator。它包含四个核心方法:update 接收输入数组并更新局部状态;state 返回表示当前状态的标量列表;merge 合并多个局部状态;evaluate 给出最终标量结果。聚合可能跨多个批次和分区,因而“能更新一个批次”还不够,状态必须可以正确合并。

下面的原文示例计算两列总和之差,内部状态只需一个数:

import pyarrow as pa
import pyarrow.compute
import datafusion
from datafusion import col, udaf, Accumulator

class MyAccumulator(Accumulator):
    def __init__(self):
        self._sum = 0.0

    def update(self, values_a: pa.Array, values_b: pa.Array) -> None:
        self._sum = (self._sum
                     + pyarrow.compute.sum(values_a).as_py()
                     - pyarrow.compute.sum(values_b).as_py())

    def merge(self, states: list[pa.Array]) -> None:
        self._sum = self._sum + pyarrow.compute.sum(states[0]).as_py()

    def state(self) -> list[pa.Scalar]:
        return [pyarrow.scalar(self._sum)]

    def evaluate(self) -> pa.Scalar:
        return pyarrow.scalar(self._sum)

ctx = datafusion.SessionContext()
df = ctx.from_pydict({"a": [4, 5, 6], "b": [1, 2, 3]})
my_udaf = udaf(
    MyAccumulator, [pa.float64(), pa.float64()], pa.float64(),
    [pa.float64()], "stable",
)
df.aggregate([], [my_udaf(col("a"), col("b")).alias("col_diff")])

样例中的结果按算术可推得为 9,但原文最后一行只构造 DataFrame;若要观察运行结果,还需调用 show() 或其他收集方法。这里没有把推算值伪装成本次实测输出。

编辑核对:原实现没有空批次或全空值保护,Arrow sum 的结果可能是 null,转成 Python 后为 None,再参与加减会失败。若业务选择“空批次与全空值贡献为零”,可将每个求和结果显式转换成 0.0 if total is None else total;这是新增业务约定,会改变“全空值返回 null”的语义,不应不加说明地替换原代码。浮点状态还可能因分区合并顺序出现舍入差异。

聚合函数怎样返回列表

evaluate 返回标量,state 返回标量列表;如果想让一个标量本身包含列表,应使用 Arrow 列表标量。例如时间戳列表可写成 pa.scalar([...], type=pa.list_(pa.timestamp("ms"))),相应注册类型为 return_type=pa.list_(pa.timestamp("ms")),或 state_type=[pa.list_(pa.timestamp("ms"))]。不要把“状态的标量列表”与“列表类型的单个标量”混为一谈。

原文说明,自 DataFusion 52.0.0 起,这些返回位置还可接收普通 Python 对象或 PyArrow 数组,由 DataFusion 尝试转成标量,已覆盖 PyArrow、nanoarrow、arro3 和常见基础类型。这个兼容说明有明确版本下限;显式构造正确类型的标量仍更容易核对。

窗口函数:选择合适的求值接口

自定义窗口函数通过 udwf() 注册实现 WindowEvaluator 的求值器。evaluate 针对单行所需范围求值,最直接但调用开销较高;evaluate_all 一次计算整个输入分区;evaluate_all_with_rank 则只依赖行的排名信息。

uses_window_frame 表示是否使用指定窗口框架,supports_bounded_execution 表示能否以有界内存增量计算,include_rank 表示能否只用排名求值。下表按核验时的 WindowEvaluator 源码整理:

uses_window_frame supports_bounded_execution include_rank 应实现
False False False evaluate_all
False True False evaluate
False True 或 False True evaluate_all_with_rank
True True 或 False True 或 False evaluate

与原文的差异:指南第三行误列为 False / True / False,与前一行冲突。当前源码将排名求值行写为 False / True或False / True,上表使用后者。版本升级时仍需核对安装版本的 API。

原文的指数平滑从首值开始,后续按“当前输入 × α + 上次结果 × (1 − α)”递推。以下保留计算主体,同时把注册改成当前接口要求的求值器工厂:

import pyarrow as pa
from datafusion import udwf, col, SessionContext
from datafusion.user_defined import WindowEvaluator

class ExponentialSmooth(WindowEvaluator):
    def __init__(self, alpha: float) -> None:
        self.alpha = alpha

    def evaluate_all(self, values: list[pa.Array], num_rows: int) -> pa.Array:
        results = []
        curr_value = 0.0
        values = values[0]
        for idx in range(num_rows):
            if idx == 0:
                curr_value = values[idx].as_py()
            else:
                curr_value = values[idx].as_py() * self.alpha + curr_value * (
                    1.0 - self.alpha
                )
            results.append(curr_value)
        return pa.array(results)

def make_exponential_smooth():
    return ExponentialSmooth(0.9)

exp_smooth = udwf(
    make_exponential_smooth, pa.float64(), pa.float64(),
    volatility="immutable",
)
ctx = SessionContext()
df = ctx.from_pydict({"a": [1.0, 2.1, 2.9, 4.0, 5.1, 6.0, 6.9, 8.0]})
df.select("a", exp_smooth(col("a")).alias("smooth_a")).show()

注册修正依据:原文传入 ExponentialSmooth(0.9) 实例;当前 WindowUDF._create_window_udf 要求参数可调用,并调用它来创建 WindowEvaluator。因此这里使用具名工厂函数。此修正只做了接口级静态核对,未实测。样例没有空值、没有显式排序,也没有验证 α 的范围;真实时间序列应明确排序及分区、定义空值策略,并按业务约束验证参数。空输入返回数组的显式类型也需要在完整实现中处理。

表函数与调用会话

自定义表函数 UDTF 与前面的函数有所不同:它接收任意数量的 Expr 参数,但当前仅支持字面量表达式;返回值必须是 自定义表提供器。定义后可通过 SessionContext.register_udtf() 注册。仓库 examples 中包含 Python 与 Rust 实现。

Rust 表函数通过 PyO3 暴露时,原文展示了下面的 PyCapsule 接口片段。它缺少类型定义、依赖及构建配置,仅说明接口形状,不能单独复制运行:

#[pymethods]
impl MyTableFunction {
    fn __datafusion_table_function__<'py>(
        &self,
        py: Python<'py>,
    ) -> PyResult<Bound<'py, PyCapsule>> {
        let name = cr"datafusion_table_function".into();
        let func = self.clone();
        let provider = FFI_TableFunction::new(Arc::new(func), None);
        PyCapsule::new(py, provider, Some(name))
    }
}

纯 Python 表函数还可以通过 with_session=True 接收调用方的 SessionContext。每次调用时,框架会把它作为 session 关键字参数传入,函数即可查询注册表、UDF 或会话配置:

from datafusion import SessionContext, Table, udtf
from datafusion.context import TableProviderExportable
import pyarrow as pa
import pyarrow.dataset as ds

@udtf("list_tables", with_session=True)
def list_tables(*, session: SessionContext) -> TableProviderExportable:
    names = sorted(session.catalog().schema().names())
    batch = pa.RecordBatch.from_pydict({"name": names})
    return Table(ds.dataset([batch]))

ctx = SessionContext()
ctx.register_batch("t1", pa.RecordBatch.from_pydict({"x": [1]}))
ctx.register_udtf(list_tables)
ctx.sql("SELECT * FROM list_tables()").show()

不启用 with_session 时,回调只接收位置表达式参数,原有 UDTF 行为保持不变。注入的对象是一个新的包装器,但底层注册表与调用方共享:注册新表或新 UDF 会传播到活动会话;配置则是克隆,修改该包装器中的会话配置不会回传。当前源码还限制这个选项只能用于纯 Python 回调,不能直接与 FFI 表函数混用。空目录时,例子中 name 列会由空列表推断类型,正式实现可以明确指定字符串类型。

选择 UDF 时,可以先检查两个问题:内置表达式是否已经能表示这项计算,输入是否必须离开 Arrow 表示。原生表达式有利于优化器分析,Arrow 内核有利于避免 Python 对象转换;确实需要自定义计算时,再选择相应的标量、聚合、窗口或表函数接口,并把状态、空值、排序和版本边界一起定义清楚。

许可与修改声明:项目源码按 Apache License 2.0 提供,许可全文保留在下方 LICENSE.txt。本稿于 2026-10-05 翻译、重排并加入静态审阅说明;未修改原项目。Apache Arrow DataFusion、Arrow DataFusion、Apache 及相关羽毛与项目标志是 Apache Software Foundation 在美国及其他国家的注册商标或商标。本文未使用项目标志。原许可按“现状”提供,不作保证。

版权与许可全文

以下保留本页涉及的来源材料或示例代码的版权、许可条件与免责声明;各自适用范围依原声明。中文翻译及编辑标注:未完纪,2026-10-05。

LICENSE.txt

Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.
      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.
      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.
      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).
      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.
      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."
      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.
   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.
   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.
   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and
      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and
      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.
      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.
   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.
   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.
   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.
   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.
   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.
   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.
      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.
   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容