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

19

我知道属性本身就是描述符,但是有没有具体的例子表明使用描述符类可能比在方法函数上使用 @property 更加优越、符合Python规范或者提供一些好处呢?

4个回答

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

谢谢!所以你可以将描述符的职责从实例化描述符的类中分离出来?太棒了! - mkelley33
1
可能性实际上是无限的 ;) 想想像延迟加载这样的东西:设置一个你想要在首次访问描述符时稍后加载的字符串,然后再也不必担心它了。或者从所有者类中删除混乱,并将其放置在逻辑上属于它的地方。 - XORcist
@möter 设置一个稍后要加载的字符串 你可以使用 @property 来完成相同的操作,不是吗? - Piotr Dobrogost
@PiotrDobrogost 当然可以。但是那至少需要另外一条语句,对吧? - XORcist
我们可以将描述符应用于实例方法吗? - variable
描述符可以返回一个可调用对象,包括实例方法,然后可以被调用。https://docs.python.org/3/howto/descriptor.html#descriptor-protocol - XORcist

5

让我引用 EuroPython 2012 的优秀视频 "Discovering Descriptors"

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

  • 当属性了解类时,属性的表现最佳
  • 描述符更通用,通常适用于任何类
  • 如果行为对类和实例有所不同,请使用描述符
  • 属性是语法糖

此外,请注意,您可以将 __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:
        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')

为了避免硬编码的名称,您可以使用__set_name__和命名约定(例如,在属性名称前加下划线)来解决。 - STerliakov
@STerliakov的__set_name__只接收(self, owner, name)参数,因此两个描述符无法共享同一个底层变量(即CelsiusFarenheit如何同时获取/设置上面的Thermometer._celsius属性)。 - undefined

-1

@property 不允许您同时定义专用的 setter 和 getter 方法。 如果 getter 方法“足够好”,则使用 @property,否则需要使用 property()。


3
为什么我会更喜欢使用属性而不是类似于“@my_method_name.setter”这样的东西?另外,这如何适用于自定义描述符类的概念? - mkelley33

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接