Je sais qu'une propriété est un descripteur, mais existe-t-il des exemples spécifiques où l'utilisation d'une classe descripteur pourrait être plus avantageuse, pythonique ou offrir certains avantages par rapport à l'utilisation de @property
sur une fonction méthode ?
Réponses
Trop de publicités?Meilleure encapsulation et réutilisabilité : Une classe de descripteur peut avoir des attributs personnalisés définis lors de l'instanciation. Parfois, il est utile de garder les données confinées de cette manière, au lieu de devoir s'inquiéter qu'elles soient définies ou écrasées sur le propriétaire du descripteur.
Permettez-moi de citer la grande vidéo EuroPython 2012 "Découverte des descripteurs" :
Comment choisir entre descripteurs et propriétés :
- Les propriétés fonctionnent mieux lorsqu'elles connaissent la classe
- Les descripteurs sont plus généraux, peuvent souvent s'appliquer à n'importe quelle classe
- Utilisez des descripteurs si le comportement est différent pour les classes et les instances
- Les propriétés sont du sucre syntaxique
De plus, veuillez noter que vous pouvez utiliser __slots__
avec les Descripteurs.
En ce qui concerne les cas d'utilisation du descripteur, vous pouvez vous retrouver à vouloir ré-utiliser des propriétés dans des classes qui ne sont pas liées.
Veuillez noter que l'analogie Thermomètre/Calculatrice peut être résolue de nombreuses autres façons -- juste un exemple imparfait.
Voici un exemple :
###################################
######## Utilisation des Descripteurs ########
###################################
# Exemple :
# La classe Thermomètre veut avoir deux propriétés, celsius et farenheit.
# La classe Thermomètre indique aux descripteurs Celsius et Farenheit qu'elle a une variable '_celsius', qui peut être manipulée.
# Le descripteur Celsius/Farenheit enregistre le nom '_celsius' pour pouvoir le manipuler plus tard.
# Thermomètre.celsius et Thermomètre.farenheit utilisent tous les deux la variable d'instance '_celsius' en interne.
# Lorsque l'un est défini, l'autre est automatiquement mis à jour.
#
# Maintenant, vous voulez créer une classe Calculatrice qui a également besoin d'effectuer des conversions celsius/farenheit.
# Une calculatrice n'est pas un thermomètre, donc l'héritage de classe ne vous sert à rien.
# Heureusement, vous pouvez ré-utiliser ces descripteurs dans la classe Calculatrice totalement non liée.
# Classe de base du descripteur sans noms de variables d'instance codés en dur.
# Les sous-classes stockent le nom d'une variable dans leur propriétaire et la modifient directement.
class TemperatureBase(object):
__slots__ = ['name']
def set_owner_var_name(self, var_name) -> None:
setattr(self, TemperatureBase.__slots__[0], var_name)
def get_owner_var_name(self) -> any:
return getattr(self, TemperatureBase.__slots__[0])
def set_instance_var_value(self, instance, value) -> None:
setattr(instance, self.get_owner_var_name(), value)
def get_instance_var_value(self, instance) -> any:
return getattr(instance, self.get_owner_var_name())
# Descripteur. Remarquez qu'il n'y a pas de noms codés en dur pour les variables d'instance.
# Utilisez les lignes commentées pour une performance plus rapide, mais avec des noms de variables de classe propriétaire codés en dur.
class Celsius(TemperatureBase):
__slots__ = []
def __init__(self, var_name) -> None:
super().set_owner_var_name(var_name)
#self.name = var_name
def __get__( self, instance, owner ) -> float:
return super().get_instance_var_value(instance)
#return instance._celsius
def __set__( self, instance, value ) -> None:
super().set_instance_var_value(instance, float(value))
#instance._celsius = float(value)
# Descripteur. Remarquez qu'il n'y a pas de noms codés en dur pour les variables d'instance.
# Utilisez les lignes commentées pour une performance plus rapide, mais avec des noms de variables de classe propriétaire codés en dur.
class FarenheitFromCelsius(TemperatureBase):
__slots__ = []
def __init__(self, var_name) -> None:
super().set_owner_var_name(var_name)
#self.name = var_name
def __get__( self, instance, owner ) -> float:
return super().get_instance_var_value(instance) * 9 / 5 + 32
#return instance._celsius * 9 / 5 + 32
def __set__( self, instance, value ) -> None:
super().set_instance_var_value(instance, (float(value)-32) * 5 / 9)
#instance._celsius = (float(value)-32) * 5 / 9
# Descripteur. Remarquez que nous avons codé en dur self.name, mais pas les noms de variables propre au propriétaire
class Celsius2(TemperatureBase):
__slots__ = []
def __init__(self, var_name) -> None:
self.name = var_name
def __get__( self, instance, type=None ) -> float:
return getattr(instance, self.name)
def __set__( self, instance, value ) -> None:
setattr(instance, self.name, float(value))
# Descripteur. Remarquez que nous avons codé en dur self.name, mais pas les noms de variables propre au propriétaire
class FarenheitFromCelsius2(TemperatureBase):
__slots__ = []
def __init__(self, var_name) -> None:
self.name = var_name
def __get__( self, instance, type=None ) -> float:
return getattr(instance, self.name) * 9 / 5 + 32
def __set__( self, instance, value ) -> None:
setattr(instance, self.name, (float(value)-32) * 5 / 9)
# Cette classe n'a qu'une seule variable d'instance autorisée, _celsius
# L'attribut 'celsius' est un descripteur qui manipule la variable d'instance '_celsius'
# L'attribut 'farenheit' manipule également la variable d'instance '_celsius'
class Thermometer(object):
__slots__ = ['_celsius']
def __init__(self, celsius=0.0) -> None:
self._celsius= float(celsius)
# Les deux descripteurs sont instanciés comme attributs de cette classe
# Ils manipuleront tous les deux une seule variable d'instance, définie dans __slots__
celsius= Celsius(__slots__[0])
farenheit= FarenheitFromCelsius(__slots__[0])
# Cette classe veut également avoir des propriétés farenheit/celsius pour une raison quelconque
class Calculator(object):
__slots__ = ['_celsius', '_meters', 'grams']
def __init__(self, value=0.0) -> None:
self._celsius= float(value)
self._meters = float(value)
self._grams = float(value)
# Nous pouvons ré-utiliser les descripteurs !
celsius= Celsius(__slots__[0])
farenheit= FarenheitFromCelsius(__slots__[0])
##################################
######## Utilisation de Propriétés ########
##################################
# Cette classe n'utilise également qu'une seule variable d'instance, _celsius
class Thermometer_Properties_NoSlots( object ):
# __slots__ = ['_celsius'] => Augmente la taille, sans slots
def __init__(self, celsius=0.0) -> None:
self._celsius= float(celsius)
# propriété farenheit
def fget( self ):
return self.celsius * 9 / 5 + 32
def fset( self, value ):
self.celsius= (float(value)-32) * 5 / 9
farenheit= property( fget, fset )
# propriété celsius
def cset( self, value ):
self._celsius= float(value)
def cget( self ):
return self._celsius
celsius= property( cget, cset, doc="Température en Celsius")
# test de performance
import random
def set_get_del_fn(thermometer):
def set_get_del():
thermometer.celsius = random.randint(0,100)
thermometer.farenheit
del thermometer._celsius
return set_get_del
# fonction principale
if __name__ == "__main__":
thermometer0 = Thermometer()
thermometer1 = Thermometer(50)
thermometer2 = Thermometer(100)
thermometerWithProperties = Thermometer_Properties_NoSlots()
# performance : les descripteurs sont meilleurs si vous utilisez les lignes commentées dans les classes de descripteurs
# cependant : Calculator et Thermometer DOIVENT nommer leur var _celsius s'ils codent en dur, plutôt que d'utiliser getattr/setattr
import timeit
print(min(timeit.repeat(set_get_del_fn(thermometer0), number=100000)))
print(min(timeit.repeat(set_get_del_fn(thermometerWithProperties), number=100000)))
# réinitialiser les thermomètres (après les tests de performance)
thermometer0.celsius = 0
thermometerWithProperties.celsius = 0
# mémoire : seulement 40 bytes plats puisque nous utilisons __slots__
import pympler.asizeof as asizeof
print(f'thermometer0: {asizeof.asizeof(thermometer0)} bytes')
print(f'thermometerWithProperties: {asizeof.asizeof(thermometerWithProperties)} bytes')
# afficher les résultats
print(f'thermometer0: {thermometer0.celsius} Celsius = {thermometer0.farenheit} Fahrenheit')
print(f'thermometer1: {thermometer1.celsius} Celsius = {thermometer1.farenheit} Fahrenheit')
print(f'thermometer2: {thermometer2.celsius} Celsius = {thermometer2.farenheit} Fahrenheit')
print(f'thermometerWithProperties: {thermometerWithProperties.celsius} Celsius = {thermometerWithProperties.farenheit} Fahrenheit')