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

Bind the instance argument of a descriptor method to the calling object instance

在第二个参数描述符,和一__get__绑定到__set__调用对象的实例(和第三参数绑定到__get__是一类对象的调用的老板):

1
2
3
4
5
6
7
8
9
10
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__)对象的实例?

实例(例如,不只是一个东西,我真的想)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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类似做装饰,在第一类参数的研究方法是结合到该类的对象类的实例对象来代替:

1
2
3
4
5
6
7
class Test():
    @classmethod
    def myclassmethod(klass):
        pass

t = Test()
t.myclassmethod()

然而,我不确定这如何应用到上述的情况。

一种方式,以避免整个问题的实例,对象是通过明确的描述方法。

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

然而,这似乎真的violate d.r.y.启动,和丑陋的。


对于这类事情,描述符协议中还有一个钩子——也就是说,当从类级别访问描述符对象时,EDOCX1的值(0)将是None

在相反的方向思考这个是很有用的。我们从Circle开始:

1
2
3
4
class Circle(object):
    diameter = Length()
    def __init__(self, diameter, units):
        self.diameter = (diameter, units)

注意,我并没有试图手动调用__set__或从类级别调用东西(例如直接从Circle调用东西),而是按预期使用描述符,只需设置一个值。

现在,对于描述符来说,实际上一切都是一样的。我清理了用于转换dict的代码样式。

但对于__get__,每当instance == None时,我都会加上额外的支票。每当访问Circle.diameter时,就会出现这种情况,而有些cCircle的实例,则会出现c.diameter的情况。确保你对差异感到满意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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的内部。但只有当我们访问挂起EDOCX1(类本身)的.diameter,而不是该类的任何实例时。

1
2
3
4
5
6
# 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')

避免需要离开实例的一个选项是monkey patch一个使用__class__属性的函数:

1
2
3
4
5
6
7
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可以这样工作:

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

您可以将convert定义为实例方法(例如,使用通常的self第一个参数),也可以使用decorator或元类来实现它,…等。

但是在一天结束的时候,你仍然需要非常小心。从表面上看,这看起来很吸引人,但实际上你在你的对象之间增加了很多耦合。从表面上看,它可能看起来像是你正在把对单位转换的关注从对对象"成为一个圆"的关注中分离出来——但实际上你在增加其他程序员必须解决的复杂层次。你要把你的类和这个特殊的描述符结合起来。如果有人在重构中确定直径转换作为一个完全在Circle对象之外的函数更好,那么当他们进行重构时,他们现在突然不得不担心准确地计算Length的所有运动部件。

在一天结束的时候,你还必须问这会给你带来什么。据我所知,在您的示例中,除了能够作为所谓的"fluent interface"设计风格的一部分诱导转换计算的非常小的便利性外,它什么都不买…例如,副作用和函数调用似乎只是属性访问。

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

1
convert(c.diameter, 'feet')

1
Circle.diameter.convert('feet')

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

这并不是说一种方法肯定比另一种更好。一个好的设计师可以在任何一种方法中找到优点。一个糟糕的设计师可能会把这两种方法搞得一团糟。但总的来说,我发现当这些异常的python角用于解决非异常的常规问题时,常常会导致混乱。


多亏了prpl.mnky.dshwshr的帮助,我能够极大地改进整个方法(并在这个过程中了解了很多描述符)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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