【问题标题】:Bind the instance argument of a descriptor method to the calling object instance将描述符方法的实例参数绑定到调用对象实例
【发布时间】:2014-12-19 22:00:34
【问题描述】:

在描述符中,__get____set__ 的第二个参数绑定到调用对象实例(__get__ 的第三个参数绑定到调用所有者类对象):

class Desc():
    def __get__(self,instance,owner): 
        print("I was called by",str(instance),"and am owned by",str(owner))
        return self

class Test():
    desc = Desc()

t = Test()
t.desc

我将如何创建一个装饰器来将另一个描述符方法(__get____set____delete__ 除外)的第二个参数绑定到实例对象?

示例(只是一个示例;不是我真正想要做的事情):

class Length(object):
    '''Descriptor used to manage a basic unit system for length'''
    conversion = {'inches':1,'centimeters':2.54,'feet':1/12,'meters':2.54/100}
    def __set__(self,instance,length):
        '''length argument is a tuple of (magnitude,unit)'''
        instance.__value = length[0]
        instance.__units = length[1]
    def __get__(self,instance,owner):
        return self
    @MagicalDecoratorOfTruth
    def get_in(self, instance, unit): #second argument is bound to instance object
        '''Returns the value converted to the requested units'''
        return instance.__value * (self.conversion[units] / self.conversion[instance.__units])

class Circle(object):
    diameter = Length()
    def __init__(self,diameter,units):
        Circle.diameter.__set__((diameter,units))

c = Circle(12,'inches')
assert c.diameter.get_in('feet') == 1
c.diameter = (1,'meters')
assert c.diameter.get_in('centimeters') == 100

我考虑尝试的一种方法是用装饰器包装get_in 方法。使用 @classmethod 装饰器可以完成类似的操作,其中类方法的第一个参数绑定到类对象而不是类实例对象:

class Test():
    @classmethod
    def myclassmethod(klass):
        pass

t = Test()
t.myclassmethod()

但是,我不确定如何将其应用于上述情况。

避免整个问题的一种方法是将实例对象显式传递给描述符方法:

c = Circle(12,'inches')
assert c.diameter.get_in(c,'feet') == 1
c.diameter = (1,'meters')
assert c.diameter.get_in(c,'centimeters') == 100

但是,这似乎违反了 D.R.Y.,而且很难启动。

【问题讨论】:

  • 你的问题是?
  • 我认为有很多混乱正在发生。如果您使用__get__ 只是像这样返回self,您将如何实际访问instance.__valueinstance.__units 中的数据?通常,对于描述符,您希望将其设置为使用 x.d = foox.ddel x.d 三种语法模式。
  • 您可以在实际的Circle 类中使用__set__ 看到这个问题,这不起作用。相反,您想将其更改为self.diameter = (diameter,units),因为self 指的是Circle 实例,它将自动传递到__set__Length。 (尝试手动传递它实际上会导致错误,与魔法装饰器问题无关)。
  • 例如,当您的代码的用户写Circle.diameter 的那一刻,然后繁荣,这是一个值。它不应该是帮助处理值的类对象(即它应该Length的实例)。因此,如果您考虑编写 some_circle.diameter.get_in('feet'),您应该能够在心理上自动将其替换为 (12, 'inches').get_in('feet'),您可以清楚地看到这是没有意义的。
  • 描述符方法的绑定行为由calling 类的元类提供(type,如果你在 Py 2.* 中继承自 object)。如果您想要不同的行为(包括其他方法的特殊绑定),您将需要实现自己的自定义元类(无疑是子类化 type,但仍然有点令人担忧)并将其应用于需要它的 calling classes(直接或通过继承)。除非您正在开发一个非常雄心勃勃的全新框架,否则不推荐...

标签: python


【解决方案1】:

Descriptor 协议中为这类事情留下了一个钩子——即当从类级别访问 Descriptor 对象时,instance 的值将是None

从相反的方向考虑这一点很有用。让我们从Circle开始:

class Circle(object):
    diameter = Length()
    def __init__(self, diameter, units):
        self.diameter = (diameter, units)

请注意,我没有尝试手动调用 __set__ 或从类级别调用事物(例如,通过直接从 Circle 调用)——我只是按预期使用描述符,只需设置一个值。

现在,对于描述符,几乎所有内容都相同。我清理了转换dict 的代码样式。

但是对于__get__,我会为instance == None 添加额外的检查。每当访问Circle.diameter 时都会出现这种情况,而不是c.diameter 用于某些cCircle 的实例。确保您对差异感到满意。

class Length(object):
    conversion = {'inches':1.0,
                  'centimeters':2.54,
                  'feet':1.0/12,
                  'meters':2.54/100}

    def __set__(self, instance, length):
        instance.__value = length[0]
        instance.__units = length[1]

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return (instance.__value, instance.__units)

    def get_in(self, instance, units):
        c_factor = self.conversion[units] / self.conversion[instance.__units]
        return (c_factor * instance.__value, units)

现在,我们可以获取实际存在于.diameter 内部的Length 实例......但前提是我们访问挂在Circle 之外的.diameter(类本身),而不是该类的任何实例。

# This works and prints the conversion for `c`.
c = Circle(12, 'inches')
Circle.diameter.get_in(c, 'feet')

# This won't work because you short-circuit as soon as you type `c.diameter`
c.diameter.get_in('feet')

避免需要离开实例的一个选项是猴子补丁一个利用__class__属性的函数:

class Circle(object):
    diameter = Length()
    def __init__(self, diameter, units):
        self.diameter = (diameter, units)
        self.convert = lambda attr, units: (
            getattr(self.__class__, attr).get_in(self, units)
        )

现在实例c 可以这样工作:

>>> c.convert('diameter', 'feet')
(1.0, 'feet')

您可以改为将 convert 定义为实例方法(例如,使用通常的 self 第一个参数),或者您可以使用装饰器或元类等来实现。

但归根结底,您仍然需要非常小心。从表面上看,这看起来很有吸引力,但实际上您在对象之间添加了很多耦合。从表面上看,您可能会将对单位转换的关注与对象对“成为一个圆”的关注分开——但实际上,您正在增加其他程序员必须解决的复杂层。而且你正在将你的班级与这个特定的描述符结合起来。如果有人在重构中确定直径转换作为一个完全在Circle 对象之外的函数更好,他们现在突然不得不担心在重构时准确计算Length 的所有移动部分。

归根结底,您还必须问这能为您带来什么。据我所知,在您的示例中,除了能够将转换计算作为所谓“流畅界面”设计风格的一部分的非常小的便利性之外,它什么也不买……例如副作用和函数调用看起来只是属性访问。

就个人而言,我不喜欢这种语法。我宁愿使用像

这样的风格
convert(c.diameter, 'feet')

Circle.diameter.convert('feet')

像第一个版本这样的函数通常存在于模块级别,它们可以概括为它们将要操作的类型。可以扩展它们以更轻松地处理新类型(如果您希望对函数进行继承,可以将它们封装到它们自己单独的类中)。通常,它们也更容易测试,因为调用它们所需的机器要少得多,并且测试模拟对象可以更简单。事实上,在 Python 这样的动态类型语言中,允许 convert 这样的函数基于鸭子类型工作通常是该语言的主要优点之一。

这并不是说一种方式肯定比另一种更好。一个好的设计师可以在这两种方法中找到优点。一个糟糕的设计师可能会把这两种方法弄得一团糟。但总的来说,我发现当 Python 的这些特殊的角落被用来解决普通的、常规的问题时,往往会导致混乱。

【讨论】:

  • 在您的帮助下,我能够想出一些比我开始时有巨大改进的东西。请参阅下面的答案。
【解决方案2】:

感谢 prpl.mnky.dshwshr 的帮助,我能够极大地改进整个方法(并在此过程中学到了很多关于描述符的知识)。

class Measurement():
    '''A basic measurement'''
    def __new__(klass,measurement=None,cls_attr=None,inst_attr=None,conversion_dict=None):
        '''Optionally provide a unit conversion dictionary.'''
        if conversion_dict is not None:
            klass.conversion_dict = conversion_dict
        return super().__new__(klass)
    def __init__(self,measurement=None,cls_attr=None,inst_attr=None,conversion_dict=None):
        '''If object is acting as a descriptor, the name of class and 
        instance attributes associated with descriptor data are stored 
        in the object instance. If object is not acting as a descriptor,
        measurement data is stored in the object instance.'''
        if cls_attr is None and inst_attr is None and measurement is not None:
            #not acting as a descriptor
            self.__measurement = measurement
        elif cls_attr is not None and inst_attr is not None and measurement is None:
            #acting as a descriptor
            self.__cls_attr = cls_attr
            self.__inst_attr = inst_attr
            #make sure class and instance attributes don't share a name
            if cls_attr == inst_attr:
                raise ValueError('Class and Instance attribute names cannot be the same.')
        else:
            raise ValueError('BOTH or NEITHER the class and instance attribute name must be or not be provided. If they are not provided, a measurement argument is required.')
    ##Descriptor only methods
    def __get__(self,instance,owner):
        '''The measurement is returned; the descriptor itself is 
        returned when no instance supplied'''
        if instance is not None:
            return getattr(instance,self.__inst_attr)
        else:
            return self
    def __set__(self,instance,measurement):
        '''The measurement argument is stored in inst_attr field of instance'''
        setattr(instance,self.__inst_attr,measurement)
    ##Other methods
    def get_in(self,units,instance=None):
        '''The magnitude of the measurement in the target units'''
        #If Measurement is not acting as a descriptor, convert stored measurement data
        try:
            return convert( self.__measurement,
                            units,
                            self.conversion_dict
                            )
        except AttributeError:
            pass
        #If Measurement is acting as a descriptor, convert associated instance data
        try:
            return convert( getattr(instance,self.__inst_attr),
                            units,
                            getattr(type(instance),self.__cls_attr).conversion_dict
                            )
        except Exception:
            raise
    def to_tuple(self,instance=None):
        try:
            return self.__measurement
        except AttributeError:
            pass
        return getattr(instance,self.inst_attr)

class Length(Measurement):
        conversion_dict =   {
                            'inches':1,
                            'centimeters':2.54,
                            'feet':1/12,
                            'meters':2.54/100
                            }

class Mass(Measurement):
        conversion_dict =   {
                            'grams':1,
                            'pounds':453.592,
                            'ounces':453.592/16,
                            'slugs':453.592*32.1740486,
                            'kilograms':1000
                            }

def convert(measurement, units, dimension_conversion = None):
    '''Returns the magnitude converted to the requested units
    using the conversion dictionary in the provide dimension_conversion 
    object, or using the provided dimension_conversion dictionary.
    The dimension_conversion argument can be either one.'''
    #If a Measurement object is provided get measurement tuple
    if isinstance(measurement,Measurement):
        #And if no conversion dictionary, use the one in measurement object
        if dimension_conversion is None:
            dimension_conversion = measurement.conversion_dict
        measurement = measurement.to_tuple()    
    #Use the dimension member [2] of measurement tuple for conversion if it's there
    if dimension_conversion is None:
        try:
            dimension_conversion = measurement[2]
        except IndexError:
            pass
    #Get designated conversion dictionary 
    try:
        conversion_dict = dimension_conversion.conversion_dict
    except AttributeError:
        conversion_dict = dimension_conversion
    #Get magnitude and units from measurement tuple
    try:
        meas_mag = measurement[0]
        meas_units = measurement[1]
    except (IndexError,TypeError):
        raise TypeError('measurement argument should be indexed type with magnitude in measurement[0], units in measurement[1]') from None
    #Finally perform and return the conversion
    try:
        return meas_mag * (conversion_dict[units] / conversion_dict[meas_units])
    except IndexError:
        raise IndexError('Starting and ending units must appear in dimension conversion dictionary.') from None

class Circle():
    diameter = Length(cls_attr='diameter',inst_attr='_diameter')
    def __init__(self,diameter):
        self.diameter = diameter

class Car():
    mass = Mass(cls_attr='mass',inst_attr='_mass')
    def __init__(self,mass):
        self.mass = mass

c = Circle((12,'inches'))
assert convert(c.diameter,'feet',Length) == 1
assert Circle.diameter.get_in('feet',c) == 1
assert c.diameter == (12,'inches')
d = Circle((100,'centimeters',Length))
assert convert(d.diameter,'meters') == 1
assert Circle.diameter.get_in('meters',d) == 1
assert d.diameter == (100,'centimeters',Length)
x = Length((12,'inches'))
assert x.get_in('feet') == 1
assert convert(x,'feet') == 1

【讨论】:

  • 我不敢相信有人赞成这个。天哪,这是一个废话。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多