Compilation de code Python et l'utiliser dans Dynamo, c'est possible ?
#DynamoBIM #Python #pyc #pyd #compile #AutodeskExpertElite
#AutodeskCommunity 
if this article is not in your language, use the Google Translate
widget ⬈ (bottom of page for Mobile version ⬇)
Si vous développez en Python, vous savez probablement que vous pouvez
importer vos propres classes/modules Python sous forme de fichier py, mais
savez-vous qu'il existe des fichiers
pyc
et
pyd
?
Voyons cela.
1. Le classique : le module sous forme de fichier
.py
Avantages :
- Lisibilité absolue : C'est du texte brut, compréhensible par n'importe quel développeur (et éditable avec un simple bloc-notes)
-
Simplicité : aucun processus de compilation complexe n'est requis pour
tester son code.
- Débogage facile : en cas de plantage, pas besoin de recompiler.
Inconvénients :
-
Lenteur relative : le code doit être analysé et compilé en "bytecode" par
l'interpréteur à chaque lancement (si aucun fichier
.pycn'est disponible).
- Propriété intellectuelle : Si vous distribuez votre application sous forme de fichiers .py, n'importe qui peut lire votre code source (surtout si celui-ci contient des informations sensibles).
2. Le compilé bytecode : le module sous forme de fichier
.pyc
Présentation
Le "c" signifie compiled (compilé). Lorsqu'un module .py est importé pour
la première fois, Python le traduit dans un format intermédiaire appelé
bytecode et le sauvegarde dans un fichier
.pyc
(généralement caché dans un dossier
__pycache__). Le bytecode est une série d'instructions optimisées que la machine
virtuelle Python (CPython) peut exécuter beaucoup plus rapidement que du
texte brut.
Avantages :
-
Temps de chargement réduit : si vous utilisez des fichiers .py pour
charger des modules personnalisés, vous pouvez les remplacer par des
.pyc, ce qui accélère le démarrage du script.
- Légère obfuscation : le code n'est plus en texte clair, ce qui offre une (très) mince couche de protection contre la lecture directe par un novice.
Inconvénients :
-
Dépendance à la version de Python : un fichier
.pycgénéré par Python 3.9 pourrait ne pas fonctionner sur Python 3.11.
-
Ce n'est pas de la vraie sécurité : Des outils de "décompilation"
(comme uncompyle6 ou
PyLingual) peuvent facilement recréer un fichier .py presque identique à
partir d'un
.pyc.
Comment le générer ?
Vous pouvez forcer la génération de fichiers
.pyc
pour tout un projet en utilisant le module intégré compileall.
-
Exemple de génération de fichier
.pycà partir d'un nœud Python Dynamo.
Ici on récupère le contenu du code Python d'un nœud et on le compile en
.pyc
directement depuis Dynamo.
..
import clr
import System
from System.Collections.Generic import List
clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)
# Add Revit API References
clr.AddReference('RevitAPI')
clr.AddReference("RevitServices")
clr.AddReference("RevitNodes")
import RevitServices
import Revit
clr.ImportExtensions(Revit.GeometryConversion)
import Autodesk
import Autodesk.Revit.DB as DB
from Autodesk.Revit.DB import *
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
# Document reference
doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument
clr.AddReference('DynamoRevitDS')
import Dynamo
#reference dynamo core to update node values
clr.AddReference('DynamoCore')
from Dynamo.Graph import UpdateValueParams
#access to the current Dynamo instance and workspace
dynamoRevit = Dynamo.Applications.DynamoRevit()
engine = dynamoRevit.RevitDynamoModel.EngineController
currentWorkspace = dynamoRevit.RevitDynamoModel.CurrentWorkspace
model = dynamoRevit.RevitDynamoModel
import os
folder = IN[0]
name_pyc = IN[1]
py_path = f"{folder}\\{name_pyc}.py"
pyc_path = f"{folder}\\{name_pyc}.pyc"
nodeK = next((i for i in currentWorkspace.Nodes\
if i.Name == "Python Script | Module1"), None)
code_py = nodeK.Script
code_py = code_py.replace("\r","")
with open(py_path, "w", encoding='utf-8') as f:
f.write(code_py)
import py_compile
py_compile.compile(py_path, cfile=pyc_path)
os.remove(py_path)
OUT = folder
3. La DLL : Le fichier
.pyd
Présentation
Le fichier
.pyd
est une bibliothèque de liens dynamiques (DLL) spécifique à Windows (son
équivalent sous Linux/macOS est le fichier .so).
Ce n'est plus du Python : c'est du code compilé (généralement à partir de
C, C++ ou Rust) qui a été empaqueté pour être importable directement dans
un script Python comme n'importe quel autre module (ex: import
mon_module_pyd).
Avantages :
- Performances élevées : le code s'exécute à la vitesse du C. C'est idéal pour les calculs lourds, le traitement d'images ou l'intelligence artificielle.
- Protection du code source : le code est compilé en langage machine. Il est extrêmement difficile (voire impossible sans ingénierie inverse lourde) de retrouver le code source d'origine.
- Accès bas niveau : permet d'interagir directement avec le matériel ou des API système de manière très pointue.
Inconvénients :
-
Complexité de développement : il faut maîtriser d'autres langages
(C/C++) et comprendre l'API C de Python.
-
Pour compiler en
.pyd, il faut obligatoirement installer Python sur votre PC (même version que celle de Python dans Dynamo) ainsi que les Microsoft Visual C++ Build Tools
L'environnement Python intégré dans le nœud Dynamo est une version "allégée" et encapsulée, optimisée pour l'exécution. Elle ne contient pas de bibliothèques de développement.
-
Perte de portabilité : un
.pydcompilé pour Windows 64 bits ne fonctionnera ni sur Windows 32 bits, ni sur Linux, ni sur macOS. Il faut recompiler le module pour chaque architecture et système d'exploitation cible.
- Dépendance stricte à la version mineure de Python : un pyd compilé en 3.11 ne pourra pas être importé avec Python 3.12
Comment le générer ?
La création d'un
.pyd
est un processus de compilation. Voici les méthodes les plus courantes
:
En utilisant Cython (à ne pas confondre avec CPython) :
on écrit un code hybride Python/C (fichier .pyx), puis vous utilisez un
script
Note : dans le fichier pyx , vous pouvez utiliser le typage statique ( en utilisant le mot-clé cdef), cela accélérera la vitesse d'exécution.
En écrivant cdef int i ou cdef double x, vous donnez une instruction stricte au compilateur : "N'utilise pas un objet Python dynamique pour cette variable. Crée une vraie variable C directement dans la mémoire vive."
setup.py
(via setuptools) pour le compiler en .pyd.Note : dans le fichier pyx , vous pouvez utiliser le typage statique ( en utilisant le mot-clé cdef), cela accélérera la vitesse d'exécution.
En écrivant cdef int i ou cdef double x, vous donnez une instruction stricte au compilateur : "N'utilise pas un objet Python dynamique pour cette variable. Crée une vraie variable C directement dans la mémoire vive."
Si, pour des raisons de tests, votre code à compiler est déjà dans votre espace Dynamo (nœud Python), vous pouvez utiliser le module subprocess pour générer le pyd directement depuis Dynamo comme suit :
code pour générer le pyd
import clr
import System
from System.Collections.Generic import List
clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)
# Add Revit API References
clr.AddReference('RevitAPI')
clr.AddReference("RevitServices")
clr.AddReference("RevitNodes")
import RevitServices
import Revit
clr.ImportExtensions(Revit.GeometryConversion)
import Autodesk
import Autodesk.Revit.DB as DB
from Autodesk.Revit.DB import *
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
# Document reference
doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument
clr.AddReference('DynamoRevitDS')
import Dynamo
#reference dynamo core to update node values
clr.AddReference('DynamoCore')
from Dynamo.Graph import UpdateValueParams
#access to the current Dynamo instance and workspace
dynamoRevit = Dynamo.Applications.DynamoRevit()
engine = dynamoRevit.RevitDynamoModel.EngineController
currentWorkspace = dynamoRevit.RevitDynamoModel.CurrentWorkspace
model = dynamoRevit.RevitDynamoModel
import os
import subprocess
import sysconfig
# standard library path
sys.path.append(sysconfig.get_path("platstdlib"))
# site-package library path
sys.path.append(sysconfig.get_path("platlib"))
folder = IN[0]
name_pyd = IN[1]
py_path = f"{folder}\\{name_pyd}.pyx"
pyc_path = f"{folder}\\{name_pyd}.pyd"
setup_path = f"{folder}\\setup.py"
python_exe = r"C:\Users\sneep\AppData\Local\Programs\Python\Python311\python.exe"
# write the py file from a python node
nodeK = next((i for i in currentWorkspace.Nodes if i.Name == "Python Script | Module1"), None)
code_py = nodeK.Script
code_py = code_py.replace("\r","")
with open(py_path, "w", encoding='utf-8') as f:
f.write(code_py)
contenu_setup = f"""# Généré automatiquement via Dynamo
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("{name_pyd}.pyx")
)
"""
with open(setup_path, "w", encoding='utf-8') as f:
f.write(contenu_setup)
# compilation via Subprocess
commande = [python_exe, setup_path, "build_ext", "--inplace"]
try:
resultat = subprocess.run(
commande,
cwd=folder,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
shell=True
)
# return
if resultat.returncode == 0:
OUT = resultat.stdout,
else:
OUT = resultat.stderr,
except Exception as e:
OUT = str(e),
- Exemple de code à générer en pyd (non optimisé )
Ici on reste sur une syntaxe
CPython.
import sys
import clr
import System
import math
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
class Utils:
guid = "a8c3aa76-f731-4086-ae08-8cb41464e425"
def __name__(self):
return "Utils"
@staticmethod
def filter_points(flat_points, pt_center, radius):
inside_points = []
center_x, center_y, center_z = pt_center.X, pt_center.Y, pt_center.Z
r_square = radius ** 2
for p in flat_points:
x, y, z = p.X, p.Y, p.Z
dist_square = (x - center_x)**2 + (y - center_y)**2 + (z - center_z)**2
if dist_square <= r_square:
inside_points.append(p)
return inside_points
OUT = Utils
- Exemple de code à compiler en pyd (optimisé)
Dans ce 2ᵉ exemple, on est sur un langage
Cython, il s'agit d'un langage hybride (techniquement un "sur-ensemble" de
Python) créé spécifiquement pour servir de pont entre les deux mondes
(le C et le Python).
On a ajouté des déclarations de types statiques du C (cdef).
On a ajouté des déclarations de types statiques du C (cdef).
import sys
import clr
import System
import math
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
class Utils:
guid = "a8c3aa76-f731-4086-ae08-8cb41464e425"
def __name__(self):
return "Utils"
@staticmethod
def filter_points_fast(points, pt_center, double radius):
# variables declarations
cdef double center_x, center_y, center_z
cdef double r_square
cdef double x, y, z, dist_square
inside_points = []
center_x = pt_center.X
center_y = pt_center.Y
center_z = pt_center.Z
r_square = radius * radius
for p in points:
x = p.X
y = p.Y
z = p.Z
dist_square = (x - center_x)*(x - center_x) + (y - center_y)*(y - center_y) + (z - center_z)*(z - center_z)
if dist_square <= r_square:
inside_points.append(p)
return inside_points
OUT = Utils
Note : on pourrait également dans cet exemple passer directement les
valeurs numériques des points au lieu des objets ".Net" , ce qui
accélérerait encore plus l'exécution, notamment au niveau de la boucle
for
4. L'importation et l'utilisation
L'importation se fait de la même manière pour les 3 types de fichiers
(py, pyc, pyd) via le module sys
import clr
import System
import sys
import os
import importlib
import inspect
pyd_folder_path = IN[0] # the folder path of py/pyc/pyd file (NOT THE FILEPATH)
flat_points = IN[1]
pt_center = IN[2]
sys.path.append(pyd_folder_path)
import Utils2
# optionnal : reload the module if it has already been loaded
importlib.reload(Utils2)
from Utils2 import Utils
# optionnal : check the filepath of module (py, pyc, pyd)
pyd_file_path = inspect.getfile(Utils2)
OUT = Utils.filter_points(flat_points, pt_center, radius=4), pyd_file_path
4. Résultats
| Format du module | Fonction testée | Temps d'exécution moyen | Nombre de tests |
|---|---|---|---|
| pyc (Bytecode standard) |
test_from_pyc
|
75.186 ms | 4 |
| pyd (CPython/Cython non optimisé) |
test_from_pyd_not_optimised
|
56.692 ms | 4 |
| pyd (Cython optimisé) |
test_from_pyd_optimised
|
43.488 ms | 4 |
Soyez gentil, car tous ceux que vous rencontrez mènent un dur combat.
Ian Maclaren (John Watson)
Ian Maclaren (John Watson)







0 commentaires:
Enregistrer un commentaire