何时以及为什么我可以将描述符类的实例分配给 Python 中的类属性而不是使用属性?

问题描述 投票:0回答:3

我知道属性是一个描述符,但是有没有具体的例子说明何时使用描述符类可能更有利,Pythonic,或者比在方法函数上使用

@property
提供一些好处?

python descriptor
3个回答
10
投票

更好的封装和可重用性:描述符类可以在实例化时设置自定义属性。有时以这种方式限制数据很有用,而不必担心它会被描述符所有者设置或覆盖。


5
投票

让我引用 EuroPython 2012 精彩视频“发现描述符”

如何在描述符和属性之间进行选择:

  • 当物业了解班级时,他们的工作效果最好
  • 描述符更通用,通常可以适用于任何类
  • 如果类和实例的行为不同,请使用描述符
  • 属性是语法糖

另外,请注意,您可以将

__slots__
与描述符一起使用。


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

# 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:
        # equivalent to self._celsius= float(celsius)
        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 has one instance variable allowed, _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')
© www.soinside.com 2019 - 2024. All rights reserved.