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
Introduction
- 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 |
- 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)
- 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()
__namespace__
à
chaque appel de la classe, avec la librairie uuid (UUID aléatoire a chaque
appel).- 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é
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)
-
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
|
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)
- 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
Cas des Interfaces de Classe .Net avec des paramètres out
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
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)