19 votes

Quand et pourquoi pourrais-je attribuer une instance d'une classe de descripteur à un attribut de classe en Python plutôt que d'utiliser une propriété?

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 ?

10voto

XORcist Points 2385

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.

5voto

gahcep Points 1789

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.

0voto

Jesse Salazar Points 317

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')

-1voto

Andreas Jung Points 1

@property ne vous permet pas de définir des méthodes setter et getter dédiées en même temps. Si une méthode getter est "suffisante", utilisez @property sinon vous avez besoin de property().

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X