3.3 使用特殊方法完成属性访问

    本节将介绍 3 个用于属性访问的标准函数:getattr()setattr()delattr()。此外,还可以用dir()函数来查看属性的名称。下一部分会介绍getattribute()函数的使用。

    关于属性,之前章节中介绍了如下的几种默认操作。

    • setattr()函数用于属性的创建和赋值。
    • getattr()函数可以用来做两件事。首先,如果属性已经被赋值,getattr()则不会被调用,直接返回属性值即可。其次,如果属性没有被赋值,那么将使用getattr()函数的返回值。如果找不到相关属性,要记得抛出AttributeError异常。
    • delattr()函数用于删除属性。
    • dir()函数用于返回属性名称列表。
    getattr()函数只是复杂逻辑中的一个小步骤而已,仅当属性未知的情况下它才会被使用。如果属性已知,这个函数将不会被使用。setattr()函数和delattr()函数没有内部的处理过程,也没有和其他函数逻辑有交互。 关于控制属性访问的设计,可以有很多选择。基于这3个基本的设计出发点:扩展、封装和创建,以下是具体描述。 - 扩展类。通过重写setattr()和delattr()函数使得它几乎是不可变的。也可以使用slots替换内部的dict对象。 - 封装类。提供对象(或对象集合)属性访问的代理实现。这可能需要完全重写和属性相关的那3个函数。 - 创建类并提供和特性功能一样的函数。使用这些方法来对特性逻辑集中处理。 - 创建延迟计算属性,仅当需要时才触发计算过程。对于一些属性,它的值可能来自文件、数据库或网络。这是getattr()函数的常见用法。 - 创建主动计算属性,其他属性更新时会相应地更新主动计算属性的值,这是通过重写setattr()函数实现的。 我们不必对以上各项逐一讨论。我们只会详细看一下其中两种最常用的:扩展和封装。我们会创建不可变对象并看一些其他有关实现提前属性的方式。 3.3.1 使用slots创建不可变对象 如果一个属性是不允许被赋值或创建的,就被称为不可变的。以下代码演示了我们所期望和Python的一种交互方式。
    >>> c= card21(1,'♠')
    >>> c.rank= 12
    Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
     File "<stdin>", line 30, in setattr
    TypeError: Cannot set rank
    >>> c.hack= 13
    Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
     File "<stdin>", line 31, in setattr
    AttributeError: 'Ace21Card' has no attribute 'hack'
    以上代码中,我们不能对当前对象属性的值进行修改。 需要相应对类的定义做两处修改。在以下代码中,我们仅关注与对象不可变相关的 3个部分。
    class BlackJackCard:
      """Abstract Superclass"""
      slots = ( 'rank', 'suit', 'hard', 'soft' )
      def init( self, rank, suit, hard, soft ):
        super().setattr( 'rank', rank )
        super().setattr( 'suit', suit )
        super().setattr( 'hard', hard )
        super().setattr( 'soft', soft )
      def str( self ):
        return "{0.rank}{0.suit}".format( self )
      def setattr( self, name, value ):
        raise AttributeError( "'{class.name}' has no
    attribute '{name}'".format( class= self.class, name= name
    ) )
    我们做了如下3处明显的修改。 - 把slots设为唯一被允许操作的属性。这会使得对象内部的dict对象不再有效并阻止对其他属性的访问。 - 在setattr()函数中,代码逻辑仅仅是抛出异常。 - 在init()函数中,调用了基类中setattr()实现,为了确保当类没有包含有效的setattr()函数时,属性依然可被正确赋值。 如果需要,也可以像如下代码绕过不可变对象。
    object.setattr(c, 'bad', 5)
    这会引发一个问题。“如何阻止恶意程序员绕过不可变对象?”这样的问题是愚蠢的。我们永远无法阻止恶意程序员的行为。另一个同样愚蠢的问题是,“为什么一些恶意程序员会试图绕过对象的不可变性?”当然,我们无法阻止恶意程序员做恶意的事情。 如果一个程序员不喜欢一个类的不可变性,他们可以修改它并移除重定义过的setattr()函数。一个类似的例子是:对hash()来说,不可变对象的目的是能够返回一致的值而非阻止程序员写糟糕的代码。
    不要误解slots

    slots 的主要目的是通过限制属性的数量来节约内存。

    3.3.2 使用tuple子类创建不可变对象

    我们也可以通过让Card特性成为tuple类的子类并重写getattr()函数来实现一个不可变对象。这样一来,我们将把对getattr(name)的访问转换为对self[index]的访问。正如我们在第6章“创建容器和集合”中会看到的,self[index]被实现为getitem(index)

    以下是对内部tuple类的一种扩展实现。

    class BlackJackCard2( tuple ):
      def new( cls, rank, suit, hard, soft ):
        return super().new( cls, (rank, suit, hard, soft) )
      def getattr( self, name ):
        return self[{'rank':0, 'suit':1, 'hard':2 ,
    'soft':3}[name]]
      def setattr( self, name, value ):
        raise AttributeError

    在以上代码中,只抛出了异常而并未包含异常的详细错误信息。

    可以按照如下代码这样使用这个类。

    >>> d = BlackJackCard2( 'A', '♠', 1, 11 )
    >>> d.rank
    'A'
    >>> d.suit
    '♠'
    >>> d.bad= 2
    Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
     File "<stdin>", line 7, in setattrAttributeError

    尽管无法轻易改变纸牌的面值,但是我们仍可通过操作d.dict来引入其他属性。

    不可变性真的有必要吗? 为确保一个对象没有被误用可能需要非常多的工作量。实际上,比起构建一个非常安全的不可变类,我们更关心的是如何通过异常抛出的诊断信息来进行错误追踪。

    3.3.3 主动计算的属性

    可以定义一个对象,当其内部一个值发生变化时,相关的属性值也会立刻更新。这种及时更新属性值的方式使得计算结果在访问时无需再次计算,从而优化了属性访问的过程。

    我们可以定义许多这样特性的setter来达到此目的。然而,如果有很多特性setter,每个setter都要去计算多个与之相关的属性值,这样做有时又是多余的。

    我们可以对有关属性的操作集中处理。以下例子中,会对Python的内部dict类型进行扩展。这样做的好处在于,可以和字符串的format()函数很好地配合。而且,无需担心不必要的属性赋值操作。

    如下代码演示了所希望的交互方式。

    >>> RateTimeDistance( rate=5.2, time=9.5 )
    {'distance': 49.4, 'time': 9.5, 'rate': 5.2}
    >>> RateTimeDistance( distance=48.5, rate=6.1 )
    {'distance': 48.5, 'time': 7.950819672131148, 'rate': 6.1}

    可以在RateTimeDistance对象中设置必需的属性值。至于其他属性值,可以当所需数据被提供时再计算。可以像以上代码演示的那样,一次性完成赋值过程,也可以按照以下代码这样分多次完成赋值。

    >>> rtd= RateTimeDistance()
    >>> rtd.time= 9.5
    >>> rtd
    {'time': 9.5}
    >>> rtd.rate= 6.24
    >>> rtd
    {'distance': 59.28, 'time': 9.5, 'rate': 6.24}

    以下代码对内部的dict进行了扩展。扩展了dict的基本映射功能,加入了对缺失属性的逻辑处理。

    class RateTimeDistance( dict ):
      def init( self, args, **kw ):
        super().init(
    args, *kw )
        self.solve()
      def getattr( self, name ):
        return self.get(name,None)
      def setattr( self, name, value ):
        self[name]= value
        self.solve()
      def __dir
    ( self ):
        return list(self.keys())
      def _solve(self):
        if self.rate is not None and self.time is not None:
          self['distance'] = self.rate
    self.time
        elif self.rate is not None and self.distance is not None:
          self['time'] = self.distance / self.rate
        elif self.time is not None and self.distance is not None:
          self['rate'] = self.distance / self.time

    dict类型使用init()方法完成字典值的填充,然后判断是否提供了足够的初始化数据。它使用了setattr()函数来为字典添加新项,每当属性的赋值操作发生时就会调用_solve()函数。

    getattr()函数中,使用None来标识属性值的缺失。对于未赋值的属性,可以使用None标记为缺失的值,这样会强制对这个值进行查找。例如,属性值来自用户输入或网络传输的数据,只有一个变量值为None而其他变量都有值。此时我们可以这样操作。

    >>> rtd= RateTimeDistance( rate=6.3, time=8.25, distance=None )
    >>> print( "Rate={rate}, Time={time}, Distance={distance}".format(
    **rtd ) )
    Rate=6.3, Time=8.25, Distance=51.975
    注意,我们不能轻易地在类定义的内部对属性赋值。

    考虑如下这行代码的实现。

    self.distance = self.rate*self.time

    如果编写了以上代码,会造成setattr()函数和solve()函数之间的无限递归调用。可使用之前演示的self['distance']方式,就可有效地避免_setattr()函数的递归调用。

    一旦3个属性被赋值,对象的灵活性会下降,了解这一点也是很重要的。

    在不改变distance的情况下,不能通过对rate赋值,从而计算出time的值。现在对这个模型做适度调整,清空一个变量的同时为另一个变量赋值。

    >>> rtd.time= None
    >>> rtd.rate= 6.1
    >>> print( "Rate={rate}, Time={time}, Distance={distance}".format(
    **rtd ) )
    Rate=6.1, Time=8.25, Distance=50.324999999999996

    以上代码中,为了使time可以使用distance既定的值,清空了time并修改了rate

    可以设计一个模型,追踪变量的赋值顺序,这个模型可以帮助来解决这样的情景。为了计算结果的正确性,在为另一个变量赋值之前,不得不先清空一个变量。