阅读实际使用的开源项目如flask,对于提高编程能力有巨大好处。flask是实现网站功能,使用数据库的一个编程框架,已有中文出版有关书籍介绍。本系列讲座涉及的是非常精彩的The Flask Mega-Tutorial,开始于2017年12月6日开始,结束于2018年5月结束,每周一课。
对于一个不太大,但足够复杂的app的代理current_app的实现方法,本系列讲座进行彻底分析,分析过程像美食家需要慢慢品味精美大餐,以体会高明厨师的精巧设计和制作。本系列首先比较各种看起来像“属性”的东西,提示一下不常用的方法,相当于在吃主餐前,品尝精美餐前小吃。
系列(三)
餐前小吃(3),还有三种属于属性王国的对象
Property属性
descriptor描述符
__slots__槽口
实例名.属性名,
1、 开篇
在系列(一)介绍的属性是attribute属性,有三个特征:
通过把文字、变量、或者这两者的表达式赋值给一个变量,创建类变量或实例变量;访问属性的方法是getaddr、setaddr函数,或通过getaddr、setaddr与__getaddr__、__setaddr__;
存储与字典__dict__相关。
本文涉及的三种对象,都是类中的变量或形式上的“类变量”,这些变量都是另一个类的实例,不再是由文字、变量、或者这两者的表达式赋值而创建的简单变量;python访问这种变量对象时,会访问另一个类,使用其中的处理方法,而不用getaddr、setaddr等;属性变量可以与字典__dict__无关。
2、 Property属性
Property属性:有一个形式上的“类变量”,是property类的实例,python访问属性时,会使用此property中自定义的函数。
在下面的例子
https://docs.python.org/3.6/library/functions.html?highlight=property#property
中的一个例子,其中第5行
x = property(getx, setx, delx, "I'm the 'x' property.")
表明类变量x是property类的实例。上面说python访问这种变量对象时,会访问另一个类,使用其中的处理方法。本例将访问property的方法,其中当读属性x时调用第一个参数的方法(本例是getx函数)、写x时调用第二个参数的方法(本例是setx函数)、删除时调用第三个(setx函数)、以及第四个doc说明。下面是程序:
class C1: def __init__(self): self._x = None def getx(self): return self._x def setx(self, value): self._x = value def delx(self): del self._x x = property(getx, setx, delx, "I'm the 'x' property.") c1 = C1() c1.x = 1 print(c1.x)
上述程序输出为1。
可以分步设置getx、setx、delx。先使用getx来创建property的实例x:
x = property(getx, doc="I'm the 'x' property.")
还可以把getx改成x,这样除了函数定义处getx改成x,上述语句也把getx改成x:
x = property(x, doc="I'm the 'x' property.")
随后使用实例x调用x.setter、 x.deleter函数,分成三次设置,与创建x时,一次设置效果一样,property使用比较灵活。相应变化的部分代码:
def x(self): return self._x x = property(x, doc="I'm the 'x' property.") def setx(self, value): self._x = value x = x.setter(setx) def delx(self): del self._x x = x.deleter(delx)
可以使用在在第二课中介绍的装饰器,用@x.setter、@x.deleter,完全等价的相关部分代码为:
@x.setter def setx(self, value): self._x = value @x.deleter def delx(self): del self._x
Property功能强大,setx、delx的名称也可使用x,在上述代码中代入x。并且使用装饰类来引入property,等效代码为:
class C5:
def __init__(self):
self._x= None
@property
def x(self):
"""I'm the 'x' property."""
return self._x
@x.setter
def x(self, value):
self._x= value
@x.deleter
def x(self):
del self._x
c5 = C5()
c5.x = 5
print(c5.x)
所以从python语法上,property就是装饰器修饰property类,没有增加新的语法。其中property允许一次或分布设置。看一下形式上的“类变量”x:
>>>C5.__dict__
mappingproxy({
'__module__': 'my_app2.flask3_2',
'__init__': <function C5.__init__at 0x02621F18>,
'x': <property object at0x02620B10>,
'__dict__': <attribute '__dict__'of 'C5' objects>,
'__weakref__': <attribute'__weakref__' of 'C5' objects>,
'__doc__': None})
注意:虽然x看起来像是类变量,但是实际上python是调用相应的方法,这些方法具体实现,决定了x是类变量或实例变量。本例C5中,对x的访问,实际上是对实例变量_x的访问,所以访问x是访问实例变量。
对于不考虑删除的,无需有@x.deleter部分。对于也不修改的只读的属性,@x.setter部分也可没有,这样变成只读。
使用property属性的好处:
增加功能,如对输入范围进行限制,规定x的范围为0~1000,则只要修改setter部分。如要隐藏了变量,可以使用__x。根据规则,双划线开始的变量被隐藏(但结尾再加上双划线,这是特殊属性,例如__dict__)。如果使用_x,变量未隐藏,可以直接改变_x,从而跳过限制。
@x.setter def x(self, x): if x < 0: self.__x = 0 elif x > 1000: self.__x = 1000 else: self.__x = x
3、 descriptor描述符
descriptor描述符:
变量是一个称为descriptor类的实例。descriptor类是指在其定义中,至少有__get__(),__set__(),或__delete__()这三个方法之一。
根据文章中的定义,descriptor是一个有绑定现象的对象的属性,绑定现象是指属性的访问方法被重载。属性访问的方法有__get__(),__set__(),和 __delete__()。只要为对象重写了其中任一方法,就说这个对象是一个descriptor对象。
这里出现了系列(一)中的__get__,当__get__报错时,会试图使用__getter__。
下面例子来自http://www.cnblogs.com/btchenguang/archive/2012/09/18/2690802.html,x与y都是MyClass中定义的类属性,其中x是RevealAccess类的实例。由于RevealAccess重写了__get__()等方法,所以x还是descriptor。
class RevealAccess(object): """创建一个Descriptor类,用来打印出访问它的操作信息 """ def __init__(self, initval=None, name='var'): self.val = initval self.name = name def __get__(self, obj, objtype): print('Retrieving: obj=%s, objtype=%s' % (str(obj), str(objtype)) ) print('objtype.__dict__=', objtype.__dict__) print('obj.y=', obj.y) print('Retrieving: self=%s' % (self.name) ) return self.val def __set__(self, obj, val): print('Updating' , self.name) self.val = val #使用Descriptor class MyClass(object): #生成一个Descriptor实例,赋值给类MyClass的x属性 x = RevealAccess(10, 'var "x"') y = 5 #普通类属性
其中本文为RevealAccess的__get__增加了三行print,以报告obj与objtype等信息,其中objtype是类变量x所在的类,本例是MyClass,obj是MyClass的实例。所以使用两者可以进行任何MyClass的类属性与实例属性所能进行的操作。
在下面的测试中,m是MyClass的实例,在MyClass的定义中包含类变量x,x还是一个descriptor。打印出来的obj的位置信息0x027961D0,的确与后面打印的str(m)中的对象m的位置信息一致。在RevealAccess中,还能得知obj.y= 5。
>>>frommy_app2.flask3_3 import *
>>>m = MyClass()
>>>m.x
Retrieving: obj=<my_app2.flask3_3.MyClass object at 0x027961D0>,objtype=<class 'my_app2.flask3_3.MyClass'>
objtype.__dict__= {'__module__': 'my_app2.flask3_3', 'x':<my_app2.flask3_3.RevealAccess object at 0x0260B9D0>, 'y': 5, '__dict__':<attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute'__weakref__' of 'MyClass' objects>, '__doc__': None}
obj.y= 5
Retrieving: self=var "x"
10
>>>str(m) #比对出obj是实例
'<my_app2.flask3_3.MyClass object at 0x027961D0>'
注意:在本例中,x是类变量,所以对另一个MyClass的实例n,总有n.x与m.x是一个对象。
有了property的灵活性,为什么还要有descriptor?
答案是为了重复使用。例如我们知道最低温度为-273.16度,可以做一个descriptor,限定最低温度,然后在其它使用温度的地方使用此descriptor。
还有下面的__slot__,也是使用descriptor。
4、 槽口__slots__
使用__slots__可以限定只能使用位于__slots__语句中的属性:
class S(object): __slots__ = ['val']
在__slot__中有属性’val’,可以写读,其它属性如’new’不在__slot__中,不可创建。
>>>x = S() >>>x.val = 42 >>>print(x.val) 42 >>>x.new = "not possible" Traceback (most recent call last): File "<input>", line 1, in <module> AttributeError: 'S' object has no attribute 'new'
Python为__slots__中的名称,内部建立一个descriptor,例如本例为__slots__中的名称val,创建一个类变量,它的值是一个descriptor类的实例。内部建立的descriptor,对类变量(例如本例的val)进行访问,可以不利用字典__dict__,所以可以没有字典__dict__。
由于没有字典__dict__,不在__slots__中的变量,就无法访问了。所以上面试图创建x.new失败。
__slots__也可有包含__dict__,这样就可以动态创建属性了。
目的可能有:
为了避免字典__dict__过于庞大,占用存储,在python3.3之前尤其如此。
为了避免低效的字典__dict__访问方式。
因为__weakref__属性与还有类的继承等,学究可以阅读下例Python的3.6.5的说明__slots__,掌握全部细节:
__slots__允许显式申明数据成员(如property),但不会生成__dict__和__weakref__(除非显式在__slots__中申明或在父类中可访问)。
不用__dict__,省下的内存可很可观。
Object.__slots__:此__slots__类变量可以被后面三种方式赋值:字符串、迭代或字符串序列,这些值均是可被实例使用的变量的名称。__slots__为申明的变量保留空间,并避免自动为每一实例生成__dict__和__weakref__。
下面是注意事项。__slots__会有这么多注意事项,表示惊奇得张大嘴。但是只要记住__slots__与descriptor的联系,大部分还是容易记住的。
注意:
- 当继承没有使用__slots__的类时,实例的__dict__和__weakref__属性都是可用的。
- 没有__dict__变量,实例不能对没有列入__slots__定义的新变量赋值。试图对未列入的变量名称赋值将引发AttributeError。如果需要动态对新的动态变量进行赋值,则在__slots__的定义的字符串序列中,加上__dict__。
- 没有针对每一个实例的__weakref__变量,类定义__slots__不能支持对它的实例的弱引用。如果需要支持弱引用,则在__slots__的定义的字符串序列中,加上__weakref__。
- __slots__的实现方法是在类这一层次上,为每一个变量名称创立描述符descriptor。结果是,类属性不能用来对__slots__定义的实例变量设置默认值,否则类属性将覆盖描述符的赋值。
注释:如果申明了一个类变量,并在__slots__中包含此名称,一方面,会由于申明类变量,有了一个类变量;另一方面,会由于是descriptor,python又建立一个同名的类变量,这样产生了两个同名类变量,所以会产生问题。
- 申明__slots__的作用并不局限于它定义所在的类。在父类中定义的__slots__,对子类也有效。但是继承的子类会有__dict__与__weakref__类变量的,除非它们也定义的__slots__(它只需包含任何其它的槽口)。
- 如果在一个类中所定义的槽口,在其基类中也定义了,则由基类槽口所定义的实例变量不可访问(除非直接从基类复原它的descriptor)。这使得程序的意义未定。将来可能增加一个检查来预防。
- 继承于可变长度的(如int,bytes和tuple)类,不能使用非空的__slots__。
- 非字符的迭代可以赋值给__slots__。也可使用映射,但将来会对每个键所对应的值指定特殊含义。
- 仅当两个类有相同的__slots__,__class__的赋值才有效。
注释:__class__的赋值是指:x.__class__ = y.__class__
- 继承多个有槽口的父类也是可以的,但只允许一个父类有槽口定义的属性。