Conception d’UDFs Python

Ce sujet vous aide à concevoir des UDFs Python.

Dans ce chapitre :

Note

Il existe une API par lots UDF Python, qui permet de définir des fonctions Python recevant des lots de lignes d’entrée sous forme de DataFrames Pandas et renvoyant des lots de résultats sous forme de tableaux Pandas ou de Series. L’interface lots permet d’obtenir de bien meilleures performances avec les scénarios d’inférence de machine learning. Pour plus d’informations, voir API par lots UDF Python.

Choisir vos types de données

Avant d’écrire votre code :

  • Choisissez les types de données que votre fonction doit accepter comme arguments et le type de données que votre fonction doit retourner.

  • Tenez compte des problèmes liés aux fuseaux horaires.

  • Décidez comment traiter les valeurs NULL.

Mappages de type de donnée SQL-Python pour les paramètres et les types de retour

Le tableau ci-dessous montre les mappages de types entre SQL et Python. Ces mappages s’appliquent généralement à la fois aux arguments transmis aux UDF de Python et aux valeurs renvoyées par les UDF.

Type SQL

Type Python

Remarques

NUMBER

int ou decimal.Decimal

Si l’échelle du type NUMBER est 0, le type Python int est utilisé. Sinon, le type decimal.Decimal est utilisé.

FLOAT

float

Les opérations en virgule flottante peuvent présenter de petites erreurs d’arrondi, qui peuvent s’accumuler, notamment lorsque les fonctions d’agrégation traitent un grand nombre de lignes. Les erreurs d’arrondi peuvent varier à chaque exécution de la requête si les lignes sont traitées dans un ordre différent. Pour plus d’informations, voir Types de données numériques : Flottant.

VARCHAR

str

BINARY

bytes

BOOLEAN

bool

DATE

datetime.date

TIME

datetime.time

Bien que Snowflake puisse stocker des valeurs temporelles avec une précision de l’ordre de la nanoseconde, le type datetime.time n’offre qu’une précision de l’ordre de la milliseconde. La conversion entre les types de données Snowflake et Python peut réduire la précision effective à des millisecondes.

TIMESTAMP_LTZ

datetime.datetime

Utilisez le fuseau horaire local pour convertir l’heure interne UTC en heure locale « naïve ». Requiert le type de retour datetime « naïf ».

TIMESTAMP_NTZ

datetime.datetime

Convertissez directement en datetime « naïf ». Requiert le type de retour datetime « naïf ».

TIMESTAMP_TZ

datetime.datetime

Convertissez en datetime « sensible » avec des informations sur le fuseau horaire. Requiert le type de retour datetime « sensible ».

VARIANT

dict, list, int, float, str, ou bool

Chaque ligne de variante est convertie dynamiquement en un type Python pour les arguments et vice versa pour les valeurs de retour. Les entrées de type chaîne doivent être explicitement converties en SQL, par exemple en utilisant TO_VARIANT('foo'). D’autres types ne nécessitent pas de conversion explicite. Les types suivants sont convertis en chaînes plutôt qu’en types Python natifs : décimal, binaire, date, heure, timestamp_ltz, timestamp_ntz, timestamp_tz. Lorsqu’un type de données Python est converti en VARIANT, s’il y a des données décimales Python intégrées, celles-ci seront converties en chaîne dans le VARIANT.

OBJECT

dict

Lorsqu’un type de données Python est converti en OBJECT, s’il y a des données décimales Python intégrées, celles-ci seront converties en chaîne dans le OBJECT.

ARRAY

list

Lorsqu’un type de données Python est converti en ARRAY, s’il y a des données décimales Python intégrées, celles-ci seront converties en chaîne dans le ARRAY.

GEOGRAPHY

dict

Formate la géographie sous forme de GeoJSON, puis la convertit en un dict Python.

Valeurs et fuseaux horaires TIMESTAMP_LTZ

Une UDF Python est largement isolée de l’environnement dans laquelle elle est appelée. Cependant, le fuseau horaire est hérité de l’environnement d’appel. Si la session de l’appelant a défini un fuseau horaire par défaut avant d’appeler l’UDF Python, alors l’UDF Python a le même fuseau horaire par défaut. Pour plus d’informations sur les fuseaux horaires, voir TIMEZONE.

Valeurs NULL

Pour tous les types Snowflake, à l’exception de Variant, un argument SQL NULL à une UDF Python se traduit par la valeur None Python et une valeur None Python retournée se traduit à nouveau par une valeur NULL en SQL.

Une valeur de type Variant peut être : SQL NULL ou un VARIANT JSON null. Pour des informations sur les données VARIANT NULL Snowflake, voir Valeurs NULL.

  • Un VARIANT JSON null est traduit en une valeur None en Python.

  • Une valeur NULL SQL est traduite en un objet Python, qui possède l’attribut is_sql_null.

Pour un exemple, voir Traitement des NULL dans les UDFs Python.

Concevoir des UDFs Python qui respectent les contraintes imposées par Snowflake

Pour assurer la stabilité dans l’environnement Snowflake, Snowflake impose les contraintes suivantes aux UDFs Python. Sauf indication contraire, ces limitations sont appliquées lorsque l’UDF est exécutée, et non lors de sa création.

La formation de modèles de machine learning (ML) peut parfois être très exigeante en ressources. Les entrepôts optimisés pour Snowpark sont un type d’entrepôt virtuel Snowflake qui peut être utilisé pour les charges de travail qui nécessitent une grande quantité de mémoire et de ressources de calcul. Pour des informations sur les modèles de machine learning et sur Snowpark Python, voir Formation de modèles de machine learning avec Snowpark Python.

Mémoire

Évitez de consommer trop de mémoire.

  • Les grandes valeurs de données peuvent consommer une grande quantité de mémoire.

  • Une profondeur de pile excessive peut consommer une grande quantité de mémoire.

Les UDFs renvoient une erreur si elles consomment trop de mémoire. La limite spécifique est sujette à changement.

Si des UDFs échouent parce qu’elles consomment trop de mémoire, envisagez d’utiliser Entrepôts optimisés pour Snowpark.

Durée

Évitez les algorithmes qui prennent beaucoup de temps par appel.

Si une UDF prend trop de temps pour se terminer, Snowflake arrête l’instruction SQL et renvoie une erreur à l’utilisateur. Cela limite l’impact et le coût d’erreurs telles que les boucles infinies.

Conception du module

Lorsqu’une instruction SQL appelle votre UDF Python, Snowflake appelle une fonction Python que vous avez écrite. Votre fonction Python est appelée « fonction de gestionnaire » ou « gestionnaire » en abrégé. Le gestionnaire est une fonction mise en œuvre dans un module fourni par l’utilisateur.

Comme toute fonction Python, votre fonction doit être déclarée comme faisant partie d’un module.

Le gestionnaire est appelé une fois pour chaque ligne transmise à l’UDF Python. Le module qui contient la fonction n’est pas réimporté pour chaque ligne. Snowflake peut appeler la fonction de gestion du même module plus d’une fois.

Pour optimiser l’exécution de votre code, Snowflake part du principe que l’initialisation peut être lente, alors que l’exécution de la fonction du gestionnaire est rapide. Snowflake fixe un délai plus long pour l’exécution de l’initialisation (y compris le temps de chargement de votre UDF et le temps nécessaire à l’initialisation du module) que pour l’exécution du gestionnaire (le temps d’appel de votre gestionnaire avec une ligne d’entrée).

Des informations supplémentaires sur la conception du module se trouvent dans Création d’UDFs Python.

Optimisation de l’initialisation et contrôle de l’état global dans des UDFs scalaires

La plupart des UDFs scalaires doivent suivre les directives ci-dessous :

  • Si vous devez initialiser un état partagé qui ne change pas d’une ligne à l’autre, initialisez-le dans le module plutôt que dans la fonction du gestionnaire.

  • Rédigez votre fonction de gestionnaire de manière à la sécuriser des threads.

  • Évitez de stocker et de partager l’état dynamique entre les lignes.

Si vos UDF ne peuvent pas suivre ces directives, sachez que Snowflake s’attend à ce que les UDFs scalaires soient traitées indépendamment. S’appuyer sur l’état partagé entre les invocations peut entraîner un comportement inattendu, car le système peut traiter les lignes dans n’importe quel ordre et répartir ces invocations sur plusieurs instances. En outre, il peut y avoir plusieurs exécutions de la même fonction de gestionnaire dans le même interpréteur Python sur plusieurs threads.

Les UDFs doivent éviter de s’appuyer sur un état partagé entre les appels vers la fonction du gestionnaire. Cependant, il existe deux situations dans lesquelles vous pourriez vouloir qu’une UDF stocke un état partagé :

  • Du code qui contient une logique d’initialisation coûteuse que vous ne voulez pas répéter pour chaque ligne.

  • Du code qui exploite l’état partagé entre les lignes, comme un cache.

Lorsqu’il est nécessaire de maintenir un état global qui sera partagé entre les invocations de gestionnaires, vous devez protéger l’état global contre les courses de données en utilisant les primitives de synchronisation décrites dans threading - Thread-based parallelism.

Optimisation de l’échelle et des performances

Utiliser l’API par lots Python avec les bibliothèques de Data Science

Lorsque votre code utilisera des bibliothèques de machine learning ou de Data Science, utilisez le l’API par lots UDF Python. Avec l’API lot, vous pouvez définir des fonctions Python qui reçoivent des lignes d’entrée par lots sur lesquels ces bibliothèques sont optimisées pour fonctionner.

Pour plus d’informations, voir API par lots UDF Python.

Écrire des gestionnaires UDF à thread unique

Écrivez des gestionnaires UDF qui sont à thread unique. Snowflake se chargera de partitionner les données et de mettre à l’échelle les UDF sur les ressources informatiques de l’entrepôt virtuel.

Placer l’initialisation coûteuse dans le module

Placez le code d’initialisation coûteuse dans la portée du module. Là, il sera exécuté une fois lors de l’initialisation de l’UDF. Évitez de réexécuter le code d’initialisation coûteuse à chaque invocation du gestionnaire d’UDF.

Gestion des erreurs

Une fonction Python utilisée comme UDF peut utiliser les techniques normales de gestion des exceptions de Python pour récupérer les erreurs dans la fonction.

Si une exception se produit à l’intérieur de la fonction et n’est pas récupérée par celle-ci, Snowflake génère une erreur qui inclut la trace de la pile pour l’exception.

Vous pouvez explicitement lancer une erreur sans la capturer afin de terminer la requête et de produire une erreur SQL. Par exemple :

if (x < 0):
  raise ValueError("x must be non-negative.");

Lors du débogage, vous pouvez inclure des valeurs dans le texte du message d’erreur SQL. Pour ce faire, placez un corps de fonction Python entier dans un bloc try-catch ; ajoutez des valeurs d’argument au message de l’erreur capturée ; et lancez une erreur avec le message étendu. Pour éviter de révéler des données sensibles, supprimez les valeurs des arguments avant déploiement dans un environnement de production.

Respecter les bonnes pratiques de sécurité

  • Votre fonction (et toute fonction de bibliothèque que vous appelez) doit se comporter comme une fonction pure, agissant uniquement sur les données qu’elle reçoit et renvoyant une valeur basée sur ces données, sans provoquer d’effets secondaires. Votre code ne doit pas tenter d’affecter l’état du système sous-jacent, si ce n’est en consommant une quantité raisonnable de mémoire et de temps processeur.

  • Les UDFs Python sont exécutées dans un moteur restreint. Ni votre code ni le code des fonctions de la bibliothèque que vous utilisez ne doit effectuer d’appels système interdits, notamment :

    • Contrôle des processus. Par exemple, vous ne pouvez pas faire bifurquer un processus. (Toutefois, vous pouvez utiliser plusieurs fils).

    • Accès au système de fichiers.

      À l’exception des cas suivants, les UDFs Python ne doivent pas lire ou écrire de fichiers :

      • Les UDFs Python peuvent lire les fichiers spécifiés dans la clause imports de la commande CREATE FUNCTION.

        Pour plus d’informations, voir Lecture de fichiers avec un gestionnaire d’UDF. Pour un exemple, voir Chargement d’un fichier d’une zone de préparation dans une UDF Python.

      • Les UDFs Python peuvent écrire des fichiers, tels que des fichiers journaux, dans le répertoire /tmp.

        Chaque requête dispose de son propre système de fichiers sauvegardés en mémoire dans lequel son propre /tmp est stocké, de sorte que des requêtes différentes ne peuvent pas avoir de conflits de noms de fichiers. Cependant, des conflits au sein d’une requête sont possibles si une seule requête appelle plusieurs UDF et que ces UDFs essaient d’écrire dans le même nom de fichier. De plus, comme les UDFs Python peut s’exécuter en parallèle dans des processus de travail distincts, vous devez être prudent lorsque vous écrivez dans le répertoire /tmp.

        Pour plus d’informations sur l’écriture de fichiers, voir Écriture de fichiers avec un gestionnaire d’UDF. Pour un exemple, voir Décompression d’un fichier en zone de préparation.

    • Accès au réseau.

      Note

      Comme votre code ne peut pas accéder au réseau directement ou indirectement, vous ne pouvez pas utiliser le code du connecteur Python de Snowflake pour accéder à la base de données. Votre UDF ne peut pas elle-même agir comme un client de Snowflake.