(网利宝项目框架是基于python自开发的,使用了大量的python自有属性方法,大家感兴趣,可以扫码注册体验产品)
Python管理属性的方法一般有三种:操作符重载(即,__getattr__、__setattr__、__delattr__和__getattribute__,有点类似于C++中的重载操作符)、property内置函数(有时又称“特性”)和描述符协议(descriptor)。
在Python中,类和类实例都可以有属性:Python中类的属性相当于C++中类的静态成员,而类实例的属性相当于C++中类的非静态成员。可以简单地这么理解。
1 操作符重载
在Python中,重载__getattr__、__setattr__、__delattr__和__getattribute__方法可以用来管理一个自定义类中的属性访问。其中,__getattr__方法将拦截所有未定义的属性获取(即,当要访问的属性已经定义时,该方法不会被调用,至于定义不定义,是由Python能否查找到该属性来决定的);__getattribute__方法将拦截所有属性的获取(不管该属性是否已经定义,只要获取它的值,该方法都会调用),由于此情况,所以,当一个类中同时重载了__getattr__和__getattribute__方法,那么__getattr__永远不会被调用,另外,__getattribute__方法仅仅存在于Python2.6的新式类和Python3的所有类中;__setattr__方法将拦截所有的属性赋值;__delattr__方法将拦截所有的属性删除。说明:在Python中,一个类或类实例中的属性是动态的(因为Python是动态的),也就是说,你可以往一个类或类实例中添加或删除一个属性。
说明:《Python学习手册(第四版)》中把这些操作分成两类:一类是__getattr__、__setattr__和__delattr__,另一类是__getattribute__。笔者认为它们都是操作符重载且与C++中的操作符重载类似,所以在这把它们归为一类。
如果我们想使用这类方法(即重载操作符)来管理自定义类的属性,就需要我们在我们的自定义类中重新定义这些方法的实现。由于__getattribute__、__setattr__、__delattr__方法对所有的属性进行拦截,所以,在重载它们时,不能再像往常的编码,要注意避免递归调用(如果出现递归,则会引起死循环);然而对__getattr__方法,则没有这么多的限制。
1.1 重载__setattr__方法
在重载__setattr__方法时,不能使用“self.name = value”格式,否则,它将会导致递归调用而陷入死循环。正确的应该是:
def __setattr__(self, name, value):
# do-something
object.__setattr__(self, name, value)
# do-something
注:其中的“object.__setattr__(self, name, value)”一句可以换成“self.__dict__[name] = value”;但前提是,必须保证__getattribute__方法重载正确(如果重载了__getattribute__方法的话),否则,将在赋值时导致错误,因为self.__dict__将要触发对self所有属性中的__dict__属性的获取,这样从而就会引发__getattribute__方法的调用,如果__getattribute__方法重载错误,__setattr__方法自然而然也就会失败。
1.2 重载__delattr__方法
在重载__delattr__方法时,不能使用del self.name格式,否则,它将会导致递归调用而陷入死循环。正确的应该是:
def __delattr__(self, name):
# do-something
object.__delattr__(self, name)
# do-something
注:其中的“object.__delattr__(self, name)”一句可以换成“del self.__dict__[name]”;但前提是,必须保证__getattribute__方法重载正确(如果重载了__getattribute__方法的话),否则,将在删除属性时导致错误,因为self.__dict__将要触发对self所有属性中的__dict__属性的获取,这样从而就会引发__getattribute__方法的调用,如果__getattribute__方法重载错误,__delattr__方法自然而然也就会失败。
1.3 重载__getattribute__方法
def __getattribute__(self, name):
# do-something
return object.__getattribute__(self, name)
# do-something
注:在__getattribute__方法中不能把“return object.__getattribute__(self, name)”一句替换成“return self.__dict__[name]”来避免循环,因为它(即其中的self.__dict__)也会触发属性获取,进而还是会导致递归调用。
1.4 重载__getattr__方法
由于__getattr__方法是拦截未定义的属性,所以它没有其他三个操作符方法中那么多的限制,因此,你可以像正常的代码一样编写它。它的作用就是,当一段代码(用户写的,可能是故意,也可以是无意)获取了类或类实例中没有定义的属性时,程序将做出怎样的反应,而这个回应就在__getattr__方法中,由你来定。
1..5 说明
这里是一些关于这几个操作符重载的额外信息,以及一些其他的内容,比如:属性存储位置。
1.5.1 默认的属性访问拦截
如果上述四个操作符方法中实现了任何一个,那么,当我们执行属性访问时,相应的操作符方法就会被调用。但是当我们没有实现某个时,那么相应的属性访问由默认实现来拦截。比如:如果我们仅仅实现了__setattr__和__delattr__两个操作符方法,而没有实现__getattribute__和__getattr__操作符方法,那么我们对属性的引用获取将使用系统默认的方式,而对于所有的属性赋值则调用__setattr__操作符方法,所有属性的删除操作则调用__delattr__操作符方法。
1.5.2 返回值
__getattr__和__getattribute__应该返回属性name的值,但__setatrr__和__delattr__就返回None(None可以显示返回,也可以不显示返回;当不显示返回时,Python中的函数或方法默认返回None)。
1.5.3 使用object类来避免循环调用
可能有些读者会对object.__getattribute__(self, name)、object.__setattr__(self, name, value)和object.__delattr__(self, name)有些疑惑。其实,只要明白了Python中的类型继承系统和类方法调用就会很容易明白了。下面仅仅简单的说明一下,有关更详细的这方面的内容请参见相关的Python书籍(因为这属于Python的基本语法范畴)。另外,这里也与类实例中属性布局有点小小的关系,我们将在本章节的末尾讨论(这部分不了解也行,了解将对Python更加清楚)。
关于Python中的类型继承系统,在Python2.6中的新式类中和Python3中的所有类中,所有的类(包括内置类型和我们的自定义类型)都是object的子类,object是类型继承系统中最顶尖的类,所有的类都直接或间接地继承自它。另外,补充一句,所有类型(包括内置类型和我们的自定义类型)的类型都是type类型,或者说它们这些类型都是由type类型创建的。这里有个例外,我们的自定义类型可以通过元类来修饰或改变我们的自定义类,这时我们的自定义类型的类型可能会改变(具体情况要看我们的元类的实现)。
关于Python中的类成员方法的调用很特别,与其他传统的编程语言不同。当我们使用Instance.Method( args …)格式来调用类成员方法时,Python在底层会做一个转换:Python先到Instance所属的类中找Method方法,如果找到就调用它,如果没有找到,就到Instance所属的类的所有基类中依次去找,直到找到第一个为止。当找到一个Method方法时,这里我们假设Method方法所属的类是Class,那么Python就会把上述格式的方法调用转换成Class.Method(Instance, args …)形式,接着在底层完成相应的工作。也就是说,Python在最终完成方法调用的是Class.Method(Instance_object, Instance_args)。那么,我们是不是可以说,可以直接使用这种形式呢?答案是。可以说,我们在进行方法调用时,可以使用这两种方式中的任何一种;但是,它们有一点区别:当使用第一种时,Python会向从类实例所属的类开始,沿着继承链向上(它的基类)搜索成员方法,直到找到第一个或找到达object类也没有找到才停止;然而,对于第二种,Python只在Class的名字空间(即__dict__字典)中搜索成员方法,而不会搜索其他的类。
由于所有的类都是object的子类,所以我们可以通过上面的方式,把属性的访问控制递交给object类,以此来避免递归调用。
1.5.4 公有、私有控制
由于Python是动态语言,也就是说,你可以动态的给已经定义好的类或类实例添加、删除某个属性;而在C++、Java等语言中,类一旦定义,其类属性或类实例属性就不能添加、删除。如果你想在Python中,像C++、Java那样对未定义属性的访问进行限制,也是可以的,你可以这样做:凡是对未定义属性的获取、赋值、删除操作,在相应的访问控制方法(__getattr__、__getattribute__、__setaatr__、__delattr__)中都抛出一个AttributeError异常(这个异常是Python内置的)。
澄清一下,我们可以像C++、Java那样,在Python中控制未定义属性的访问,但是我们不能像C++、Java那样控制公有(public)、私有(private)、保护(protected)性的访问。在Python中,所有的属性都是公有的,你永远都可以访问(获取、赋值、删除),我们没有办法把它们变成私有的。虽然Python中有一个“私有”成员的概念,但它并不是C++、Java中的那个“私有”,它十分的脆弱,你可以把它给忽略掉。
笔者上面所述的“我们没有办法把它们变成私有的”并不十分准确,笔者在这样说时,是按照Python中默认的方式,也就是说,你没有蓄意地改变这种行为。换句话说,就是我们确实可以通过某种手段,潜在地来把Python中的某些类或类实例的“公有”属性变成一个“私有”的属性。在《Python学习手册(第四版)》中,作者Mark Lutz给出了一个解决方案——他写了一个装饰器,通过这个装饰器可以把任何一类的某些属性变成“私有”或“公有”的(这里所说的“公有”和“私有”相当于C++、Java中的公有和私有),但是这种“公有”性也不是很强,有点脆弱,能够被恶意的代码所击穿(Mark Lutz也承认了这点)。除此之外,我们还可以通过原始的方法来实现“公有”和“私有”性。这种方法就是,把所有的属性分为两类:公有集合和私有集合;把所有的公有属性名放在一个公有集合中,把所有的私有属性放到私有集合当中,当我们访问一个属性时,先判断它是否是存在于私有集合中,如果是,则不能直接访问,对于成员变量必须通过一个成员方法来间接访问(在C#中,比较严格,为了访问私有成员变量,必须要定义get和set方法),对于成员方法则不能访问,否则就抛出一个异常(比如:AttributeError内置异常)或者什么都不做;如果它不在于私有集合当中,再判断它是否是存在于公有集合当中,如果是,则可以进行相应的操作,如果不是,则抛出一个异常(比如:AttributeError异常,表示没有该属性)。在Python中,我们很难甚至没办法来实现像C++、Java中的“保护”(protected)机制。
1.5.5 Python中类或类实例的属性的存储位置
关于这部分内容,其实也很简单,就像C++中的成员变量一样。简单地说,C++中的成员变量怎样理解,在Python中也怎样理解。
这里,笔者简述一下:在类继承体系中,在不同类中声明的成员变量如何存储?
在Python中,所有的东西都是对象,这些对象是由一个类型实例化而来;那么说,这些对象就有属性和方法,属性可以存储一些值。而从直观上来看,任何可以存储东西的事物,我们都可以说它有一个空间(只有有空间,才能存储东西嘛),而在编程中,我们一般使用术语“名字空间”或“命名空间”(namespace)来称呼它。这样,我们就得出,每个对象都有一个名字空间。而在Python中,我们使用对象的__dict__属性来保存该对象的名字空间中的东西,__dict__是一个字典(“键-值”对,一般“键”就是属性名或方法名,“值”就是属性的值或方法名所指向的真正的方法实体对象)。
因此,我们看出,类有它自己的名字空间,它的名字空间中保存的是它的属性和方法;而类实例也有它自己的名字空间,它的名字空间中保存的是它自己的属性(不包含类的属性)。但是我们要知道,Python在对类实例的属性查找时,可以向类中查找,也就是说,可能通过类实例来引用类的属性;反过来,却是不行。我们虽然能够通过类实例来引用类的属性,但是却不能通过类实例来给类的属性赋值或删除类的属性:当我们通过类实例来给某个属性赋值或删除某个属性时,Python只认为该属性是该类实例的,而不会到类的名字空间中查找它。当通过类实例给个属性赋值时,如果该类实例中已经有该属性,则Python将把它的值修改成新值;如果该类实例中没有该属性,那么,Python将会在该类实例的名字空间(即__dict__)中创建该属性,并给它赋值成新值,这时,如果该类实例所属的类及其基类中也有同名的属性,那么这些同名属性在查找时将会被该类实例的属性所覆盖,也就是,Python将找到该类实例中的属性,而不会找到它所属的类及其基类中的那些同名属性。
那么,我们如何才能修改或删除类中类属性呢?其实很简单,我们不能通过类实例,而是要通过类本身(类本身也是一个对象)。比如:类A中有一个类属性a,如果要想修改a的值,就可以使用“A.a = 123”。如果我们非要使用类实例来操作类属性的赋值和删除怎么办?其实也很简单,我们可以像在C++、Java中访问私有成员变量一样,定义一个成员方法,用这个成员方法来修改或删除该类的类属性,然后类实例去调用即可,而且这种方式的控制可以用于继承体系当中。
1.5.6 最后一点事实的澄清
前面我们已经陈述,__getattr__会拦截所有未定义的属性获取,__getattribute__会拦截所有的属性获取,__setattr__会拦截所有的属性赋值,__delattr__会拦截所有的属性删除。在这里,笔者承认自己“说了慌”,其实它们并没有这么大的能力。前面的陈述是有一个限制的:它不适用于隐式地使用内置操作获取的方法名属性。这意味着操作符重载方法调用不能委托给被包装的对象,除非包装类自己重新定义这些方法(附:这些方法非常适合用于基于委托的编码模式)。
2 特性
在Python中,除了重载操作符外来管理类实例属性的访问控制外,也可以使用特性(property)。
2.1 property类
在没有讲解使用特性来管理类实例的属性访问控制时,我们先来探讨一下“什么是特性”。
其实,在Python中,隐式的存在一种类型,它就是property类,可以把它看成是int类型,并可以使用它来定义变量(在面向对象里,一般称为“对象”,也就是类实例)。而用来管理类实例的属性访问控制的“特性”正是使用这个property类。该类可以管理自定义类的实例的属性访问控制。
在property类中,有三个成员方法和三个装饰器函数。三个成员方法分别是:fget、fset、fdel,它们分别用来管理属性访问;三个装饰器函数分别是:getter、setter、deleter,它们分别用来把三个同名的类方法装饰成property。其中,fget方法用来管理类实例属性的获取,fset方法用来管理类实例属性的赋值,fdel方法用来管理类实例属性的删除;getter装饰器把一个自定义类方法装饰成fget操作,setter装饰器把一个自定义类方法装饰成fset操作,deleter装饰器把一个自定义类方法装饰成fdel操作。
以上的说明,笔者有点把fget、fset、fdel的功能说死了,其它这三个函数中不仅仅可以分别管理自定义类实例属性的获取、赋值、删除,它们还可以做其它的任何事情,就像普通函数一样(普通函数能完成什么样的功能,它们也都可以),甚至不做它们的本职工作也行(不过,我们一般都会完成自定义类实例属性的获取、赋值、删除等操作,有时,可能还会完成一些其它额外的工作)。其实,不管这三个函数完成什么的功能,只要在获取自定义类实例的属性时就会自动调用fget成员方法,给自定义类实例的属性赋值时就会自动调用fset成员方法,在删除自定义类实例的属性时就会自动调用fdel成员方法。
2.2 property类的使用
根据property类中的成员类别,笔者把有关“特性”的使用分成两部分:一般成员方法和装饰器方法。
2.2.1 使用一般成员方法
我们上面提到“property类是隐式的”,所以,我们不能直接使用这个property类。那怎么办呢?没关系,Python为我们提供了一个标准的内置函数property,该函数通过我们传递给它的参数自动帮我们创建一个property类实例,并将该类实例返回。所以,我要想创建property类实例,就需要调用标准的内置函数property。
2.2.1.1 property函数
property函数的原型:property(fget=None, fset=None, fdel=None, doc=None)
其中,前三个参数分别是自定义类中的方法名,property函数会根据这三个参数自动创建property类中的三个相应的方法。第四个参数文档(也就是字符串),把它作为property类的说明文档;我们知道,在Python中,可以为一个类或函数添加一个文档说明;当doc参数为None时,property函数会提取第一个参数fget的说明文档,如果fget参数的说明文档也为None,那么doc参数的值就为None。property函数返回一个property类型的实例。
2.2.1.2 property的使用方法
关于“特性”的使用方法,是在自定义类中通过定义一个类属性而完成的;虽然property类实例是自定义类的类属性,但对property类实例的操作将作用于自定义类的实例的属性上。其实,自定义类的实例的属性的信息仍然是存储于自定义类的实例中,而“特性”(即property类实例)只不过在管理如何对它进行访问(获取、赋值、删除)。
有些读者可能会对上述的“自定义类的实例的属性”有所疑惑。就像开头所述的,其实它就相当于C++中类的实例的成员。
由property创建的属性(也就是管理自定义类实例的“特性”)是属于自定义类的,而不是属于自定义类实例的;但是,它却是管理自定义类实例的属性的,而不管理自定义类的类属性。下面,我们具体地再把“特性”的执行流程阐述一下。
由以上,我们可以知道,要用“特性”来管理自定义类的实例的属性访问控制,必须有两个属性,一个是自定义类的类属性(也就是“特性”,porperty类的实例),另一个自定义类的实例的属性。既然“特性”是自定义类的属性,那么可以我们可以通过自定义类对象(因为类型也是一个对象,所以自定义类自然也是一个对象,我们称为自定义类对象)来访问“特性”;又由于“特性”是property类的实例,因此,此时我们得到的是一个实例或者说是对象,而它有fget、fset、fdel三个成员方法,那么我们可以来间接的访问这些方法,而我们知道这三个成员方法是用来管理自定义类的实例的属性访问控制,此时调用这些方法将会间接地管理自定义类的实例的属性访问控制。因此,这种通过自定义类对象来调用其成员方法来控制自定义类的实例的属性访问控制是一种间接性的,我们可以这么做,但实际应用中,我们是不会这么用,这里只是说明一下,让读者对“特性”有个更深层次的了解;在实际应用中使用的是下面的一种方法。
对于面向对象,我们知道,类的实例可以访问类的属性(在C++中就是类中的静态成员)。所以,在Python中,我们也可以通过自定义类的实例来访问“特性”。但是,此时的访问却与一般的类实例访问类属性的行为有些不同:自定义类实例访问到的“特性”(这里笔者没有说“类属性”,就是为了从字面上与此区别)不是property类实例本身,而是进行自动的调用转,也就是说,Python会根据属性的访问类别(获取、赋值、删除)自动调用相应的fget、fset、fdel成员方法并得出结果,这也正是“特性”的本质所在——“特性”能够通过自定义类实例去自动管理自定义类实例的属性访问控制(其实还可以做些其它的工作,甚至不管理自定义类实例的属性访问控制——请参见2.1中对fget、fset、fdel的描述说明),我们一般称此为“特性拦截了属性的访问”。
我们介绍完了“特性”(即自定义类的类属性——property类实例)的访问,我们再看看自定义类实例的属性访问控制。既然是自定义类实例的属性,那么就只能通过自定义类实例来访问。
当我们通过自定义类实例来访问其自身的属性时,其访问控制是由__getattr__、__getattribute__、__setattr__和__delattr__四个重载操作符来完成的,与“特性”(以及后面讲到的“描述符”)无关,并且其访问方式和正常的访问一样。
至此,我们已经基本上讲完通过成员方法来使用“特性”了。最后,我们要注意两点:第一,一个“特性”只能控制自定义类的一个属性的访问控制,如果想控制多个,就必须定义多个“特性”;第二,在通过“特性”来控制自定义类实例的属性访问控制时,“特性”的名字不能和它所控制的自定义类实例的属性的名字相同。为什么不能一样呢?我们试想,当一样时,如果我们在“特性”的成员方法中再次访问了该属性,它就会触发属性的获取、赋值或删除操作,那么由于“特性”会拦截该访问控制,所以就会再次引发该成员方法的调用,这样,就会导致递归调用而进入死循环,直到内存耗尽。当然,如果你不会在“特性”的成员方法中再次访问该属性,就不会导致递归调用。
2.2.1.3 例子
现在,我们已讲解完“特性”的理论知识,再看个例子或许就更清楚了。
注:此例适用于Python2.6中的新式类和Python3中的所有类(因为Python3中的类都是新式类),不适用于Python2.6旧式类。在下面例子中,“#”后面的注释是相应的语言的输出结果,除了说明不是注释外。
class A:
def __init__(self, value):
print(“init …”)
self._x = 0
def getx(self):
print(“getx …”)
return self._x
def setx(self, value):
print(“setx …”)
self._x = value
def delx(self):
print(“delx …”)
del self._x
x = property(getx, setx, delx)
a = A() # init ...
a.x # getx ...
# 0
a.x = 123 # setx ...
a.x # getx ...
# 123
a._x # 123
a._x = 456 # 这个注释不是输出,它不会输出任何东西
a.x # getx ...
# 456
del a.x # delx ...
del a._x # 这个注释不是输出,这句语句将抛出异常
a.x = 789 # setx ...
del a.x # delx ...
a._x = 100 # 这个注释不是输出,它不会输出任何东西
a.x # 100
上述代码讲解:
我们定义了一个自定义类A,然后定义了三个成员方法getx、setx、delx,这三个成员方法分别作为参数传递给property内置函数,通过property内置函数隐式地创建了一个property类实例,并把该实例作为返回值传递给了自定义类A的成员x(即类属性),换句话说,就是x是一个property类实例,其中getx、setx、delx三个成员方法将成为property类的三个成员方法(fget、fset、fdel)。我们就是通过这个x类属性来管理_x实例属性的访问控制。
然后我们通过“a = A( )”语句定义了一个A类的实例a,它将调用构造函数__init__。a.x将调用property类的fget成员方法(即A类中的getx方法,下面直接简单地说“setx”和“delx”,而不再提“fset”和“fdel”);“a.x = 123”语句将调用“setx”方法;“a.x”语句将调用“getx”方法;“a._x”语句将调用A类中的默认的属性获取方法(你可以重载__getattr__和__getattribute__操作符来重载默认的属性获取方法,参见上一章节的“重载操作符”);“a._x = 456”语句将调用A类中的默认的属性赋值方法(你可以重载__setattr__操作符);“del a.x”语句将调用“delx”方法;“del a._x”语句将调用A类中的默认的属性删除方法(你可以重载__delattr__操作符)。当执行了“del a.x”语句后,再执行“del a._x”语句时,将引发异常,原因是:“del a.x”语句调用“delx”方法后,a的属性_x已经被删除、不存在了,如果再执行“del a._x”语句,就要引发“与属性不存在性有关的”异常了(删除一个不存在的属性)。当接着执行语句“a.x = 789”,将会调用“setx”方法;“del a.x”语句将调用“delx”方法方法;“a._x = 100”语句将调用A类中默认的属性赋值方法;“a.x”语句将调用“getx”方法。
2.2.2 使用装饰器方法
我们可以使用装饰器来使自定义类的方法成为“特性”,装饰器的接口比较简洁。
property类有三个装饰器方法(getter、setter、deleter),按照装饰器的一般使用方法,如果我们把一个方法成为property类的fget方法,就需要调用property的getter装饰器方法。但是,上面我们已经知道,property类是隐式的,所以,我们不能直接使用getter装饰器(就是想使用也没有办法使用——因为我们不能直接获取到property类),所以,我们还需要借助property内置函数。
2.2.2.1 使用方法
property不仅是可以作为内置函数来使用,而且还可以作为装饰器来使用。当我们把property内置函数当作装饰器来使用时,它将会隐式的创建一个property类实例,并调用该实例的getter装饰器,使得property装饰器所装饰的方法成为property类的fget成员方法。最终,由property将重新绑定它所装饰的方法,使它所装饰的方法名重新绑定到新创建的property类实例,而它所装饰的方法成为该property类实例的fget方法。
假设property所装饰的方法的名字为name,那么,从此以后,我们可以使用name.setter和name.deleter来装饰其它的方法,使其成为fset和fdel成员方法,因为,此时的name被property装饰器重新绑定到新创建的property类实例,也就是说,name是一个property类实例。注:在用name.setter和name.deleter装饰其它方法时,其它方法必须是name。总之,用setter和deleter装饰器修饰的方法的名字必须和property装饰器修饰的方法的名字相同。
2.2.2.2 例子
注:此例适用于Python2.6中的新式类和Python3中的所有类(因为Python3中的类都是新式类),不适用于Python2.6旧式类。
class Person:
def __init__(self, a_name):
self._name = a_name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@name.deleter
def name(self):
del self._name
person = Person(“student”)
person.name
person.name = “teacher”
del person.name
person._name # 会抛出异常
person._name = “123”
del person._name
在类Person中,property装饰器将隐式地创建一个property类实例,然后把它所装饰的方法(第一个name方法)重新绑定到该property类实例,并把第一个name方法变成该property类实例的fget方法。接着分别用name.setter和name.deleter装饰器分别第二个、第三个name方法变成name的fset和fdel方法。在类Person定义结束后,我们定义了一个变量person,然后对person对象中name属性的访问控制就分别调用相应的三个name方法。实际上,我们对name属性的操作,最终会转移到类Person实例的_name属性身上。
2.3 小结
特性是在自定义类的成员中创建一个property类的实例(该property类实例是自定义类的属性,不是自定义类实例的属性),然后通过该property类实例来管理自定义类实例中的某个特定的属性。该property类实例是自定义类中的一个属性(我们在这称之为“类属性”),当通过自定义类对象访问它时,是直接访问该类属性;当通过自定义类实例访问它时,该类属性的三个相应的属性管理方法(fget,fset,fdel)将会自动调用。在这三个属性管理方法中,我们可以像其他普通函数或类方法一样做任何事情,不过,我们一般是把对该类属性的访问操作(获取、赋值、删除)转移到自定义类实例的属性身上。
在访问通过特性创建的类的属性时,装饰特性的三个方法会自动调用。
特性是在类的属性(注:该类属性也是一个对象,因此也是可以有属性的)中创建一个属性,对该属性的所有操作(获取、赋值、删除)都将作用在类实例的属性身上。特性是用来管理类实例的属性访问的,而不是管理类本身的属性访问的(即,特性不会作用在类属性上,只会作用在类实例的属性上)。当然,我们也可以跳过特性而直接访问类实例的属性,此时,访问操作(获取、赋值、删除)与特性无关,只受__getattr__、__getattribute__、__setattr__、__delattr__四个重载操作符的影响。当使特性时,其属性管理只受特性(的三个方法)所影响,不受那些重载操作符的影响。
一旦一个自定义类使用了“特性”来管理自定义类实例某个属性,那么凡是通过“特性”来对该属性的所有访问(获取、赋值、删除)都有“特性”的三个成员方法(fget、fset、fdel)来控制。这里隐含着一个事实,前面没有说明,这里必须说明一下:如果“特性”中的三个成员方法有任何一个没有被定义,那么,就不能通过“特性”来进行相应的操作;如果进行了这样的操作,那么将会引发AttributeError异常(Python找不到相应的方法来调用)。比如:如果没有定义fset成员方法,那么就不能通过“特性”来对被该“特性”所管理的属性进行赋值操作;否则,将引发AttributeError异常。
3 描述符
特性只是创建一个特定类型的描述符的一种简化方式,即:可以把特性看成是简化了的、受限的描述符;换句话,你可以这样理解,“特性”是Python内部已经实现好的一个描述符,但它被固定了,也就是说,它没有你自己定义的描述符的功能那么强大。因此,笔者不打算再过于详细的介绍描述符。这看起来可能有点反传统——按照传统的,我们应该是先介绍的描述符,再介绍特性,这里,我们反一传统,因为我们已经介绍过“特性”了,如果再详细的介绍描述符,就有点重复了,而只介绍描述符与“特性”不同的地方。
3.1 理解
描述符是一个类,类中定义了三个成员方法:__get__、__set__、__delete__。换句话说,只要一个类中定义了这三个方法中任何一个,那么,这个类就自动的成为一个描述符。
我们已经知道,特性可以看成是一个简化的描述符。
用于”特性“的property类就相当于一个描述符(类),你就可以把它看成一个描述符;property中的三个成员方法就相当于描述符的三个成员方法:fget相当于__get__、fset相当于__set__、fdel相当于__delete__。由于特性是一个简化了的描述符,所以,描述符的原理和特性的原理差不多,可以把特性的原理应用于描述符身上。
虽然理解描述符可以用特性的原理,但描述符本身没有内置装饰器功能。正所谓“祸兮福之所倚”,描述符反而比特性有更多的自由——描述符可以使用所有的OOP功能,因为“特性”的property类是隐式的(你不能控制它),而描述符类是显示的,可以由你来控制。因此,我们可以把描述符所管理的自定义类实例属性的值存储在描述符类实例中,而不是自定义类实例中(当然,也可以存储在自定义类实例中,甚至可以同时在两者中都存储);而“特性”所管理的自定义类实例属性的值只能存储在自定义类实例中。
3.2 例子
注:此例适用于Python2.6中的新式类和Python3中的所有类(因为Python3中的类都是新式类),不适用于Python2.6旧式类。
class Name:
def __init__(self, value):
self._name = value
def __get__(self, instance, owner):
print(‘fetch ...’)
print(self._name)
return instance._name
def __set__(self, instance, value):
print(‘change ...’)
instance._name = value
self._name = value
def __delete__(self, instance):
print(‘remove ...’)
del instance._name
del self._name
class Person:
def __init__(self, name):
self._name = name
name = Name(“” )
说明:同“特性”的限制一样,Person类中的类属性name和Person类实例的属性_name不能同样,因为这将导致递归调用而进入死循环。但是,不像“特性”,描述符也可以把它所管理的属性放在自身身上,因此,在此例中,为了展现这一点,笔者把它所管理的属性同时存储在了它自身和Person类实例身上。
3.3 小结
总之,和特性一样,描述符类就相当于一个转接类,把对一个变量(自定义类的类属性)的访问控制转接(或嫁接)到另一个变量(自定义类的实例属性)身上。
特性和描述符一次只能用来管理一个单个的、特定的属性,既一个特性或描述符对应一个属性;如果想要管理多个属性,就必须定义多个特性和描述符。
4 三者之间的关系
(1)特性充当个特定角色,而描述符更为通用。特性定义特定属性的获取、设置和删除功能。描述符也提供了一个类,带有完成这些操作的方式,但是,它们提供了额外的灵活性以支持更多任意行为。实际上,特性真的的只是创建特定描述符的一种简单方法——即在属性访问上运行的一个描述符。编码上也有区别:特性通过一个内置函数创建,而描述符用一个类来编码;同样,描述符可以利用类的所有常用OOP功能,例如:继承。此外,除了实例的状态信息,描述符有它们自己的本地状态,因此,它们可以避免在实例中的名称冲突。
(2)__getattr__、__getattribute__、__setattr__和__delattr__方法更为通用:它们用来捕获任意多的属性。相反,每个特性或描述符只针对一个特定属性提供访问拦截——我们不能用一个单个的特性或描述符捕获每个属性获取。其实现也不同:__getattr__、__getattribute__、__setattr__和__delattr__是操作符重载方法,而特性和描述符是手动赋给类属性的对象。