2014-12-19 75 views
1

在描述符中,__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方法與裝飾。

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

t = Test() 
t.myclassmethod() 

不過,我不能確定如何將其應用到:​​類似的東西使用@classmethod裝飾,其中一類方法的第一個參數被綁定到類對象,而不是類的實例對象來完成上面的情況。

,以避免整個問題將是實例對象傳遞給明確的描述方法的一種方式:

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

而你的問題是什麼? –

+0

我覺得有很多困惑正在進行。如果你使用'__get__'來僅僅這樣返回'self',你將如何實際訪問'instance .__ value'或'instance .__ units'中的數據?通常,使用描述符,您需要將其設置爲使用三種語法模式「x.d = foo」,「x.d」或「del x.d」。 – ely

+0

你可以在實際的'Circle'類中使用'__set__'來看這個問題,這是行不通的。相反,您想將其更改爲'self.diameter =(diameter,units)',因爲'self'指的是'Circle'實例,它會自動傳遞到'__set__'中作爲'Length'。 (嘗試手動傳遞它會導致錯誤,與魔術裝飾器問題無關)。 – ely

回答

2

在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實例的保持......但只有當我們訪問.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第一個參數),或者你庫侖d做它與裝飾,或元類,...等

但在這一天結束時,你仍然需要非常小心。從表面上看,這看起來很有吸引力,但實際上你在對象之間添加了很多耦合。它表面上看起來像是將單元轉換的擔憂從對象關於「成爲一個圓圈」的擔憂中分離出來 - 但實際上,您正在添加其他程序員必須理清的複雜層次。而你正在將你的課程與這個特殊的描述符結合起來。如果某人在重構中確定直徑轉換更好地完成了整個Circle對象之外的功能,那麼他們現在突然不得不擔心在重構時準確計算了所有Length的移動部分。

在這一天結束時,你也必須要問這是什麼給你買。據我所知,在你的例子中,除了作爲所謂的「流暢界面」設計風格的一部分能夠誘導轉換計算的非常小的便利之外,它不會購買任何東西。副作用和函數調用看起來就像是屬性訪問一樣。

個人而言,我不喜歡這種語法。我寧願使用樣式像

convert(c.diameter, 'feet') 

Circle.diameter.convert('feet') 

喜歡的第一個版本的功能通常住在模塊級,他們可以在他們將要操作的類型一概而論。它們可以擴展爲更容易處理新類型(如果你想繼承函數,你可以將它們封裝到它們自己的獨立類中)。通常,它們也更容易測試,因爲調用它們所需的機器少得多,並且測試模擬對象可能更簡單。事實上,在像Python這樣的動態類型語言中,允許像convert這樣的函數基於鴨子打字工作通常是該語言的主要優點之一。

這並不是說一種方法是絕對優於其他。一個好的設計師可以在兩種方法中找到優點。一個糟糕的設計師可能會弄糊塗兩種方法。但是總的來說,我發現當Python的這些特殊的角落被用來解決普通的常見問題時,通常會導致混亂。

+0

這太棒了,我學到了很多! –

+0

在你的幫助下,我能夠想出一些與我剛開始時相比有了巨大改進的東西。請參閱下面的答案。 –

0

由於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 
相關問題