将描述符方法的实例参数绑定到调用对象实例

时间:2014-12-19 22:00:34

标签: python

在描述符中,__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.,并且真的很难启动。

2 个答案:

答案 0 :(得分:2)

在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的{​​{1}}是c的{​​{1}}。确保你对这种差异感到满意。

Circle

现在,我们可以获取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) 内部的实际Length实例...但仅当我们访问.diameter时才会停止.diameter(类本身)而不是该类的任何实例。

Circle

避免需要离开实例的一个选项是使用# 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的这些特殊角落被用来解决普通的常规问题时,它往往会导致混乱。

答案 1 :(得分:1)

感谢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