|
| 1 | +>我们既蒙怜悯,受了这职分,就不丧胆,乃将那些暗昧可耻的事弃绝了,不行诡诈,不谬讲神的道理,只将真理表明出来,好在神面前把自己荐与各人的良心。(2 CORINTHIANS 4:1-2) |
| 2 | +
|
| 3 | +#黑魔法 |
| 4 | + |
| 5 | +围绕类的话题,真实说也说不完,仅特殊方法,除了前面遇到过的`__init__()`,`__new__()`,`__str__()`等之外,还有很多。虽然它们仅仅是在某些特殊情景中使用,但是,因为本教程是“From Beginner to Master”。当然,不是学习了类的更多特殊方法就能达到Master水平,但是这是通往Master的一步。 |
| 6 | + |
| 7 | +本节试图再介绍一些点“黑魔法”,既能窥探到Python的更高境界,也能感受到Master的未来能力。俗话说“艺不压身”,还是多学点好。 |
| 8 | + |
| 9 | +##优化内存的`__slots__` |
| 10 | + |
| 11 | +首先声明,`__slots__`能够限制属性的定义,但是这不是它存在终极目标,它存在的终极目标更应该是一个在编程中非常重要的方面:优化内存使用。 |
| 12 | + |
| 13 | + >>> class Spring(object): |
| 14 | + ... __slots__ = ("tree", "flower") |
| 15 | + ... |
| 16 | + >>> dir(Spring) |
| 17 | + ['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'flower', 'tree'] |
| 18 | + |
| 19 | +仔细看看`dir()`的结果,还有`__dict__`属性吗?没有了,的确没有了。也就是说`__slots__`把`__dict__`挤出去了,它进入了类的属性。 |
| 20 | + |
| 21 | + >>> Spring.__slots__ |
| 22 | + ('tree', 'flower') |
| 23 | + |
| 24 | +这里可以看出,类Spring有且仅有两个属性。 |
| 25 | + |
| 26 | + >>> t = Spring() |
| 27 | + >>> t.__slots__ |
| 28 | + ('tree', 'flower') |
| 29 | + |
| 30 | +实例化之后,实例的`__slots__`与类的完全一样,这跟前面的`__dict__`大不一样了。 |
| 31 | + |
| 32 | + >>> Spring.tree = "liushu" |
| 33 | + |
| 34 | +通过类,先赋予一个属性值。然后,检验一下实例能否修改这个属性: |
| 35 | + |
| 36 | + >>> t.tree = "guangyulan" |
| 37 | + Traceback (most recent call last): |
| 38 | + File "<stdin>", line 1, in <module> |
| 39 | + AttributeError: 'Spring' object attribute 'tree' is read-only |
| 40 | + |
| 41 | +看来,我们的意图不能达成,报错信息中显示,`tree`这个属性是只读的,不能修改了。 |
| 42 | + |
| 43 | + >>> t.tree |
| 44 | + 'liushu' |
| 45 | + |
| 46 | +因为前面已经通过类给这个属性赋值了。不能用实例属性来修改。只能: |
| 47 | + |
| 48 | + >>> Spring.tree = "guangyulan" |
| 49 | + >>> t.tree |
| 50 | + 'guangyulan' |
| 51 | + |
| 52 | +用类属性修改。但是对于没有用类属性赋值的,可以通过实例属性赋值。 |
| 53 | + |
| 54 | + >>> t.flower = "haitanghua" |
| 55 | + >>> t.flower |
| 56 | + 'haitanghua' |
| 57 | + |
| 58 | +但此时: |
| 59 | + |
| 60 | + >>> Spring.flower |
| 61 | + <member 'flower' of 'Spring' objects> |
| 62 | + |
| 63 | +实例属性的值并没有传回到类属性,你也可以理解为新建立了一个同名的实例属性。如果再给类属性赋值,那么就会这样了: |
| 64 | + |
| 65 | + >>> Spring.flower = "ziteng" |
| 66 | + >>> t.flower |
| 67 | + 'ziteng' |
| 68 | + |
| 69 | +当然,此时在给`t.flower`重新赋值,就会爆出跟前面一样的错误了。 |
| 70 | + |
| 71 | + >>> t.water = "green" |
| 72 | + Traceback (most recent call last): |
| 73 | + File "<stdin>", line 1, in <module> |
| 74 | + AttributeError: 'Spring' object has no attribute 'water' |
| 75 | + |
| 76 | +这里试图给实例新增一个属性,也失败了。 |
| 77 | + |
| 78 | +看来`__slots__`已经把实例属性牢牢地管控了起来,但更本质是的是优化了内存。诚然,这种优化会在大量的实例时候显出效果。 |
| 79 | + |
| 80 | +书接上回,不管是实例还是类,都用`__dict__`来存储属性和方法,可以笼统地把属性和方法称为成员或者特性,一句话概括,就是`__dict__`存储对象成员。但,有时候访问的对象成员没有存在其中,就是这样: |
| 81 | + |
| 82 | + >>> class A(object): |
| 83 | + ... pass |
| 84 | + ... |
| 85 | + >>> a = A() |
| 86 | + >>> a.x |
| 87 | + Traceback (most recent call last): |
| 88 | + File "<stdin>", line 1, in <module> |
| 89 | + AttributeError: 'A' object has no attribute 'x' |
| 90 | + |
| 91 | +`x`不是实例的成员,用`a.x`访问,就出错了,并且错误提示中报告了原因:“'A' object has no attribute 'x'” |
| 92 | + |
| 93 | +在很多情况下,这种报错是足够的了。但是,在某种我现在还说不出的情况下,你或许不希望这样报错,或许希望能够有某种别的提示、操作等。也就是我们更希望能在成员不存在的时候有所作为,不是等着报错。 |
| 94 | + |
| 95 | +要处理类似的问题,就要用到本节中的知识了。 |
| 96 | + |
| 97 | +##属性拦截 |
| 98 | + |
| 99 | +有时候,访问某个类或者实例属性,它不存在,就会异常。对于异常,总是要处理的。就好像“寻隐者不遇”,却被童子“遥指杏花村”,将你“拦截”了,不至于因为“不遇”而垂头丧气。 |
| 100 | + |
| 101 | +在Python中,有一些方法就具有这种“拦截”能力。 |
| 102 | + |
| 103 | +- `__setattr__(self, name,value)`:如果要给name赋值,就调用这个方法。 |
| 104 | +- `__getattr__(self, name)`:如果name被访问,同时它不存在的时候,此方法被调用。 |
| 105 | +- `__getattribute__(self, name)`:当name被访问时自动被调用(注意:这个仅能用于新式类),无论name是否存在,都要被调用。 |
| 106 | +- `__delattr__(self, name)`:如果要删除name,这个方法就被调用。 |
| 107 | + |
| 108 | +用例子说明。 |
| 109 | + |
| 110 | + >>> class A(object): #Python 3: class A: |
| 111 | + ... def __getattr__(self, name): |
| 112 | + ... print "You use getattr" #Python 3: print("You use getattr"),下同,从略 |
| 113 | + ... def __setattr__(self, name, value): |
| 114 | + ... print "You use setattr" |
| 115 | + ... self.__dict__[name] = value |
| 116 | + ... |
| 117 | + |
| 118 | +类`A`除了两个方法,没有别的了。 |
| 119 | + |
| 120 | + >>> a = A() |
| 121 | + >>> a.x |
| 122 | + You use getattr |
| 123 | + |
| 124 | +`a.x`这个实例属性,本来是不存在的,但是,由于类中有了`__getattr__(self, name)`方法,当发现属性`x`不存在于对象的`__dict__`中的时候,就调用了`__getattr__`,即所谓“拦截成员”。 |
| 125 | + |
| 126 | + >>> a.x = 7 |
| 127 | + You use setattr |
| 128 | + |
| 129 | +给对象的属性赋值时候,调用了`__setattr__(self, name, value)`方法,这个方法中有一句`self.__dict__[name] = value`,通过这个语句,就将属性和数据保存到了对象的`__dict__`中,如果再调用这个属性: |
| 130 | + |
| 131 | + >>> a.x |
| 132 | + 7 |
| 133 | + |
| 134 | +它已经存在于对象的`__dict__`之中。 |
| 135 | + |
| 136 | +在上面的类中,当然可以使用`__getattribute__(self, name)`,并且,只要访问属性就会调用它。例如: |
| 137 | + |
| 138 | + >>> class B(object): |
| 139 | + ... def __getattribute__(self, name): |
| 140 | + ... print "you are useing getattribute" |
| 141 | + ... return object.__getattribute__(self, name) |
| 142 | + |
| 143 | +为了与前面的类区分,新命名一个类名字。需要提醒注意,在这里返回的内容用的是`return object.__getattribute__(self, name)`,而没有使用`return self.__dict__[name]`样式。因为如果用`return self.__dict__[name]`这样的方式,就是访问`self.__dict__`,只要访问这个属性,就要调用`__getattribute__``,这样就导致了无线递归下去(死循环)。要避免之。 |
| 144 | + |
| 145 | + >>> b = B() |
| 146 | + >>> b.y |
| 147 | + you are useing getattribute |
| 148 | + Traceback (most recent call last): |
| 149 | + File "<stdin>", line 1, in <module> |
| 150 | + File "<stdin>", line 4, in __getattribute__ |
| 151 | + AttributeError: 'B' object has no attribute 'y' |
| 152 | + >>> b.two |
| 153 | + you are useing getattribute |
| 154 | + Traceback (most recent call last): |
| 155 | + File "<stdin>", line 1, in <module> |
| 156 | + File "<stdin>", line 4, in __getattribute__ |
| 157 | + AttributeError: 'B' object has no attribute 'two' |
| 158 | + |
| 159 | +访问不存在的成员,可以看到,已经被`__getattribute__`拦截了,虽然最后还是要报错的。 |
| 160 | + |
| 161 | + >>> b.y = 8 |
| 162 | + >>> b.y |
| 163 | + you are useing getattribute |
| 164 | + 8 |
| 165 | + |
| 166 | +当给其赋值后,意味着已经在`__dict__`里面了,再调用,依然被拦截,但是由于已经在`__dict__`内,会把结果返回。 |
| 167 | + |
| 168 | +当你看到这里,是不是觉得上面的方法有点魔力呢?不错,的确是“黑魔法”。但是,它有什么具体应用呢?看下面的例子,能给你带来启发。 |
| 169 | + |
| 170 | + #!/usr/bin/env python |
| 171 | + # coding=utf-8 |
| 172 | + |
| 173 | + """ |
| 174 | + study __getattr__ and __setattr__ |
| 175 | + """ |
| 176 | + |
| 177 | + class Rectangle(object): #Python 3: class Rectangle: |
| 178 | + """ |
| 179 | + the width and length of Rectangle |
| 180 | + """ |
| 181 | + def __init__(self): |
| 182 | + self.width = 0 |
| 183 | + self.length = 0 |
| 184 | + |
| 185 | + def setSize(self, size): |
| 186 | + self.width, self.length = size |
| 187 | + def getSize(self): |
| 188 | + return self.width, self.length |
| 189 | + |
| 190 | + if __name__ == "__main__": |
| 191 | + r = Rectangle() |
| 192 | + r.width = 3 |
| 193 | + r.length = 4 |
| 194 | + print r.getSize() #Python 3: print(r.getSize()) |
| 195 | + r.setSize( (30, 40) ) |
| 196 | + print r.width #Python 3: print(r.width) |
| 197 | + print r.length #Python 3: print(r.length) |
| 198 | + |
| 199 | +上面代码来自《Beginning Python:From Novice to Professional,Second Edittion》(by Magnus Lie Hetland),根据本教程的需要,稍作修改。 |
| 200 | + |
| 201 | + $ python 21301.py |
| 202 | + (3, 4) |
| 203 | + 30 |
| 204 | + 40 |
| 205 | + |
| 206 | +这段代码已经可以正确运行了。但是,作为一个精益求精的程序员。总觉得那种调用方式还有可以改进的空间。比如,要给长宽赋值的时候,必须赋予一个元组,里面包含长和宽。这个能不能改进一下呢? |
| 207 | + |
| 208 | + #!/usr/bin/env python |
| 209 | + # coding=utf-8 |
| 210 | + |
| 211 | + """ |
| 212 | + study __getattr__ and __setattr__ |
| 213 | + """ |
| 214 | + |
| 215 | + class Rectangle(object): #Python 3: class Rectangle: |
| 216 | + """ |
| 217 | + the width and length of Rectangle |
| 218 | + """ |
| 219 | + def __init__(self): |
| 220 | + self.width = 0 |
| 221 | + self.length = 0 |
| 222 | + |
| 223 | + def setSize(self, size): |
| 224 | + self.width, self.length = size |
| 225 | + def getSize(self): |
| 226 | + return self.width, self.length |
| 227 | + |
| 228 | + size = property(getSize, setSize) |
| 229 | + |
| 230 | + if __name__ == "__main__": |
| 231 | + r = Rectangle() |
| 232 | + r.width = 3 |
| 233 | + r.length = 4 |
| 234 | + print r.size |
| 235 | + r.size = 30, 40 |
| 236 | + print r.width |
| 237 | + print r.length |
| 238 | + |
| 239 | +以上代码的运行结果同上。但是,因为加了一句`size = property(getSize, setSize)`,使得调用方法是不是更优雅了呢?原来用`r.getSize()`,现在使用`r.size`,就好像调用一个属性一样。难道你不觉得眼熟吗?在[《多态和封装》](./211.md)中已经用到过property函数了,虽然写法略有差别,但是作用一样。 |
| 240 | + |
| 241 | +本来,这样就已经足够了。但是,因为本节中出来了特殊方法,所以,一定要用这些特殊方法从新演绎一下这段程序。虽然重新演绎的不一定比原来的好,主要目的是演示本节的特殊方法应用。 |
| 242 | + |
| 243 | + #!/usr/bin/env python |
| 244 | + # coding=utf-8 |
| 245 | + |
| 246 | + class NewRectangle(object): |
| 247 | + def __init__(self): |
| 248 | + self.width = 0 |
| 249 | + self.length = 0 |
| 250 | + |
| 251 | + def __setattr__(self, name, value): |
| 252 | + if name == "size": |
| 253 | + self.width, self.length = value |
| 254 | + else: |
| 255 | + self.__dict__[name] = value |
| 256 | + |
| 257 | + def __getattr__(self, name): |
| 258 | + if name == "size": |
| 259 | + return self.width, self.length |
| 260 | + else: |
| 261 | + raise AttributeError |
| 262 | + |
| 263 | + if __name__ == "__main__": |
| 264 | + r = NewRectangle() |
| 265 | + r.width = 3 |
| 266 | + r.length = 4 |
| 267 | + print r.size #Python 3: print(r.size) |
| 268 | + r.size = 30, 40 |
| 269 | + print r.width #Python 3: print(r.width) |
| 270 | + print r.length #Python 3: print(r.length) |
| 271 | + |
| 272 | +除了类的样式变化之外,调用样式没有变。结果是一样的。 |
| 273 | + |
| 274 | +如果要对于这种黑魔法有更深的理解,可以阅读:[Python Attributes and Methods](http://www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html),读了这篇文章,对Python的对象属性和方法会有更深入的理解。 |
| 275 | + |
| 276 | +至此,是否注意到,我们使用了很多以双下划线开头和结尾的方法或者属性,比如`__dict__`,`__init__()`等。在Python中,用这种方法表示特殊的方法和属性,当然,这是一个惯例,之所以这样做,主要是确保这些特殊的名字不会跟你自己所定义的名称冲突,我们自己定义名称的时候,是绝少用双划线开头和结尾的。如果你需要重写这些方法,当然是可以的。 |
| 277 | + |
| 278 | +------ |
| 279 | + |
| 280 | +[总目录](./index.md) | [上节:定制类](./239.md) | [下节:迭代器](./214.md) |
| 281 | + |
| 282 | +如果你认为有必要打赏我,请通过支付宝: **[email protected]**,不胜感激。 |
0 commit comments