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 duckdbfrom duckdb.sqltypes import VARCHARfrom 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 duckdbcon = duckdb.connect()con.create_function(name, function, parameters, return_type)Die Methode create_function nimmt die folgenden Parameter entgegen:
nameEine Zeichenkette, die den eindeutigen Namen der UDF im Katalog der Verbindung angibt.functionDie Python-Funktion, die Sie als UDF registrieren möchten.parametersSkalarfunktionen können auf einer oder mehreren Spalten arbeiten. Dieser Parameter nimmt eine Liste der als Eingabe verwendeten Spaltentypen entgegen.return_typeSkalarfunktionen geben ein Element pro Zeile zurück. Dieser Parameter gibt den Rückgabetyp der Funktion an.type(optional): DuckDB unterstützt sowohl native Python-Typen als auch PyArrow-Arrays. Standardmäßig wirdtype = 'native'angenommen, Sie können jedochtype = 'arrow'angeben, um PyArrow-Arrays zu verwenden. Im Allgemeinen ist eine Arrow-UDF deutlich effizienter als eine native, weil sie in Batches arbeiten kann.null_handling(optional): Standardmäßig werdenNULL-Werte automatisch alsNULL-inNULL-out behandelt. Benutzer können ein gewünschtes Verhalten fürNULL-Werte festlegen, indem sienull_handling = 'special'setzen.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 stattdessenNULLzurückgeben, indem sie diesen Parameter auf'return_null'setzen.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, mussside_effectsaufTruegesetzt 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 datetimeimport duckdbimport 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 duckdbfrom 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 duckdbfrom 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 duckdbfrom 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 duckdbfrom 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)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 = 0Wenn 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 = 0con.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 duckdbimport pyarrow as pafrom duckdb.sqltypes import VARCHARfrom 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 DATEfrom 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),)]