Python使用技巧--拦截内置运算属性

​ 对于__getattr__和__getattribute__的作用,它们分别拦截未定义的以及所有的属性获取,这使得它们很适合用于基于委托的编码模式。尽管对于常规命名的属性来说是这样,但它们的行为需要一些额外的澄清:对于隐式地使用内置操作获取的方法名属性,这些方法可能根本不会运行。这意味着操作符重载方法调用不能委托给被包装的对象,除非包装类自己重新定义这些方法

​ 例如,针对__str__、__add__和__getitem__方法的属性获取分别通过打印、+表达式和 索引隐式地运行,而不会指向Python 3.x中的类属性拦截方法。具体来说:

  • 在Python 3.x中,__getattr__和__getattribute__都不会针对这样的属性而运行。
  • 在Python 2.x中,如果属性在类中未定义的话,__getattr__会针对这样的属性运行。
  • 在Python 2.x中,__getattribute__只对于新式类可用,并且在Python 3.0中也可以使用。

​ 换句话说,在Python 3.x的类中(以及Python 2.x的新式类中),没有直接的方法来通用地拦截像打印和加法这样的内置操作。在Python 2.X中,这样的操作调用的方法在运行时从实例中查找,就像所有其他属性一样;在Python 3.x中,这样的方法在类中查找。 这种修改使得基于委托的编码模式在Python 3.0中更为复杂,因为它们不能通用地拦截操作符重载方法调用并将它们指向一个嵌入的对象。

​ 这虽然造成了不便,但却并不一定是一个碍事者——包装类 可以通过在自身中重新定义所有相关的操作符重载方法,从而委托调用以解决这一约 束。这些额外的方法可以手动添加,用工具添加,或者通过在共同超类中定义并从共同超类继承。然而,相对于操作符重载方法是被包装对象接口的一部分的情况,这种方法确实增加了编写包装器的工作量。

​ 记住,这个问题只适用于__getattr__和__getattribute__。由于特性和描述符只针对特定属性定义,所以它们根本不能真正应用于基于代理的类——单个特性(property)或描述符不能用于拦截任意属性。此外,定义操作符重载方法和属性拦截的一个类将能够正确地工作, 而不管定义的属性拦截的类型。我们在这里只是关心没有定义操作符重载方法,但是力图通用地拦截它们的类。

​ 考虑如下的例子,它在包含了__getattr__和__getattribute__方法的类的实例上,测试各种属性类型和内置操作:

# -*-coding:utf-8-*-

class GetAttr:
    eggs = 88  # eggs stored on class, spam on instance

    def __init__(self):
        self.spam = 77

    def __len__(self):  # len here, else __getattr__ called with __len__
        print('__len__: 42')
        return 42

    def __getattr__(self, attr):  # Provide __str__ if asked, else dummy func
        print('getattr: ' + attr)
        if attr == '__str__':
            return lambda *args: '[Getattr str]'
        else:
            return lambda *args: None


class GetAttribute(object):  # object required in 2.6, implied in 3.0
    eggs = 88  # In 2.6 all are isinstance(object) auto

    def __init__(self):  # But must derive to get new-style tools,
        self.spam = 77  # incl __getattribute__, some __X__ defaults

    def __len__(self):
        print('__len__: 42')
        return 42

    def __getattribute__(self, attr):
        print('getattribute: ' + attr)
        if attr == '__str__':
            return lambda *args: '[GetAttribute str]'
        else:
            return lambda *args: None


for Class in GetAttr, GetAttribute:
    print('\n' + Class.__name__.ljust(50, '='))

    X = Class()
    X.eggs  # Class attr
    X.spam  # Instance attr
    X.other  # Missing attr
    len(X)  # __len__ defined explicitly

    try:  # New-styles must support [], +, call directly: redefine
        X[0]  # __getitem__?
    except:
        print('fail []')

    try:
        X + 99  # __add__?
    except:
        print('fail +')

    try:
        X()  # __call__?  (implicit via built-in)
    except:
        print('fail ()')
    X.__call__()  # __call__?  (explicit, not inherited)

    print(X.__str__())  # __str__?   (explicit, inherited from type)
    print(X)  # __str__?   (implicit via built-in)

​ 在Python 2.x下运行的时候,__getattr__的确接收针对内置操作的各种隐式属性获取,因为python通常在实例中查询这样的属性。相反,对于任何被内置操作调用的操作符重载名, __getattribute__不会运行,因为内置操作符重载的名称只从新式类模型中的在类中查找:

​ 对于__getattribute__的情况,在python2.x和python3.x中是相同的,因为在python2.x中类必须通过派生自object成为新式类,才能使用这个方法。这段代码的object派生在python3.x中是可选的,因为其中所有的类都是新式类。

GetAttr===========================================
getattr: other
__len__: 42
getattr: __getitem__
getattr: __coerce__
getattr: __add__
getattr: __call__
getattr: __call__
getattr: __str__
[Getattr str]
getattr: __str__
[Getattr str]

GetAttribute======================================
getattribute: eggs
getattribute: spam
getattribute: other
__len__: 42
fail []
fail +
fail ()
getattribute: __call__
getattribute: __str__
[GetAttribute str]
<__main__.GetAttribute object at 0x104ea74e0>

​ 然而,在Python 3.x下运行的时候,__getattr__的结果与python2.x有所不同。当调用和打印这两个内置操作获取属性的时候,__getattr__和__getattribute__属性拦截方法都不会被触发。在解析内置操作服重载名称的时候,python3.x(以及python2.x中的新式类)跳过了常规的示例查找机制,尽管被一般命名的方法依然能像之前一样被拦截:

GetAttr===========================================
getattr: other
__len__: 42
fail []
fail +
fail ()
getattr: __call__
<__main__.GetAttr object at 0x10488a850>
<__main__.GetAttr object at 0x10488a850>

GetAttribute======================================
getattribute: eggs
getattribute: spam
getattribute: other
__len__: 42
fail []
fail +
fail ()
getattribute: __call__
getattribute: __str__
[GetAttribute str]
<__main__.GetAttribute object at 0x104844460>

跟踪这些输出,从而了解到脚本中的打印,看看这是如何工作的:

  • 在Python 3.x中,__str__访问有两次未能被__getattr__捕获:一次是针对内置打印,一次是针对显式获取,因为从该类继承了一个默认方法(实际上,该类来自内置object,它是每个类的一个超类)。
  • __str__只有一次未能被__getattribute__捕获,即在内置打印操作中,显式获取绕过了继承的版本,被__getattribute__捕获。
  • __call__在Python 3.x中用于内置调用表达式的两次都没有捕获,但是,当显式获取的时候,它两次都拦截到了;和__str__不同,object中并不存在能够被继承的__cal l__默认版本来阻碍__getattr__的拦截。
  • __len__被__getattr__和__getattribute__都捕获了,直接原因是,它在类自身中是一个显式定义的方法--它的名称指明了,在Python 3.x中,如果我们删除了类的__len__方法,它就不会指向__getattr__或__getattribute__。
  • 所有其他的内置操作在Python 3.x中都没有被__getattr__和__getattribute__拦截。

​ 再一次,直接的效果是,由内置操作隐式地运行的操作符重载方法始终不会通过Python3.x中的__getattr__和__getattribute__属性拦截方法指向:Python 3.x在类中查找这样的属性,并且完全跳过了实例查找。

​ 这使得基于委托的包装类在Python 3.x中更难以编写,如果被包装的类可能包含操作符重载方法,这些方法必须冗余地在包装类中重新定义,从而能够委托给被包装的对象。 在一般的委托工具中,这可能会增加很多额外的方法。

​ 当然,这些方法的增加可能一部分是工具自动进行的,通过用新的方法来扩展类做到 (比如使用类装饰器和元类可能会有帮助)。此外,一个父类可能能够一次性定义所有这些额外方法,以便在基于委托的类中继承。然而,在Python 3.x中,委托编码模式还是需要额外的工作。

​ 要了解关于这一现象的更实际的说明及其解决方法,参考<<Python使用技巧--python装饰器的使用>>的类装饰器的运用的示例二。正如将学到的那样,我们也可以在客户类中插入一个__getattribute__从而保留其最初的类型,尽管这个方法仍然不会为了操作符重载方法而调用;例如,打印仍然直接运行在这样的一个类中定义的__str__,而不是通过__getattribute__指向请求。

posted on 2021-11-07 15:56  xufat  阅读(230)  评论(0)    收藏  举报

导航

/* 返回顶部代码 */ TOP