- Preface 前言
- 第1章 引论
- 第2章 编程惯用法
- 第3章 基础语法
- 建议19:有节制地使用 from…import 语句
- 建议20:优先使用 absolute import 来导入模块
- 建议21:i+=1 不等于 ++i
- 建议22:使用 with 自动关闭资源
- 建议23:使用 else 子句简化循环(异常处理)
- 建议24:遵循异常处理的几点基本原则
- 建议25:避免 finally 中可能发生的陷阱
- 建议26:深入理解 None 正确判断对象是否为空
- 建议27:连接字符串应优先使用 join 而不是 +
- 建议28:格式化字符串时尽量使用 .format 方式而不是 %
- 建议29:区别对待可变对象和不可变对象
- 建议30:[]、() 和 {}:一致的容器初始化形式
- 建议31:记住函数传参既不是传值也不是传引用
- 建议32:警惕默认参数潜在的问题
- 建议33:慎用变长参数
- 建议34:深入理解 str() 和 repr() 的区别
- 建议35:分清 staticmethod 和 classmethod 的适用场景
- 第4章 库
- 建议36:掌握字符串的基本用法
- 建议37:按需选择 sort() 或者 sorted()
- 建议38:使用 copy 模块深拷贝对象
- 建议39:使用 Counter 进行计数统计
- 建议40:深入掌握 ConfigParser
- 建议41:使用 argparse 处理命令行参数
- 建议42:使用 pandas 处理大型 CSV 文件
- 建议43:一般情况使用 ElementTree 解析 XML
- 建议44:理解模块 pickle 优劣
- 建议45:序列化的另一个不错的选择 JSON
- 建议46:使用 traceback 获取栈信息
- 建议47:使用 logging 记录日志信息
- 建议48:使用 threading 模块编写多线程程序
- 建议49:使用 Queue 使多线程编程更安全
- 第5章 设计模式
- 第6章 内部机制
- 建议54:理解 built-in objects
- 建议55:init() 不是构造方法
- 建议56:理解名字查找机制
- 建议57:为什么需要 self 参数
- 建议58:理解 MRO 与多继承
- 建议59:理解描述符机制
- 建议60:区别 getattr() 和 getattribute() 方法
- 建议61:使用更为安全的 property
- 建议62:掌握 metaclass
- 建议63:熟悉 Python 对象协议
- 建议64:利用操作符重载实现中缀语法
- 建议65:熟悉 Python 的迭代器协议
- 建议66:熟悉 Python 的生成器
- 建议67:基于生成器的协程及 greenlet
- 建议68:理解 GIL 的局限性
- 建议69:对象的管理与垃圾回收
- 第7章 使用工具辅助项目开发
- 第8章 性能剖析与优化
建议59:理解描述符机制
除了在不同的局部变量、全局变量中查找名字,还有一个相似的场景不可不察,那就是查找对象的属性。在Python中,一切皆是对象,所以类也是对象,类的实例也是对象。
>>> class MyClass(object): ... class_attr = 1 ... >>> MyClass.__dict__ dict_proxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>, ' __module__': '__main__', '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None, ' class _attr': 1})
每一个类都有一个__dict__属性,其中包含的是它的所有属性,又称为类属性。留意类属性的最后一个元素,可以看到我们代码中定义的属性在其中的体现。
>>> my_instance = MyClass() >>> my_instance.__dict__ {}
除了与类相关的类属性之外,每一个实例也有相应的属性表(__dict__),称为实例属性。当我们通过实例访问一个属性时,它首先会尝试在实例属性中查找,如果找不到,则会到类属性中查找。
>>> my_instance. class _attr 1
可以看到实例my_instance可以访问类属性class_attr。但与读操作有所不同,如果通过实例增加一个属性,只能改变此实例的属性,对类属性而言,并没有丝毫变化。这从下面的代码中可以得到印证。
>>> my_instance.inst_attr = 'china' >>> my_instance.__dict__ {'inst_attr': 'china'} >>> MyClass.__dict__ dict_proxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>, ' __module__': '__main__', '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None, ' class _attr': 1})
那么,能不能给类增加一个属性呢?答案是,能,也不能。说能,是因为每一个class也是一个对象,动态地增减对象的属性与方法正是Python这种动态语言的特性,自然是支持的。
>>> MyClass.class_attr2 = 100 >>> my_instance.class_attr2 100
说不能,是因为在Python中,内置类型和用户定义的类型是有分别的,内置类型并不能够随意地为它增加属性或方法。
>>> str.new_attr = 1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't set attributes of built-in/extension type 'str' >>> setattr(str, 'new_attr', 1) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't set attributes of built-in/extension type 'str'
至此,我们应当理解了,当我们通过“.”操作符访问一个属性时,如果访问的是实例属性,与直接通过__dict__属性获取相应的元素是一样的;而如果访问的是类属性,则并不相同:“.”操作符封装了对两种不同属性进行查找的细节。
>>> my_instance.__dict__['inst_attr'] 'china' >>> my_instance.__dict__['class_attr2'] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'class_attr2'
不过,这里要讲的并不止于此,“.”操作符封装了对实例属性和类属性查找的细节,只讲了一半事实,还有一部分隐而未谈,那就是描述符机制。
>>> MyClass.__dict__['inst_attr'] 'china' >>> MyClass.inst_attr 'china'
我们已经知道访问类属性时,通过__dict__访问和使用“.”操作符访问是一样的,但如果是方法,却又不是如此了。
>>> class MyClass(object): ... def my_method(self): ... print 'my_method' ... >>> MyClass.__dict__['my_method'] <function my_method at 0x102773aa0> >>> MyClass.my_method <unbound method MyClass.my_method>
甚至它们的类型都不一样!
>>> type(MyClass.my_method) <type 'instancemethod'> >>> type(MyClass.__dict__['my_method']) <type 'function'>
这其中作怪的就是描述符了。当通过“.”操作符访问时,Python的名字查找并不是之前说的先在实例属性中查找,然后再在类属性中查找那么简单,实际上,根据通过实例访问属性和根据类访问属性的不同,有以下两种情况:
一种是通过实例访问,比如代码obj.x,如果x是一个描述符,那么__getattribute__()会返回type(obj).__dict__['x'].__get__(obj,type(obj))结果,即:type(obj)获取obj的类型;type(obj).__dict__['x']返回的是一个描述符,这里有一个试探和判断的过程;最后调用这个描述符的__get__()方法。
另一种是通过类访问的情况,比如代码cls.x,则会被__getattribute__()转换为cls.__dict__['x'].__get__(None,cls)。
至此,就能够明白MyClass.__dict__['my_method']返回的是function而不是instancemethod了,原因是没有调用它的__get__()方法。是否如此呢?怎么验证一下?我们可以尝试手动调用__get__()。
>>> t = f.__get__(None, MyClass) >>> t <unbound method MyClass.my_method> >>> type(t) <type 'instancemethod'>
看,果然是这样!这是因为描述符协议是一个Duck Typing的协议,而每一个函数都有__get__方法,也就是说其他每一个函数都是描述符。
描述符机制有什么作用呢?其实它的作用编写一般程序的话还真用不上,但对于编写程序库的读者来说,就非常有用了。比如大家熟悉的已绑定方法和未绑定方法,它是怎么来的呢?
>>> MyClass.my_method <unbound method MyClass.my_method> >>> a = MyClass() >>> a.my_method <bound method MyClass.my_method of <__main__.MyClass object at 0x10277a490>>
上面例子输出的不同,其实来自于对描述符的__get__()的调用参数的不同,当以obj.x的形式访问时,调用参数是__get__(obj,type(obj));而以cls.x的形式访问时,调用参数是__get__(None,type(obj)),这可以通过未绑定方法的im_self属性为None得到印证。
>>> print MyClass.my_method.im_self None >>> a.my_method.im_self <__main__.MyClass object at 0x10277a490>
除此之外,所有对属性、方法进行修饰的方案往往都用到了描述符,比如classmethod、staticmethod和property等。在这里,给出property的参考实现作为本节的结束,更深入的应用可以进一步参考Python源码中的其他用法。
class Property(object): "Emulate PyProperty_Type() in Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError, "unreadable attribute" return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError, "can't set attribute" self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError, "can't delete attribute" self.fdel(obj)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论