Zum Inhalt springen

Python-Funktions-API

Sie können eine benutzerdefinierte DuckDB-Funktion (UDF) aus einer Python-Funktion erstellen, sodass sie in SQL-Abfragen verwendet werden kann. Wie bei regulären Funktionen müssen ein Name, ein Rückgabetyp und Parametertypen angegeben werden.

Hier ein Beispiel mit einer Python-Funktion, die eine Drittanbieterbibliothek aufruft.

import duckdb
from duckdb.sqltypes import VARCHAR
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',)]

Funktionen erstellen

Um eine Python-UDF zu registrieren, verwenden Sie die Methode create_function einer DuckDB-Verbindung. Hier die Syntax:

import duckdb
con = duckdb.connect()
con.create_function(name, function, parameters, return_type)

Die Methode create_function nimmt die folgenden Parameter entgegen:

  1. name Eine Zeichenkette, die den eindeutigen Namen der UDF im Katalog der Verbindung angibt.
  2. function Die Python-Funktion, die Sie als UDF registrieren möchten.
  3. parameters Skalarfunktionen können auf einer oder mehreren Spalten arbeiten. Dieser Parameter nimmt eine Liste der als Eingabe verwendeten Spaltentypen entgegen.
  4. return_type Skalarfunktionen geben ein Element pro Zeile zurück. Dieser Parameter gibt den Rückgabetyp der Funktion an.
  5. type (optional): DuckDB unterstützt sowohl native Python-Typen als auch PyArrow-Arrays. Standardmäßig wird type = 'native' angenommen, Sie können jedoch type = 'arrow' angeben, um PyArrow-Arrays zu verwenden. Im Allgemeinen ist eine Arrow-UDF deutlich effizienter als eine native, weil sie in Batches arbeiten kann.
  6. null_handling (optional): Standardmäßig werden NULL-Werte automatisch als NULL-in NULL-out behandelt. Benutzer können ein gewünschtes Verhalten für NULL-Werte festlegen, indem sie null_handling = 'special' setzen.
  7. exception_handling (optional): Standardmäßig wird eine in der Python-Funktion ausgelöste Ausnahme in Python erneut ausgelöst. Benutzer können dieses Verhalten deaktivieren und stattdessen NULL zurückgeben, indem sie diesen Parameter auf 'return_null' setzen.
  8. side_effects (optional): Standardmäßig wird erwartet, dass Funktionen für dieselbe Eingabe dasselbe Ergebnis liefern. Wird das Ergebnis einer Funktion durch irgendeine Form von Zufälligkeit beeinflusst, muss side_effects auf True gesetzt werden.

Um eine UDF zu deregistrieren, können Sie die Methode remove_function mit dem UDF-Namen aufrufen:

con.remove_function(name)

Partielle Funktionen verwenden

DuckDB-UDFs können auch mit partiellen Python-Funktionen erstellt werden.

Im folgenden Beispiel zeigen wir, wie ein benutzerdefinierter Logger die Konkatenation des Ausführungszeitpunkts im ISO-Format zurückgibt, stets gefolgt vom bei der UDF-Erstellung übergebenen Argument und dem beim Funktionsaufruf übergebenen Eingabeparameter:

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 │
└────────────────────────────────┘

Typannotation

Wenn die Funktion Typannotationen hat, können oft alle optionalen Parameter weggelassen werden. Mit DuckDBPyType können viele bekannte Typen implizit in das Typsystem von DuckDB umgewandelt werden. Zum Beispiel:

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 │
└─────────────┘

Wenn nur die Typen der Parameterliste abgeleitet werden können, müssen Sie None als parameters übergeben.

NULL-Behandlung

Wenn Funktionen standardmäßig einen NULL-Wert erhalten, wird sofort NULL zurückgegeben – als Teil der Standard-NULL-Behandlung. Wenn das nicht erwünscht ist, müssen Sie diesen Parameter explizit auf "special" setzen.

import duckdb
from duckdb.sqltypes import BIGINT
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,)]

Mit null_handling="special":

import duckdb
from duckdb.sqltypes import BIGINT
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,)]

Verwenden Sie immer null_handling="special", wenn die Funktion NULL zurückgeben kann.

import duckdb
from duckdb.sqltypes 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,)]

Ausnahmebehandlung

Standardmäßig wird eine in der Python-Funktion ausgelöste Ausnahme weitergeleitet (erneut ausgelöst). Wenn Sie dieses Verhalten deaktivieren und stattdessen NULL zurückgeben möchten, müssen Sie diesen Parameter auf "return_null" setzen.

import duckdb
from duckdb.sqltypes import BIGINT
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)
Terminal window
Invalid Input Error:
Python exception occurred while executing the UDF: ValueError: ERROR
At:
...(5): will_throw
...(9): <module>
[(None,)]

Seiteneffekte

Standardmäßig geht DuckDB davon aus, dass die erstellte Funktion eine reine Funktion ist, also bei gleicher Eingabe dieselbe Ausgabe erzeugt. Folgt Ihre Funktion dieser Regel nicht, zum Beispiel wenn sie Zufallswerte verwendet, müssen Sie diese Funktion mit side_effects markieren.

Diese Funktion erzeugt beispielsweise bei jedem Aufruf einen neuen Zählerstand.

def count() -> int:
old = count.counter;
count.counter += 1
return old
count.counter = 0

Wenn wir diese Funktion erstellen, ohne sie mit Seiteneffekten zu markieren, ist das Ergebnis das folgende:

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,)]

Das ist offensichtlich nicht das gewünschte Ergebnis. Mit side_effects=True entspricht das Ergebnis der Erwartung:

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-Funktionstypen

Derzeit werden zwei Funktionstypen unterstützt: native (Standard) und arrow.

Arrow

Wenn die Funktion Arrow-Arrays entgegennehmen soll, setzen Sie den Parameter type auf 'arrow'.

Damit weiß das System, der Funktion Arrow-Arrays mit bis zu STANDARD_VECTOR_SIZE Tupeln bereitzustellen, und erwartet, dass die Funktion ein Array mit derselben Anzahl von Tupeln zurückgibt.

Im Allgemeinen ist eine Arrow-UDF deutlich effizienter als eine native, weil sie in Batches arbeiten kann.

import duckdb
import pyarrow as pa
from duckdb.sqltypes import VARCHAR
from pyarrow import compute as pc
def mirror(strings: pa.Array, sep: pa.Array) -> pa.Array:
assert isinstance(strings, pa.ChunkedArray)
assert isinstance(sep, pa.ChunkedArray)
return pc.binary_join_element_wise(strings, pc.ascii_reverse(strings), sep)
duckdb.create_function(
"mirror",
mirror,
[VARCHAR, VARCHAR],
return_type=VARCHAR,
type="arrow",
)
duckdb.sql(
"CREATE OR REPLACE TABLE strings AS SELECT 'hello' AS str UNION ALL SELECT 'world' AS str;"
)
print(duckdb.sql("SELECT mirror(str, '|') FROM strings;").fetchall())
[('hello|olleh',), ('world|dlrow',)]

Native

Wenn der Funktionstyp auf native gesetzt ist, erhält die Funktion jeweils ein einzelnes Tupel und erwartet, dass nur ein einzelner Wert zurückgegeben wird. Das kann nützlich sein, um mit Python-Bibliotheken zu interagieren, die nicht mit Arrow arbeiten, etwa faker:

import duckdb
from duckdb.sqltypes import DATE
from faker import Faker
def random_date():
fake = Faker()
return fake.date_between()
duckdb.create_function(
"random_date",
random_date,
parameters=[],
return_type=DATE,
type="native",
)
res = duckdb.sql("SELECT random_date()").fetchall()
print(res)
[(datetime.date(2019, 5, 15),)]