Si vous développez en Python avec PythonNet, vous avez peut-être été confronté aux problèmes de l'implémentation des Interfaces .Net, voici une solution
#DynamoBIM #Revit #PythonNet #NET #Interface
#AutodeskExpertElite #AutodeskCommunity
if this article is not in your language, use the Google Translate
widget ⬈ (bottom of page for Mobile version ⬇)
Introduction
Dans cet article, nous allons explorer comment utiliser une interface de
Classe .Net avec PythonNet
- une interface ?
En .NET, une interface est un contrat que les classes doivent respecter en implémentant ses membres (méthodes, propriétés, événements). Cela permet de créer des architectures flexibles et modifiables sans dépendre directement d'une classe spécifique
-
Différence entre IronPython et PythonNet
Critère | IronPython (DLR) | PythonNet (CPython + .NET) |
---|---|---|
Basé sur | DLR (Dynamic Language Runtime) | CPython (interpréteur Python standard) |
Interopérabilité avec .NET | Excel (intégration native via CLR) | Très bonne (accès direct via clr en CPython) |
Interopérabilité avec les objets COM | Excellente (grâce à la prise en charge dynamique du DLR) | Possible, mais nécessite parfois des conversions de types explicites |
Gestion des objets dynamiques | Très naturelle et intuitive (support direct du DLR) | Plus limitée, demande souvent des déclarations explicites et des conversions de types |
Utilisation des bibliothèques CPython | Non (pas de support pour les modules C comme NumPy, SciPy, etc.) | Oui (support complet des modules CPython comme NumPy) |
Limitation par le GIL | Non (pas de Global Interpreter Lock) | Oui (limité par le GIL de CPython) |
Multithreading | Véritable exécution parallèle (pas de GIL) | Concurrence limitée à cause du GIL |
Interopérabilité avec les types statiquement typés | Excellente (gestion par le CLR) | Excellente (intégration directe des types .NET) |
Interopérabilité avec les bibliothèques .NET dynamiques | Très fluide grâce au DLR | Moins fluide pour les objets dynamiques, meilleure pour les types statiques |
Performance en multithreading | Très bonne (pas de GIL) |
Limité par le GIL, sauf si multiprocessing est utilisé
|
Gestion de la mémoire | Gérée par le garbage collector du CLR | Gérée par le garbage collector de CPython |
Explications CLR / DLR:
- CLR est le runtime de base utilisé pour tous les langages .NET, gérant principalement des langages statiquement typés et offrant des services comme la gestion de la mémoire, la gestion des exceptions, et la sécurité.
-
DLR est une couche supplémentaire au-dessus du CLR, conçue
spécifiquement pour prendre en charge les langages dynamiques et
permettre des opérations dynamiques, comme la modification des
objets et l'exécution de méthodes à l'exécution.
Création d'un Objet .Net
(avec héritage d'une Interface)
- Avec IronPython, une instance d'une classe qui hérite d'un type .NET est un objet .NET. Cela signifie que bien que l'objet soit créé à partir d'une classe définie dans IronPython, il est traité comme un objet .NET dans le runtime .NET (CLR)
class Temperature(System.IComparable):
def __init__(self, init_value):
self.__t = System.Int32(init_value)
def CompareTo(self, obj):
if obj is None:
return System.Int32(1)
else:
return self.Celsius.CompareTo(obj.Celsius)
@property
def Celsius(self):
return self.__t
@Celsius.setter
def Celsius(self, value: System.Int32):
self.__t = System.Int32(value)
- Avec PythonNet, il faut rajouter une syntaxe spécifique
__namespace__
- Lorsque vous héritez de types .NET en Python, si vous utilisez/surchargez la méthode d'initialisation
__init__
, vous devez appeler explicitement le constructeur de base en utilisant
super().__init__(.....)
(surtout valable avec PythonNet3).
class Temperature(System.IComparable):
__namespace__ = "My.Temperature"
def __init__(self, init_value):
self.__t = System.Int32(init_value)
super().__init__()
def CompareTo(self, obj):
if obj is None:
return System.Int32(1)
else:
return self.Celsius.CompareTo(obj.Celsius)
@property
def Celsius(self):
return self.__t
@Celsius.setter
def Celsius(self, value: System.Int32):
self.__t = System.Int32(value)
Autre Exemple de construction d'objet .Net
import sys
import clr
import System
from System.Collections.Generic import List
class ExampleClrClass(System.Object):
__namespace__ = "MyNameSpace_"
def __init__(self, lst):
super().__init__()
self.__lst = lst
@clr.clrmethod(List[System.Int32], [int])
def Multiply(self, x):
return List[System.Int32]([n * x for n in self.__lst])
@clr.clrproperty(List[System.Int32])
def GetList(self):
return List[System.Int32](self.__lst)
a = ExampleClrClass(list(range(10000)))
OUT = a
Le problème, c'est qu'à chaque appel de cette classe est créé :
Il en résulte que l'exécution d'un script python avec des types contenant
- 1 Espace de Nom + 1 Type
- avec PythonNet 2.5 → localisable dans l'Assembly Python.Runtime
- avec PythonNet 3 → localisable dans sys.modules
Il en résulte que l'exécution d'un script python avec des types contenant
__namespace__
, plus d'une fois, lèvera une exception Duplicate Type Name.
Solution "uuid" : un espace de nom différent a chaque appel
import clr
import sys
import System
#import net library
from System import Array, GC
from System.Collections.Generic import List, IList, Dictionary
#import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB
#import Revit APIUI namespace
clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Selection import *
my_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments)
import uuid
import os
import logging
from logging.handlers import RotatingFileHandler
import traceback
## Start create logger Object ##
logger = logging.getLogger("TestInterface_ByRename_Logger")
# set to DEBUG
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(funcName)s :: %(process)d :: %(message)s')
# create handler
file_handler = RotatingFileHandler(my_path + '\\TestInterface_ByRename_Logger.log', mode='a', maxBytes=100000, backupCount=1)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.disabled = False
class Custom_SelectionElem1(ISelectionFilter):
__namespace__ = "CustomSelectionElem_"+ str(uuid.uuid4())
def __init__(self, bic):
super().__init__()
self.bic = bic
def AllowElement(self, e):
if e.Category.Id == ElementId(self.bic):
return True
else:
return False
def AllowReference(self, ref, point):
return True
def get_Memory(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
GC.Collect()
memory = GC.GetTotalMemory(True) / 1024
type_name = ""
total_number_type = 0
for asm in System.AppDomain.CurrentDomain.GetAssemblies():
for atyp in asm.GetTypes():
if "Custom_SelectionElem" in atyp.Name :
total_number_type += 1
type_name = atyp.Name
for module_name in dict(sys.modules).keys():
if "Custom_SelectionElem" in module_name :
total_number_type += 1
type_name = module_name
logger.info((func.__name__, type_name, memory, total_number_type))
return result
return wrapper
@get_Memory
def Test_Rename_NameSpace():
a = Custom_SelectionElem1(BuiltInCategory.OST_AbutmentPiles)
Test_Rename_NameSpace()
for handler in logger.handlers:
if isinstance(handler, logging.FileHandler):
handler.close()
Ici la solution consiste à générer un nouvel
__namespace__
à chaque appel de la classe, avec la librairie uuid (UUID aléatoire a
chaque appel).
Cependant, lorsque nous lançons plusieurs fois ce script, nous observons
qu'à chaque lancement :
- le runtime PythonNet crée un nouveau Type en mémoire
-
la mémoire utilisée augmente en conséquence (même en fermant Dynamo
entre chaque lancement)
(prenant soin de supprimer les objets non référencés avec GC.Collect() )
Solution "__import__()" : re-utiliser le Type généré
Exemple :
import clr
import System
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference('RevitAPIUI')
import Autodesk.Revit.UI as RUI
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Selection import *
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument
class Custom_SelectionElem:
def __new__(cls, *args, **kwargs):
cls.args = args
# name of namespace : "CustomNameSpace" + "_" + shortUUID (8 characters)
# IMPORTANT NOTE Each time you modify this class, you must change the namesapce name.
cls.__namespace__ = "SelectionNameSpace_tEfYX0DHE"
try:
# 1. Try to import the module and class. If it already exists, you've probably already created
module_type = __import__(cls.__namespace__)
return module_type._InnerClassInterface(*cls.args)
except ImportError as ex:
print(ex)
# 2. If we get an ImportError, then the derivative class hasn't been built. Build it. Make sure to set the
# __namespace__ so that it is created with the appropriate .Net namespace.
class _InnerClassInterface(ISelectionFilter):
__namespace__ = cls.__namespace__
#
def __init__(self, bic):
super().__init__()
self.bic = bic
def AllowElement(self, e):
if e.Category.Id == ElementId(self.bic):
return True
else:
return False
def AllowReference(self, ref, point):
return True
return _InnerClassInterface(*cls.args)
for i in range(3):
RUI.TaskDialog.Show("Select", f"Select a door {i+1}/3")
ref = uidoc.Selection.PickObject(ObjectType.Element, Custom_SelectionElem(BuiltInCategory.OST_Doors))
print(ref.ElementId)
Cette fois, la solution est de créer un Type de Classe (avec Namespace) une
seule fois et de réutilisé celui lors des appels suivants.
Pour cela, j'utilise ici :
-
la méthode
__new__()
afin de pouvoir construire et retourner un objet -
la méthode
__import__()
pour essayer d'importer le namespace (module) pendant l'exécution
|
Note pour le nommage du
cls.__namespace__
:- je recommande un nom unique avec éventuellement un short UUID
- à chaque fois que vous modifierez cette classe, il faudra changer cette variable (d'où le short UUID ) sinon vous allez charge les anciens types/modules (jusqu'au redémarrage de l'application hôte)
Compatibilité de cette méthode :
- Net Framework 4.x
- Revit / Civil 2022 - CPython3/PythonNet2.5 : Oui
- Revit / Civil 2023 - CPython3/PythonNet2.5 : Oui
-
Revit / Civil 2024 (Dynamo 2.19.3) -
CPython3/PythonNet2.5 : NON (comptabilité incomplète, probablement dû au ciblage du Framework
NetStandard 2.0 avec PythonNet2.5 dans un environnement Framework
4.8)
- Net Core 8+
- Revit / Civil 2025 - CPython3/PythonNet2.5 : Oui
- Revit / Civil 2025+ - PythonNet3 : Oui
Pour ceux qui travaillent avec IronPython3 et qui souhaitent basculer
vers PythonNet3 ultérieurement, cette méthode est compatible avec
IronPython3
Cas des Interfaces de Classe .Net avec des paramètres out
Les méthodes .NET qui ont des paramètres
out
au
sein des Interfaces de classe .Net, leurs valeurs doivent être
retournées dans un tuple comprenant :- la valeur de retour de la méthode
- la valeur de retour du / des valeur(s) modifiée(s) des paramètres out
Attention cette syntaxe ne fonctionne qu'avec PythonNet3,
n'essayez pas avec CPython3/PythonNet2 sous peine de faire crasher
l'application hôte.
Exemple :
import clr
import System
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference('RevitAPIUI')
import Autodesk.Revit.UI as RUI
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Selection import *
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument
#
class Custom_FamilyOption:
def __new__(cls, *args, **kwargs):
cls.args = args
# name of namespace : "CustomNameSpace" + "_" + shortUUID (8 characters)
# IMPORTANT NOTE Each time you modify this class, you must change the namesapce name.
cls.__namespace__ = "FamilyOptionNameSpace_tEfYX0DHE"
try:
# 1. Try to import the module and class. If it already exists, you've probably already created it once, so you can use it.
module_type = __import__(cls.__namespace__)
return module_type._InnerClassInterface(*cls.args)
except ImportError as ex:
print(ex)
# 2. If we get an ImportError, then the derivative class hasn't been built. Build it. Make sure to set the
# __namespace__ so that it is created with the appropriate .Net namespace.
class _InnerClassInterface(IFamilyLoadOptions) :
__namespace__ = cls.__namespace__
def __init__(self):
super().__init__()
def OnFamilyFound(self, familyInUse, _overwriteParameterValues):
overwriteParameterValues = True
return (True, overwriteParameterValues)
def OnSharedFamilyFound(self, sharedFamily, familyInUse, source, _overwriteParameterValues):
overwriteParameterValues = True
return (True, overwriteParameterValues)
return _InnerClassInterface(*cls.args)
#
def loadFamily(path):
TransactionManager.Instance.EnsureInTransaction(doc)
opts = Custom_FamilyOption()
dummyFamily = None
loadFamily = doc.LoadFamily(path, opts, dummyFamily)
TransactionManager.Instance.TransactionTaskDone()
for path in IN[0] : loadFamily(path)
Note avec cette syntaxe, nous pouvons redéfinir notre classe
Température avec des setter et getter, ce qui donne :
À mesure que PythonNet continue de s'améliorer, notamment avec la version 3,
nous pouvons nous espérer à une simplification de ces processus à l'avenir.
class Temperature:
def __new__(cls, *args, **kwargs):
cls.args = args
cls.__namespace__ = "Temperature_tEfYX0DHE"
try:
module_type = __import__(cls.__namespace__)
return module_type._Temperature(*cls.args)
except ImportError as ex:
class _Temperature(System.IComparable):
__namespace__ = cls.__namespace__
def __init__(self, init_value):
self.__t = System.Int32(init_value)
super().__init__()
def CompareTo(self, obj):
if obj is None:
return System.Int32(1)
else:
return self.Celsius.CompareTo(obj.Celsius)
# define getter and setter
def get_Celsius(self):
return self.__t
def set_Celsius(self, value: System.Int32):
self.__t = System.Int32(value)
Celsius = clr.clrproperty(System.Int32, get_Celsius, set_Celsius)
# add custom method
@clr.clrmethod(System.Int32)
def ToFahrenheit(self):
return (self.Celsius * 1.8) + 32
@clr.clrmethod(System.Int32, System.Int32)
def Multiply(self, x):
return self.Celsius * x
return _Temperature(*cls.args)
"La simplicité est la sophistication ultime."
— Steve Jobs
0 commentaires:
Enregistrer un commentaire