Когда и почему я могу назначить экземпляр класса дескриптора атрибуту класса в 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().