Création d’UDFs Python

Cette rubrique montre comment créer et installer une UDF Python (fonction définie par l’utilisateur).

Dans ce chapitre :

Écrire le code Python

Écriture le module et la fonction Python

Écrivez un module qui suit les spécifications ci-dessous :

  • Définissez le module. Un module est un fichier contenant des définitions et des instructions Python.

  • Définissez une fonction à l’intérieur du module.

  • Si la méthode accepte des arguments, chaque argument doit être l’un des types de données spécifiés dans la colonne Python Data Type de la table SQL-Python Type Mappings.

    Les arguments de fonction sont liés par leur position, et non par leur nom. Le premier argument transmis à l’UDF est le premier argument reçu par la méthode Python.

  • Spécifiez une valeur de retour appropriée. Comme une UDF Python doit être une fonction scalaire, elle doit renvoyer une valeur à chaque fois qu’elle est appelée. Le type de la valeur de retour doit être l’un des types de données spécifiés dans la colonne Python Data Type de la table SQL-Python Type Mappings. Le type de la valeur de retour doit être compatible avec le type de données SQL spécifié dans la clause RETURNS de l’instruction CREATE FUNCTION.

  • Votre module peut contenir plus d’une fonction. La fonction qui est appelée par Snowflake peut appeler d’autres fonctions dans le même module ou dans d’autres modules.

  • Votre fonction (et toute fonction appelée par votre fonction) doit respecter les contraintes Snowflake imposées pour les UDFs Python.

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. Pour plus d’informations, voir API par lots UDF Python.

Lecture et écriture de fichiers avec un gestionnaire d’UDF

Vous pouvez lire et écrire des fichiers avec le code du gestionnaire d’UDF. Pour le faire en toute sécurité au sein du moteur restreint dans lequel Snowflake exécute des UDFs, suivez les directives décrites ici.

Lecture de fichiers avec un gestionnaire d’UDF

Un gestionnaire d’UDF peut lire des fichiers qui ont été téléchargés sur une zone de préparation Snowflake. La zone de préparation hébergeant le fichier doit être lisible par le propriétaire de l’UDF.

Lorsque vous spécifiez l’emplacement de zone de préparation d’un fichier dans la clause IMPORTS de la FUNCTION CREATE, Snowflake copie le fichier en zone de préparation dans un répertoire d’importation disponible spécifiquement pour les UDF. Votre code de gestionnaire peut lire le fichier à partir de là.

Notez que Snowflake copie tous les fichiers importés, potentiellement de plusieurs zones de préparation, dans un seul répertoire d’importation. Pour cette raison, les noms des fichiers spécifiés dans la clause IMPORTS doivent être distincts les uns par rapport aux autres.

Pour un exemple de code, voir Chargement d’un fichier d’une zone de préparation dans une UDF Python dans cette rubrique.

Pour lire un fichier avec le code du gestionnaire d’UDF :

  1. Copiez le fichier dans une zone de préparation de Snowflake.

    Vous pouvez utiliser la commande PUT pour télécharger des fichiers depuis un répertoire local sur un ordinateur client ; pour plus d’informations, voir PUT. Pour des informations plus générales sur le chargement de fichiers dans une zone de préparation, voir Vue d’ensemble du chargement des données.

  2. Lorsque vous créez l’UDF à l’aide de la FUNCTION CREATE, spécifiez l’emplacement du fichier dans la clause IMPORTS.

    Le code de l’exemple suivant spécifie un fichier file.txt dans une zone de préparation appelée my_stage.

    create or replace function my_udf()
       ...
       imports=('@my_stage/file.txt')
       ...
    
    Copy
  3. Dans votre code de gestionnaire, lisez le fichier du répertoire d’importation.

    Snowflake copie le fichier en zone de préparation dans le répertoire d’importation de l’UDF. Vous pouvez récupérer l’emplacement du répertoire en utilisant l’option système snowflake_import_directory.

    En code Python, vous pouvez récupérer l’emplacement du répertoire en utilisant la méthode Python sys._xoptions, comme dans l’exemple suivant :

    IMPORT_DIRECTORY_NAME = "snowflake_import_directory"
    import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]
    
    def compute():
       with open(import_dir + 'file.txt', 'r') as file:
          return file.read()
    
    Copy

Écriture de fichiers avec un gestionnaire d’UDF

Un gestionnaire d’UDF peut écrire des fichiers dans un répertoire /tmp créé pour la requête appelant l’UDF.

N’oubliez pas qu’un répertoire /tmp est réservé à une seule requête d’appel, alors que plusieurs processus de travail Python peuvent être exécutés en même temps. Pour éviter les collisions, vous devez vous assurer que l’accès au répertoire /tmp est synchronisé avec les autres processus de travail Python ou que les noms des fichiers écrits dans /tmp sont uniques.

Pour un exemple de code, voir Décompression d’un fichier en zone de préparation dans cette rubrique.

Le code de l’exemple suivant écrit l’entrée text dans le répertoire /tmp, en ajoutant l’ID du processus de la fonction pour garantir l’unicité de l’emplacement du fichier.

def func(text):
   # Ensure the file name's uniqueness by appending the function's process ID.
   file_path = '/tmp/content' + str(os.getpid())
   with open(file_path, "w") as file:
      file.write(text)
Copy

Création de la fonction dans Snowflake

Vous devez exécuter une instruction CREATE FUNCTION pour spécifier :

  • Le nom de la fonction SQL à utiliser.

  • Le nom de la fonction Python à appeler lorsque l’UDF Python est appelée.

Le nom de l’UDF ne doit pas nécessairement correspondre au nom de la fonction du gestionnaire écrit en Python. L’instruction CREATE FUNCTION associe le nom de l’UDF à la fonction Python.

Pour choisir un nom pour l’UDF :

  • Suivez les règles de Identificateurs d’objet.

  • Choisissez un nom qui soit unique, ou suivez les règles pour Surcharge.

    Important

    Contrairement à la surcharge pour les UDFs SQL, qui distingue les fonctions en fonction du nombre et des types de données des arguments, les UDFs Python distinguent les fonctions selon uniquement le nombre d’arguments.

Les arguments de fonction sont liés par leur position, et non par leur nom. Le premier argument transmis à l’UDF est le premier argument reçu par la méthode Python.

Pour plus d’informations sur les types de données des arguments, voir Mappages des types de données SQL-Python.

UDFs avec du code en ligne vs. UDFs avec du code téléchargé à partir d’une zone de préparation

Le code d’une UDF Python peut être spécifié de l’une des manières suivantes :

  • Téléchargé à partir d’une zone de préparation : l’instruction CREATE FUNCTION spécifie l’emplacement d’un code source Python existant dans une zone de préparation.

  • En ligne : l’instruction CREATE FUNCTION spécifie le code source Python.

Création d’une UDF Python en ligne

Pour une UDF en ligne, vous fournissez le code source Python dans le cadre de l’instruction CREATE FUNCTION.

Par exemple, l’instruction suivante crée une UDF Python en ligne qui ajoute +1 à un nombre entier donné :

create or replace function addone(i int)
returns int
language python
runtime_version = '3.8'
handler = 'addone_py'
as
$$
def addone_py(i):
  return i+1
$$;
Copy

Le code source Python est spécifié dans la clause AS. Le code source peut être délimité soit de guillemets simples, soit d’une paire de signes de dollar ($$). L’utilisation du double signe dollar est généralement plus facile si le code source contient des guillemets simples intégrés.

Appelez l’UDF :

select addone(10);
Copy

Voici la sortie :

+------------+
| ADDONE(10) |
|------------|
|         11 |
+------------+
Copy

Le code source Python peut contenir plus d’un module, et plus d’une fonction dans un module, donc la clause HANDLER spécifie le module et la fonction à appeler.

Une UDF Python en ligne peut appeler du code dans les modules qui sont inclus dans la clause IMPORTS.

Pour plus de détails sur la syntaxe de l’instruction CREATE FUNCTION, voir CREATE FUNCTION.

Pour plus d’exemples, voir Exemples d’UDF Python en ligne.

Création d’une UDF Python avec du code téléchargé à partir d’une zone de préparation

Les instructions suivantes créent une simple UDF Python en utilisant du code téléchargé à partir d’une zone de préparation. La zone de préparation hébergeant le fichier doit être lisible par le propriétaire de l’UDF. En outre, les fichiers ZIP doivent être autonomes et ne pas dépendre de scripts d’installation supplémentaires pour être exécutés.

Créez un fichier Python nommé sleepy.py qui contient votre code source :

def snore(n):   # return a series of n snores
    result = []
    for a in range(n):
        result.append("Zzz")
    return result
Copy

Lancez SnowSQL (client CLI) et utilisez la commande PUT pour copier le fichier du système de fichiers local vers une zone de préparation utilisateur par défaut, nommée @~. (La commande PUT ne peut pas être exécutée par la GUI Snowflake.)

put
file:///Users/Me/sleepy.py
@~/
auto_compress = false
overwrite = true
;
Copy

Si vous supprimez ou renommez le fichier, vous ne pouvez plus appeler l’UDF. Si vous devez mettre à jour votre fichier, faites-le pendant qu’aucun appel à l’UDF ne soit possible. Si l’ancien fichier .jar se trouve toujours dans la zone de préparation, la commande PUT doit inclure la clause OVERWRITE=TRUE.

Créez l’UDF. Le gestionnaire spécifie le module et la fonction.

create or replace function dream(i int)
returns variant
language python
runtime_version = '3.8'
handler = 'sleepy.snore'
imports = ('@~/sleepy.py')
Copy

Appelez l’UDF :

select dream(3);

+----------+
| DREAM(3) |
|----------|
| [        |
|   "Zzz", |
|   "Zzz", |
|   "Zzz"  |
| ]        |
+----------+
Copy

Spécification de fichiers d’importation multiples

Voici un exemple de la façon de spécifier plusieurs fichiers d’importation.

create or replace function multiple_import_files(s string)
returns string
language python
runtime_version=3.8
imports=('@python_udf_dep/bar/python_imports_a.zip', '@python_udf_dep/foo/python_imports_b.zip')
handler='compute'
as
$$
def compute(s):
  return s
$$;
Copy

Note

Les noms des fichiers d’importation spécifiés doivent être différents. Par exemple, ceci ne fonctionnera pas : imports=('@python_udf_dep/bar/python_imports.zip', '@python_udf_dep/foo/python_imports.zip').

Accorder des privilèges sur la fonction

Pour qu’un rôle autre que le propriétaire de la fonction puisse appeler la fonction, le propriétaire doit accorder les privilèges appropriés au rôle.

Les instructions GRANT pour une UDF Python sont essentiellement identiques aux instructions GRANT pour d’autres UDFs, comme les UDFs JavaScript.

Par exemple :

GRANT USAGE ON FUNCTION my_python_udf(number, number) TO my_role;
Copy

Exemples

Utilisation d’un paquet importé dans une UDF Python en ligne

Une sélection de paquets tiers d’Anaconda est disponible. Pour plus d’informations, voir Utilisation de paquets tiers.

Note

Avant que vous puissiez utiliser les paquets fournis par Anaconda, l’administrateur de votre organisation Snowflake doit accepter les conditions de tiers de Snowflake. Pour plus d’informations, voir Prise en main.

Le code suivant montre comment importer des paquets et renvoyer leurs versions.

Créez l’UDF :

create or replace function py_udf()
returns variant
language python
runtime_version = 3.8
packages = ('numpy','pandas','xgboost==1.5.0')
handler = 'udf'
as $$
import numpy as np
import pandas as pd
import xgboost as xgb
def udf():
    return [np.__version__, pd.__version__, xgb.__version__]
$$;
Copy

Appelez l’UDF :

select py_udf();
Copy

Voici la sortie :

+-------------+
| PY_UDF()    |
|-------------|
| [           |
|   "1.19.2", |
|   "1.4.0",  |
|   "1.5.0"   |
| ]           |
+-------------+
Copy

Chargement d’un fichier d’une zone de préparation dans une UDF Python

Cet exemple montre comment importer des fichiers non codés dans une UDF Python à partir d’une zone de préparation. Le fichier ne sera lu qu’une seule fois pendant la création de l’UDF, et ne sera pas relu pendant l’exécution de l’UDF si la lecture du fichier a lieu en dehors du gestionnaire cible. Pour plus d’informations sur la lecture d’un fichier, voir Lecture de fichiers avec un gestionnaire d’UDF.

Note

Vous ne pouvez importer des fichiers qu’à partir du répertoire de premier niveau d’une zone de préparation, pas des sous-dossiers.

Créez l’UDF :

create or replace function my_udf()
  returns string
  language python
  runtime_version=3.8
  imports=('@my_stage/file.txt')
  handler='compute'
as
$$
import sys
import os

with open(os.path.join(sys._xoptions["snowflake_import_directory"], 'file.txt'), "r") as f:
    s = f.read()

def compute():
    return s
$$;
Copy

Décompression d’un fichier en zone de préparation

Vous pouvez stocker un fichier .zip dans une zone de préparation, puis le décompresser dans une UDF en utilisant le module Python zipfile.

Par exemple, vous pouvez télécharger un fichier .zip dans une zone de préparation, puis référencer le fichier .zip à son emplacement de zone de préparation dans la clause IMPORTS lorsque vous créez l’UDF. Au moment de l’exécution, Snowflake copiera le fichier en zone de préparation dans un répertoire d’importation à partir duquel votre code pourra y accéder.

Pour plus d’informations sur la lecture et l’écriture de fichiers, voir Lecture et écriture de fichiers avec un gestionnaire d’UDF.

Dans l’exemple suivant, le code de l’UDF utilise un modèle NLP pour découvrir les entités dans le texte. Le code renvoie un tableau de ces entités. Pour configurer le modèle NLP afin de traiter le texte, le code utilise d’abord le module zipfile pour extraire le fichier du modèle (en_core_web_sm-2.3.1) d’un fichier .zip. Le code utilise ensuite le module spaCy pour charger le modèle à partir du fichier.

Notez que le code écrit le contenu des fichiers extraits dans le répertoire /tmp créé pour la requête qui appelle cette fonction. Le code utilise des verrous de fichier pour garantir que l’extraction est synchronisée entre les processus de travail Python ; de cette façon, le contenu n’est décompressé qu’une seule fois. Pour plus d’informations sur l’écriture de fichiers, voir Écriture de fichiers avec un gestionnaire d’UDF.

Pour en savoir plus sur le module zipfile, consultez la référence zipfile. Pour en savoir plus sur le module spaCy, consultez la documentation de l”API spaCy.

Créez l’UDF :

create or replace function py_spacy(str string)
   returns array
   language python
   runtime_version = 3.8
   handler = 'func'
   packages = ('spacy')
   imports = ('@spacy_stage/spacy_en_core_web_sm.zip')
   as
$$
import fcntl
import os
import spacy
import sys
import threading
import zipfile

# File lock class for synchronizing write access to /tmp
class FileLock:
   def __enter__(self):
      self._lock = threading.Lock()
      self._lock.acquire()
      self._fd = open('/tmp/lockfile.LOCK', 'w+')
      fcntl.lockf(self._fd, fcntl.LOCK_EX)

   def __exit__(self, type, value, traceback):
      self._fd.close()
      self._lock.release()

# Get the location of the import directory. Snowflake sets the import
# directory location so code can retrieve the location via sys._xoptions.
IMPORT_DIRECTORY_NAME = "snowflake_import_directory"
import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]

# Get the path to the ZIP file and set the location to extract to.
zip_file_path = import_dir + "spacy_en_core_web_sm.zip"
extracted = '/tmp/en_core_web_sm'

# Extract the contents of the ZIP. This is done under the file lock
# to ensure that only one worker process unzips the contents.
with FileLock():
   if not os.path.isdir(extracted + '/en_core_web_sm/en_core_web_sm-2.3.1'):
      with zipfile.ZipFile(zip_file_path, 'r') as myzip:
         myzip.extractall(extracted)

# Load the model from the extracted file.
nlp = spacy.load(extracted + "/en_core_web_sm/en_core_web_sm-2.3.1")

def func(text):
   doc = nlp(text)
   result = []

   for ent in doc.ents:
      result.append((ent.text, ent.start_char, ent.end_char, ent.label_))
   return result
$$;
Copy

Traitement des NULL dans des UDFs Python

Le code suivant montre comment les valeurs NULL sont traitées. Pour plus d’informations, voir Valeurs NULL.

Créez l’UDF :

create or replace function py_udf_null(a variant)
returns string
language python
runtime_version = 3.8
handler = 'udf'
as $$

def udf(a):
    if not a:
        return 'JSON null'
    elif getattr(a, "is_sql_null", False):
        return 'SQL null'
    else:
        return 'not null'
$$;
Copy

Appelez l’UDF :

select py_udf_null(null);
select py_udf_null(parse_json('null'));
select py_udf_null(10);
Copy

Voici la sortie :

+-------------------+
| PY_UDF_NULL(NULL) |
|-------------------|
| SQL null          |
+-------------------+

+---------------------------------+
| PY_UDF_NULL(PARSE_JSON('NULL')) |
|---------------------------------|
| JSON null                       |
+---------------------------------+

+-----------------+
| PY_UDF_NULL(10) |
|-----------------|
| not null        |
+-----------------+
Copy