在Python中何时及为什么将描述符类的实例赋值给类属性,而不是使用属性?
我知道属性其实是一种描述符,但能不能给我一些具体的例子,说明在什么情况下使用描述符类会比在方法上用@property
更有优势、更符合Python的风格,或者能带来一些好处呢?
3 个回答
0
关于描述符的使用场景,你可能会发现自己想在一些没有关系的类中重复使用属性。
请注意,温度计/计算器的类比可以用很多其他方法来解决——这只是一个不太完美的例子。
下面是一个例子:
###################################
######## 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
# Descriptor. Notice we hard-coded self.name, but not owner variable names
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))
# Descriptor. Notice we hard-coded self.name, but not owner variable names
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)
# 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')
10
更好的封装和重用性:描述符类在创建时可以设置自定义属性。有时候,把数据这样封闭起来是很有用的,这样就不用担心这些数据会被描述符的拥有者修改或覆盖。