Когда и почему я могу назначить экземпляр класса дескриптора атрибуту класса в Python, а не использовать свойство?

Я знаю, что свойство - это дескриптор, но есть ли конкретные примеры, когда использование класса дескриптора может быть более выгодным, питонным или предоставлять некоторые преимущества по сравнению с использованием @property на метод функции?

4 ответа

Решение

Лучшая инкапсуляция и возможность повторного использования: класс дескриптора может иметь настраиваемые атрибуты, установленные при создании экземпляра. Иногда полезно ограничивать данные таким образом, вместо того, чтобы беспокоиться о том, что они будут установлены или перезаписаны владельцем дескриптора.

Позвольте мне привести цитату из отличного видео EuroPython 2012 "Открытие дескрипторов":

Как выбрать между дескрипторами и свойствами:

  • Свойства работают лучше всего, когда они знают о классе
  • Дескрипторы являются более общими, часто могут применяться к любому классу
  • Используйте дескрипторы, если поведение отличается для классов и экземпляров
  • Свойства синтаксического сахара

Также обратите внимание, что вы можете использовать __slots__ с дескрипторами.

Что касается вариантов использования дескрипторов, вы можете захотеть повторно использовать свойства в несвязанных между собой классах .

Обратите внимание, что аналогию с термометром и калькулятором можно решить многими другими способами — это всего лишь несовершенный пример.

Вот пример:

      ###################################
######## Using Descriptors ########
###################################

# Example:
#     Thermometer class wants to have two properties, celsius and farenheit.
#     Thermometer class tells the Celsius and Farenheit descriptors it has a '_celsius' var, which can be manipulated.
#     Celsius/Farenheit descriptor saves the name '_celsius' so it can manipulate it later.
#     Thermometer.celsius and Thermometer.farenheit both use the '_celsius' instance variable under the hood.
#     When one is set, the other is inherently up to date.
#
#     Now you want to make some Calculator class that also needs to do celsius/farenheit conversions.
#     A calculator is not a thermometer, so class inheritance does nothing for you.
#     Luckily, you can re-use these descriptors in the totally unrelated Calculator class.

# Descriptor base class without hard-coded instance variable names.
# Subclasses store the name of some variable in their owner, and modify it directly.
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())

# Descriptor. Notice there are no hard-coded instance variable names.
# Use the commented lines for faster performance, but with hard-coded owner class variables names.
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)

# Descriptor. Notice there are no hard-coded instance variable names.
# Use the commented lines for faster performance, but with hard-coded owner class variables names.
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

# This class only has one instance variable allowed, _celsius
# The 'celsius' attribute is a descriptor which manipulates the '_celsius' instance variable
# The 'farenheit' attribute also manipulates the '_celsius' instance variable
class Thermometer(object):
    __slots__ = ['_celsius']
    def __init__(self, celsius=0.0) -> None:
        self._celsius= float(celsius)
    
    # Both descriptors are instantiated as attributes of this class
    # They will both manipulate a single instance variable, defined in __slots__
    celsius= Celsius(__slots__[0])
    farenheit= FarenheitFromCelsius(__slots__[0])

# This class also wants to have farenheit/celsius properties for some reason
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)
    
    # We can re-use descriptors!
    celsius= Celsius(__slots__[0])
    farenheit= FarenheitFromCelsius(__slots__[0])

##################################
######## Using Properties ########
##################################

# This class also only uses one instance variable, _celsius
class Thermometer_Properties_NoSlots( object ):
    # __slots__ = ['_celsius'] => Blows up the size, without slots
    def __init__(self, celsius=0.0) -> None:
        self._celsius= float(celsius)
        
    # farenheit property
    def fget( self ):
        return self.celsius * 9 / 5 + 32
    def fset( self, value ):
        self.celsius= (float(value)-32) * 5 / 9
    farenheit= property( fget, fset )

    # celsius property
    def cset( self, value ):
        self._celsius= float(value)
    def cget( self ):
        return self._celsius
    celsius= property( cget, cset, doc="Celsius temperature")

# performance testing
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

# main function
if __name__ == "__main__":
    thermometer0 = Thermometer()
    thermometer1 = Thermometer(50)
    thermometer2 = Thermometer(100)
    thermometerWithProperties = Thermometer_Properties_NoSlots()

    # performance: descriptors are better if you use the commented lines in the descriptor classes
    # however: Calculator and Thermometer MUST name their var _celsius if hard-coding, rather than using 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)))
    
    # reset the thermometers (after testing performance)
    thermometer0.celsius = 0
    thermometerWithProperties.celsius = 0
    
    # memory: only 40 flat bytes since we use __slots__
    import pympler.asizeof as asizeof
    print(f'thermometer0: {asizeof.asizeof(thermometer0)} bytes')
    print(f'thermometerWithProperties: {asizeof.asizeof(thermometerWithProperties)} bytes')

    # print results    
    print(f'thermometer0: {thermometer0.celsius} Celsius = {thermometer0.farenheit} Farenheit')
    print(f'thermometer1: {thermometer1.celsius} Celsius = {thermometer1.farenheit} Farenheit')
    print(f'thermometer2: {thermometer2.celsius} Celsius = {thermometer2.farenheit} Farenheit')
    print(f'thermometerWithProperties: {thermometerWithProperties.celsius} Celsius = {thermometerWithProperties.farenheit} Farenheit')

@property не позволяет вам определять выделенные методы setter и getter одновременно. Если метод получения "достаточно хорош", используйте @property, в противном случае вам понадобится property().

Другие вопросы по тегам