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