[译] 属性访问、特性和描述符 2

注:原书做者 Steven F. Lott,原书名为 Mastering Object-oriented Pythonpython

__getattribute__()方法

__getattribute__()方法是一个更底层的属性处理。它的默认实现试图把一个属性做为一个已经存在于内部__dict__(或__slots__)的属性来定位值。若是没有找到该属性,它会调用__getattr__()。若是值被定位为描述符(参见下面《建立描述符》部分),则处理描述符;不然只是简单的返回值。c++

经过重写此方法,咱们能够完成如下任何一个任务:程序员

  • 咱们能够有效地防止对属性的访问。这种方法经过抛出异常来代替返回一个值,可使一个属性比咱们仅仅使用下划线(_)将一个命名标记为私有更私密。web

  • 咱们能够发明新的属性,相似于__getattr__()如何发明新的属性。然而,在这种状况下,咱们能够经过默认版本的__getattribute__()来绕过默认查找。数据库

  • 咱们可让属性执行惟一且不一样的任务。这会使得程序很是难以理解和维护。这是一个糟糕的想法。设计模式

  • 咱们能够改变描述符的行为。虽然技术上可能,但改变一个描述符的行为是一个可怕的想法。缓存

在咱们实现__getattribute__()方法时,重要的是要注意在方法体中不能有任何的内部属性访问。若是咱们试图经过self.name获取值,将致使无限递归。函数

__getattribute__()方法不能提供任何简单的self.name属性访问,这将致使无限递归。性能

为了在__getattribute__()方法中获取属性值,咱们必须显式地访问object定义的基础方法,如如下所示声明:优化

object.__getattribute__(self, name)

例如,咱们可使用__getattribute__()修改咱们的不可变类以及防止访问内部__dict__属性。下面这个类,隐藏了全部如下划线(_)开头的命名:

class BlackJackCard3:
    """Abstract Superclass"""
    def __init__(self, rank, suit, hard, soft):
        super().__setattr__('rank', rank)
        super().__setattr__('suit', suit)
        super().__setattr__('hard', hard)
        super().__setattr__('soft', soft)

    def __setattr__(self, name, value):
        if name in self.__dict__:
            raise AttributeError("Cannot set {name}".format(name=name))
        raise AttributeError("'{__class__.__name__}' has no attribute
        '{name}'".format(__class__= self.__class__, name= name))

    def __getattribute__(self, name):
        if name.startswith('_'):
            raise AttributeError
        return object.__getattribute__(self, name)

咱们已经覆写了__getattribute__()的私有名称以及Python内部名称来抛出一个属性错误。这前面的示例有一个微小的优点:咱们再也不容许调整对象。咱们将会看到该类的实例交互的示例。

下面示例是该类对象的变形:

>>> c = BlackJackCard3('A', '♠', 1, 11)
>>> c.rank = 12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in __setattr__
  File "<stdin>", line 13, in __getattribute__
AttributeError
>>> c.__dict__['rank']= 12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 13, in __getattribute__
AttributeError

一般建议,搞混__getattribute__()不是一个好主意。默认方法至关复杂,并且几乎全部咱们须要的都是做为特性利用或做为__getattr__()的改变。

建立描述符

描述符是调和属性访问的一个类。描述符类可用来获取、设置或删除属性值。描述符对象是在类定义的时候构建在一个类中的。

描述符设计模式有两个部分:一个全部者类属性描述符自己。全部者类给它的属性使用一个或多个描述符。描述符类定义了获取、设置和删除方法的组合。描述符类的一个实例将会是全部者类的一个属性。

特性是基于全部者类的方法函数。描述符不像特性,是一个类的实例,与全部者类不一样。所以,描述符一般是可重用的通用属性。全部者类能够有多个不一样描述符类的实例来类管理具备类似行为的属性。

不像其余属性,描述符在类级别上建立。它们不是在__init()__初始化时建立。然而描述符的值能够在初始化期间设置,描述符一般是做为类的一部分,在任何方法函数以外来构建的。

当全部者类被定义时,每一个描述符对象都是被绑定到一个不一样的类级别属性的描述符类实例。

被确认为一个描述符,一个类必须实现如下三个方法的任意组合。

  • Descriptor.__get__(self, instance, owner) -> object:在这个方法中,instance参数是即将被访问的对象的self变量。owner参数是全部者类的对象。若是这个描述符在类的上下文中被调用,instance参数将获得一个None值。这必须返回描述符的值。

  • Descriptor.__set__(self, instance, value):在这个方法中,instance参数是即将被访问的对象的self变量。value参数是描述符须要设置的新值。

  • Descriptor.__delete__(self, instance)在这个方法中,instance参数是即将被访问的对象的self变量。该描述符的方法必须删除这个属性的值。

有时,一个描述符类还将须要一个__init__()方法函数来初始化描述符的内部状态。

有两种基于已定义方法的描述符,以下所示:

  • 非数据描述符:这种描述符定义__set__()__delete__()或二者皆有。它不能定义__get__()。非数据描述符对象每每会被用做表达式的一部分。它多是一个可调用对象,或者它可能有本身的属性或方法。一个不可变的非数据描述符必须实现__set__(),但可能只是抛出AttributeError。这些描述符设计时很简单,由于接口更灵活。

  • 数据描述符:这种描述符至少定义__get__()。一般,它定义__get__()__set__()来建立一个可变对象。鉴于描述符将在很大程度上是不可见的,则不能更进一步的再定义属性或方法。属性的引用有一个数据描述符的数据被委托给描述符的__get__()__set__()__delete__()方法。这些是很难设计的,因此咱们稍后来再看。

描述符有各类各样的用例。在内部,Python使用描述符有如下几个缘由:

  • 在隐藏的内部,类的方法是做为描述符来实现。这些非数据描述符应用方法函数到对象以及不一样的参数值。

  • property()函数经过给一个字段建立数据描述符来实现。

  • 一个类方法或静态方法被实现为一个描述符;这被应用到类中来代替类的实例。

当咱们在第11章《经过SQLite存储和检索对象》看到对象-关系映射的时候,咱们将看到许多ORM类定义大量使用描述符将Python类映射到SQL表和列。

当咱们考虑一个描述符的目的,咱们还必须为数据做为描述符能够正常工做来考察三种常见用例,以下所示:

  • 描述符对象有数据或获取到了数据。在这种状况下,描述符对象的self变量是有意义的且描述符是有状态的。数据描述符的__get__()方法返回这个内部数据。非数据描述符,描述符有其余方法或属性来访问这些数据。

  • 包含数据的全部者实例。在这种状况下,描述符对象必须使用instance参数来引用值到全部者对象中。数据描述符的__get__()方法从实例获取数据。非数据描述符有其余方法访问实例数据。

  • 包含相关数据的全部者类。在这种状况下,描述符对象必须使用owner参数。这是经常使用的当描述符实现了应用于整个类的静态方法或类方法。

咱们将仔细看下第一种状况。咱们看看建立带有__get__()__set__()方法的数据描述符。咱们也会看看建立没有__get__()方法的非数据描述符。

第二种状况(全部者实例中的数据)展现了@property装饰器都作了些什么。可能的优点是描述符有一个传统的特性将计算从拥有者类移到描述符类中。这倾向于分片类设计且可能不是最好的方法。若是计算是真正史诗般的复杂,策略模式可能会更好。

第三种状况展现@staticmethod@classmethod装饰器是如何实现的。咱们不须要从新发明轮子。

一、使用非数据描述符

咱们常常会有一些紧密绑定了属性值的小对象。对于这个示例,咱们将看看数值被绑定到单位的举措。

下面是一个简单的非数据描述符类,它缺乏一个__get__()方法:

class UnitValue_1:
    """Measure and Unit combined."""
    def __init__(self, unit):
        self.value = None
        self.unit = unit
        self.default_format = "5.2f"

    def __set__(self, instance, value):
        self.value = value

    def __str__(self):
        return "{value:{spec}} {unit}"
        .format(spec=self.default_format, **self.__dict__)

    def __format__(self, spec="5.2f"):
        #print( "formatting", spec )
        if spec == "":
            spec = self.default_format
        return "{value:{spec}} {unit}".format(spec=spec, **self.__dict__)

这个类定义了一对简单的值,一个可变的(值),另外一个是有效的不可变对象(单位)。

当这个描述符被访问时,描述符对象自己是可用的,且描述符的其余方法或属性能够被使用。咱们可使用这个描述符来建立类去管理尺寸和其余与物理单位有关的数值。

下面是一个类,作速度-时间-距离的及早计算:

class RTD_1:
    rate = UnitValue_1("kt")
    time = UnitValue_1("hr")
    distance = UnitValue_1("nm")
    def __init__(self, rate=None, time=None, distance=None):
        if rate is None:
            self.time = time
            self.distance = distance
            self.rate = distance / time
        if time is None:
            self.rate = rate
            self.distance = distance
            self.time = distance / rate
        if distance is None:
            self.rate = rate
            self.time = time
            self.distance = rate * time

    def __str__(self):
        return "rate: {0.rate} time: {0.time} distance:{0.distance}".format(self)

一旦对象被建立且属性被加载,丢失的值就已经被计算。一旦计算,描述符能够检查获取值或单位的名称。此外,描述符对str()有一个方便的响应和请求格式。

下面是描述符和RTD_1类之间的交互:

>>> m1 = RTD_1(rate=5.8, distance=12)
>>> str(m1)
'rate:  5.80 kt time:  2.07 hr distance: 12.00 nm'
>>> print("Time:", m1.time.value, m1.time.unit)
Time: 2.0689655172413794 hr

咱们建立了一个带有ratedistance参数的RTD_1实例。这些都是用来计算ratedistance描述符的__set__()方法。

当咱们请求str(m1),这会计算RTD_1的全部str()方法,转而使用ratetimedistance描述符的__format__()方法。这为咱们提供了数字和单位。

鉴于非数据描述符没有__get__()且不返回其内部值,咱们能够访问描述符的单个元素。

二、使用数据描述符

数据描述符设计要复杂一些,由于它对接口有限制。它必须有一个__get__()方法,且只能有__set__()__delete__()。这是全部的接口:这些方法从一到三,没有其余方法。引入一个额外的方法意味着Python不会把该类看成一个正确的数据描述符。

咱们会使用描述符设计一个简单的单位转换模式,能够在__get__()__set__()方法作适当的转换。

下面是一个单位描述符的超类,它在其余单位和标准单位之间作转换:

class Unit:
    conversion = 1.0
    def __get__(self, instance, owner):
        return instance.kph * self.conversion

    def __set__(self, instance, value):
        instance.kph = value / self.conversion

该类用简单的乘法和除法将标准单位转换为其余非标准单位,反之亦然。

经过这个超类,咱们能够从一个标准单位定义一些转换。在前面的示例,标准单位是公里时(千米/小时)。

如下是这两个转换描述符

class Knots(Unit):
    conversion = 0.5399568

class MPH(Unit):
    conversion = 0.62137119

继承方法很是有用。惟一改变的是转换因子。这些类可用于处理涉及单位转换的值。咱们能够处理英里每小时或可交换的节点。下面是一个标准单位的单位描述符,千米每小时:

class KPH(Unit):

    def __get__(self, instance, owner):
        return instance._kph

    def __set__(self, instance, value):
        instance._kph = value

这个类表明一个标准,因此不作任何转换。它使用一个私有变量实例保存速度公里每小时的标准值。避免任何算术转换是一个简单的技术优化。避免任何一个公共字段的引用是相当重要的,来规避无限递归。

下面这个类,它对于一个给定的尺寸提供了一组转换:

class Measurement:
    kph = KPH()
    knots = Knots()
    mph = MPH()
    def __init__(self, kph=None, mph=None, knots=None):
        if kph:
            self.kph = kph
        elif mph:
            self.mph = mph
        elif knots:
            self.knots = knots
        else:
            raise TypeError

    def __str__(self):
        return "rate: {0.kph} kph = {0.mph} mph = {0.knots} knots".format(self)

对于不一样的单位每一个类级别的属性都是描述符。各类描述符的获取和设置方法会作适当的转换。咱们可使用这个类在各类单位之间进行速度转换。

如下是与Measurement类交互的一个例子:

>>> m2 = Measurement(knots=5.9)
>>> str(m2)
'rate: 10.92680006993152 kph = 6.789598762345432 mph = 5.9 knots'
>>> m2.kph
10.92680006993152
>>> m2.mph
6.789598762345432

咱们经过设置不一样的描述符建立了一个Measurement类的对象。在第一个示例中,咱们设置了节点描述符。

当咱们显示的值是一个大字符串,则每一个描述符的__get__()都将被使用。这些方法从全部者对象获取内部kph字段值,应用一个转换因子,且返回一个结果值。

kph字段还使用了一个描述符。这个描述符不作任何转换;然而,它只是返回了缓存在全部者对象的私有值。KPHKnots描述符要求全部者类实现一个kph属性。

总结,设计要素和权衡

在这一章,咱们研究了几种使用一个对象属性的方式。咱们可使用内置object类的特性以及获取和设置属性值。咱们能够定义属性来修改属性的行为。

若是咱们想要更复杂,咱们能够调整底层__getattr__()__setattr__()__delattr__()__getattribute__()特殊方法实现。这些让咱们能够更精细的控制字段的行为。当咱们接触到这些方法咱们走的很顺利,由于咱们能够对Python的行为进行基本(和使人困惑的)改变。

在内部,Python使用描述符来实现特性,例如方法函数、静态方法函数和属性。描述符许多很酷的用例已是语言的最好的特性。

来自其余语言(特别是Java和c++)的程序员一般有试图让全部属性私有以及编写大量的gettersetter函数的冲动。

在Python中,将全部属性做看成公有的是至关简单的。这意味着以下:

  • 它们应该有良好的文档记录。

  • 它们应该正确的反映对象的状态,它们不该该是暂时的或临时的值。

  • 一个字段有使人困惑的(或易变的)字段值是很是罕见的,一个如下划线(_)开头的命名做为“不是已定义接口中的一部分”不是真的私有。

把私有字段看作麻烦事是很重要的。在语言中封装并无由于缺少复杂的私有机制而受损;而会由于糟糕的设计而受损。

一、特性对属性

在大多数状况下,字段能够设置在类以外且没有不良后果。咱们的Hand类的示例展现了这一点。对于许多版本的类,咱们能够简单地追加到hand.cards,以及完美的工做经过特性延迟计算total

在改变属性这种状况下,会致使相应的其余字段的变化,这须要一些更复杂的类设计:

  • 一个方法函数能够阐明状态变化。当须要多个参数值时这将是必要的。

  • 一个特性setter可能比一个方法函数更清晰。当须要单个值时这将是一个明智的选择。

  • 咱们还可使用原地操做符。咱们将到第七章《创造数字》看到这些。

没有严格的规则。在这种状况下,咱们须要设置一个参数值,方法函数和特性之间的区别彻底是API语法和如何传达意图的区别。

为了计算值,特性容许延迟计算,而一个属性须要及早计算。这是性能的问题。延迟计算对及早计算的优点是基于对用例的预期。

二、使用描述符设计

不少描述符的示例已是Python的一部分。咱们不须要重复特性、类方法或静态方法。

建立新的描述符最引人注目的状况是在Python和一些非Python之间创建映射关系。例如,对象-关系数据库映射,须要大量的维护以确保一个Python类有正确的属性以正确的顺序匹配一个SQL表和列。一样,当映射到一些Python以外,一个描述符类能够处理数据编码和解码或从外部获取数据来源。

当构建一个web服务端,咱们可能会考虑使用描述符来作web服务请求。例如,__get__()方法可能会变成一个HTTP的GET请求,__set__()方法可能会变成一个HTTP的PUT请求。

在某些状况下,一个单一的请求可能填充几个描述符的数据。在这种状况下,__get__()方法将检查实例的缓存以及在一个HTTP请求以前返回一个值。

经过属性来操做许多数据描述符操做会很是简单。这给咱们提供开始的机会:首先编写属性。若是属性处理变得很是昂贵和复杂,则咱们能够切换到描述符来对类进行重构。

三、展望将来

在下一章,咱们将密切关注将在五、六、7章探索的ABCs(抽象基类)。这些基础知识将帮助咱们定义类,它会与现有Python特性很好地集成。它们还将容许咱们建立执行一致的设计和扩展的类层次结构。

相关文章
相关标签/搜索