您可以从 Python 函数创建 DuckDB 用户定义函数 (UDF),以便在 SQL 查询中使用。与常规函数类似,它们需要有名称、返回类型和参数类型。
以下是一个使用调用第三方库的 Python 函数的示例。
import duckdb
from duckdb.typing import *
from faker import Faker
def generate_random_name():
fake = Faker()
return fake.name()
duckdb.create_function("random_name", generate_random_name, [], VARCHAR)
res = duckdb.sql("SELECT random_name()").fetchall()
print(res)
[('Gerald Ashley',)]
创建函数
要注册 Python UDF,请使用 DuckDB 连接的 create_function
方法。语法如下
import duckdb
con = duckdb.connect()
con.create_function(name, function, parameters, return_type)
create_function
方法接受以下参数
name
一个字符串,表示 UDF 在连接目录中的唯一名称。function
您希望注册为 UDF 的 Python 函数。parameters
标量函数可以对一个或多个列进行操作。此参数接受用作输入的列类型列表。return_type
标量函数每行返回一个元素。此参数指定函数的返回类型。type
(可选):DuckDB 同时支持内置 Python 类型和 PyArrow 表。默认情况下,假定为内置类型,但您可以指定type = 'arrow'
来使用 PyArrow 表。null_handling
(可选):默认情况下,NULL
值会自动处理为NULL
输入NULL
输出。用户可以通过设置null_handling = 'special'
来指定NULL
值的所需行为。exception_handling
(可选):默认情况下,当 Python 函数抛出异常时,该异常将在 Python 中重新抛出。用户可以通过将此参数设置为'return_null'
来禁用此行为,并改为返回NULL
。side_effects
(可选):默认情况下,函数对于相同的输入应产生相同的结果。如果函数的结果受任何类型的随机性影响,则必须将side_effects
设置为True
。
要注销 UDF,您可以调用 remove_function
方法并传入 UDF 名称
con.remove_function(name)
使用偏函数
DuckDB UDF 也可以通过 Python 偏函数创建。
在下面的示例中,我们展示了自定义记录器如何返回 ISO 格式的执行日期时间串联,其后始终是 UDF 创建时传递的参数和提供给函数调用的输入参数。
from datetime import datetime
import duckdb
import functools
def get_datetime_iso_format() -> str:
return datetime.now().isoformat()
def logger_udf(func, arg1: str, arg2: int) -> str:
return ' '.join([func(), arg1, str(arg2)])
with duckdb.connect() as con:
con.sql("select * from range(10) tbl(id)").to_table("example_table")
con.create_function(
'custom_logger',
functools.partial(logger_udf, get_datetime_iso_format, 'logging data')
)
rel = con.sql("SELECT custom_logger(id) from example_table;")
rel.show()
con.create_function(
'another_custom_logger',
functools.partial(logger_udf, get_datetime_iso_format, ':')
)
rel = con.sql("SELECT another_custom_logger(id) from example_table;")
rel.show()
┌───────────────────────────────────────────┐
│ custom_logger(id) │
│ varchar │
├───────────────────────────────────────────┤
│ 2025-03-27T12:07:56.811251 logging data 0 │
│ 2025-03-27T12:07:56.811264 logging data 1 │
│ 2025-03-27T12:07:56.811266 logging data 2 │
│ 2025-03-27T12:07:56.811268 logging data 3 │
│ 2025-03-27T12:07:56.811269 logging data 4 │
│ 2025-03-27T12:07:56.811270 logging data 5 │
│ 2025-03-27T12:07:56.811271 logging data 6 │
│ 2025-03-27T12:07:56.811272 logging data 7 │
│ 2025-03-27T12:07:56.811274 logging data 8 │
│ 2025-03-27T12:07:56.811275 logging data 9 │
├───────────────────────────────────────────┤
│ 10 rows │
└───────────────────────────────────────────┘
┌────────────────────────────────┐
│ another_custom_logger(id) │
│ varchar │
├────────────────────────────────┤
│ 2025-03-27T12:07:56.812106 : 0 │
│ 2025-03-27T12:07:56.812116 : 1 │
│ 2025-03-27T12:07:56.812118 : 2 │
│ 2025-03-27T12:07:56.812119 : 3 │
│ 2025-03-27T12:07:56.812121 : 4 │
│ 2025-03-27T12:07:56.812122 : 5 │
│ 2025-03-27T12:07:56.812123 : 6 │
│ 2025-03-27T12:07:56.812124 : 7 │
│ 2025-03-27T12:07:56.812126 : 8 │
│ 2025-03-27T12:07:56.812127 : 9 │
├────────────────────────────────┤
│ 10 rows │
└────────────────────────────────┘
类型注解
当函数具有类型注解时,通常可以省略所有可选参数。使用 DuckDBPyType
我们可以将许多已知类型隐式转换为 DuckDB 的类型系统。例如
import duckdb
def my_function(x: int) -> str:
return x
duckdb.create_function("my_func", my_function)
print(duckdb.sql("SELECT my_func(42)"))
┌─────────────┐
│ my_func(42) │
│ varchar │
├─────────────┤
│ 42 │
└─────────────┘
如果只能推断出参数列表类型,则需要将 None
作为 parameters
传入。
NULL
值处理
默认情况下,当函数接收到 NULL
值时,它会立即返回 NULL
,这是默认 NULL
值处理的一部分。如果不需要这种行为,您需要显式地将此参数设置为 "special"
。
import duckdb
from duckdb.typing import *
def dont_intercept_null(x):
return 5
duckdb.create_function("dont_intercept", dont_intercept_null, [BIGINT], BIGINT)
res = duckdb.sql("SELECT dont_intercept(NULL)").fetchall()
print(res)
[(None,)]
使用 null_handling="special"
import duckdb
from duckdb.typing import *
def dont_intercept_null(x):
return 5
duckdb.create_function("dont_intercept", dont_intercept_null, [BIGINT], BIGINT, null_handling="special")
res = duckdb.sql("SELECT dont_intercept(NULL)").fetchall()
print(res)
[(5,)]
当函数可能返回 NULL 时,请始终使用
null_handling="special"
。
import duckdb
from duckdb.typing import VARCHAR
def return_str_or_none(x: str) -> str | None:
if not x:
return None
return x
duckdb.create_function(
"return_str_or_none",
return_str_or_none,
[VARCHAR],
VARCHAR,
null_handling="special"
)
res = duckdb.sql("SELECT return_str_or_none('')").fetchall()
print(res)
[(None,)]
异常处理
默认情况下,当 Python 函数抛出异常时,我们会转发(重新抛出)该异常。如果您想禁用此行为,并改为返回 NULL
,则需要将此参数设置为 "return_null"
。
import duckdb
from duckdb.typing import *
def will_throw():
raise ValueError("ERROR")
duckdb.create_function("throws", will_throw, [], BIGINT)
try:
res = duckdb.sql("SELECT throws()").fetchall()
except duckdb.InvalidInputException as e:
print(e)
duckdb.create_function("doesnt_throw", will_throw, [], BIGINT, exception_handling="return_null")
res = duckdb.sql("SELECT doesnt_throw()").fetchall()
print(res)
Invalid Input Error: Python exception occurred while executing the UDF: ValueError: ERROR
At:
...(5): will_throw
...(9): <module>
[(None,)]
副作用
默认情况下,DuckDB 将假定创建的函数是纯函数,这意味着在给定相同输入时它将产生相同的输出。如果您的函数不遵循此规则,例如当您的函数使用随机性时,则需要将此函数标记为具有 side_effects
。
例如,此函数将在每次调用时生成一个新的计数。
def count() -> int:
old = count.counter;
count.counter += 1
return old
count.counter = 0
如果我们创建此函数时未将其标记为具有副作用,结果将是以下内容
con = duckdb.connect()
con.create_function("my_counter", count, side_effects=False)
res = con.sql("SELECT my_counter() FROM range(10)").fetchall()
print(res)
[(0,), (0,), (0,), (0,), (0,), (0,), (0,), (0,), (0,), (0,)]
这显然不是期望的结果,当我们添加 side_effects=True
时,结果将符合我们的预期
con.remove_function("my_counter")
count.counter = 0
con.create_function("my_counter", count, side_effects=True)
res = con.sql("SELECT my_counter() FROM range(10)").fetchall()
print(res)
[(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,), (8,), (9,)]
Python 函数类型
目前支持两种函数类型:native
(默认) 和 arrow
。
Arrow
如果函数预期接收 Arrow 数组,请将 type
参数设置为 'arrow'
。
这将告知系统向函数提供最多 STANDARD_VECTOR_SIZE
个元组的 Arrow 数组,并期望函数返回相同数量元组的数组。
Native
当函数类型设置为 native
时,函数将一次接收一个元组,并期望只返回一个值。这对于与不操作 Arrow 的 Python 库(例如 faker
)进行交互非常有用。
import duckdb
from duckdb.typing import *
from faker import Faker
def random_date():
fake = Faker()
return fake.date_between()
duckdb.create_function("random_date", random_date, [], DATE, type="native")
res = duckdb.sql("SELECT random_date()").fetchall()
print(res)
[(datetime.date(2019, 5, 15),)]