本文摘自php中文网,作者coldplay.xixi,侵删。
python视频教程栏目介绍认识Python类的内部。
这篇文章和大家一起聊一聊 Python 3.8 中类和对象背后的一些概念和实现原理,主要尝试解释 Python 类和对象属性的存储,函数和方法,描述器,对象内存占用的优化支持,以及继承与属性查找等相关问题。
让我们从一个简单的例子开始:
classEmployee:
outsource = False
def __init__(self, department, name):
self.department = department
self.name = name @property
def inservice(self):
returnself.department is not None
def __repr__(self):
returnf"<Employee: {self.department}-{self.name}>"employee = Employee('IT','bobo')复制代码
employee对象是Employee类的一个实例,它有两个属性department和name,其值属于该实例。outsource是类属性,所有者是类,该类的所有实例对象共享此属性值,这跟其他面向对象语言一致。
更改类变量会影响到该类的所有实例对象:
>>> e1 = Employee('IT','bobo')>>> e2 = Employee('HR','cici')>>> e1.outsource, e2.outsource
(False, False)>>> Employee.outsource = True>>> e1.outsource, e2.outsource>>> (True, True)复制代码
这仅限于从类更改,当我们从实例更改类变量时:
>>> e1 = Employee('IT','bobo')>>> e2 = Employee('HR','cici')>>> e1.outsource, e2.outsource
(False, False)>>> e1.outsource = True>>> e1.outsource, e2.outsource
(True, False)复制代码
是的,当你试图从实例对象修改类变量时,Python 不会更改该类的类变量值,而是创建一个同名的实例属性,这是非常正确且安全的。在搜索属性值时,实例变量会优先于类变量,这将在继承与属性查找一节中详细解释。
值得特别注意的是,当类变量的类型是可变类型时,你是从实例对象中更改的它们的:
>>>classS:... L = [1, 2]
...>>> s1, s2 = S(), S()>>> s1.L, s2.L
([1, 2], [1, 2])>>> t1.L.append(3)>>> t1.L, s2.L
([1, 2, 3], [1, 2, 3])复制代码
好的实践方式是应当尽量的避免这样的设计。
属性的存储
本小节我们一起来看看 Python 中的类属性、方法及实例属性是如何关联存储的。
实例属性
在 Python 中,所有实例属性都存储在__dict__字典中,这就是一个常规的dict,对于实例属性的维护即是从该字典中获取和修改,它对开发者是完全开放的。
>>> e = Employee('IT','bobo')>>> e.__dict__
{'department':'IT','name':'bobo'}>>> type(e.__dict__)dict>>> e.name is e.__dict__['name']True>>> e.__dict__['department'] ='HR'>>> e.department'HR'复制代码
正因为实例属性是采用字典来存储,所以任何时候我们都可以方便的给对象添加或删除字段:
>>> e.age = 30 # 并没有定义 age 属性>>> e.age30>>> e.__dict__
{'department':'IT','name':'bobo','age': 30}>>> del e.age>>> e.__dict__
{'department':'IT','name':'d'}复制代码
我们也可以从字典中实例化一个对象,或者通过保存实例的__dict__来恢复实例。
>>> def new_employee_from(d):... instance = object.__new__(Employee)... instance.__dict__.update(d)... returninstance
...>>> e1 = new_employee_from({'department':'IT','name':'bobo'})>>> e1
<Employee: IT-bobo>>>> state = e1.__dict__.copy()>>> del e1>>> e2 = new_employee_from(state)>>> e2>>> <Employee: IT-bobo>复制代码
因为__dict__的完全开放,所以我们可以向其中添加任何 hashable 的 immutable key,比如数字:
>>> e.__dict__[1] = 1>>> e.__dict__
{'department':'IT','name':'bobo', 1: 1}复制代码
这些非字符串的字段是我们无法通过实例对象访问的,为了确保不会出现这样的情况,除非必要的情况下,一般最好不要直接对__dict__进行写操作,甚至不要直接操作__dict__。
所以有一种说法是 Python is a "consenting adults language"。
这种动态的实现使得我们的代码非常灵活,很多时候非常的便利,但这也付出了存储和性能上的开销。所以 Python 也提供了另外一种机制(__slots__)来放弃使用__dict__,以节约内存,提高性能,详见 __slots__ 一节。
类属性
同样的,类属性也在存储在类的__dict__字典中:
>>> Employee.__dict__
mappingproxy({'__module__':'__main__', 'outsource': True, '__init__': <function__main__.Employee.__init__(self, department, name)>, 'inservice': <property at 0x108419ea0>, '__repr__': <function__main__.Employee.__repr__(self)>, '__str__': <function__main__.Employee.__str__(self)>, '__dict__': <attribute'__dict__'of'Employee'objects>, '__weakref__': <attribute'__weakref__'of'Employee'objects>, '__doc__': None}>>> type(Employee.__dict__)
mappingproxy复制代码
与实例字典的『开放』不同,类属性使用的字典是一个MappingProxyType对象,它是一个不能setattr的字典。这意味着它对开发者是只读的,其目的正是为了保证类属性的键都是字符串,以简化和加快新型类属性的查找和__mro__的搜索逻辑。
>>> Employee.__dict__['outsource'] = FalseTypeError:'mappingproxy'object does not support item assignment复制代码
因为所有的方法都归属于一个类,所以它们也存储在类的字典中,从上面的例子中可以看到已有的__init__和__repr__方法。我们可以再添加几个来验证:
classEmployee:
# ... @staticmethod
def soo():
pass @classmethod
def coo(cls):
pass
def foo(self):
pass复制代码
>>> Employee.__dict__
mappingproxy({'__module__':'__main__', 'outsource': False, '__init__': <function__main__.Employee.__init__(self, department, name)>, '__repr__': <function__main__.Employee.__repr__(self)>, 'inservice': <property at 0x108419ea0>, 'soo': <staticmethod at 0x1066ce588>, 'coo': <classmethod at 0x1066ce828>, 'foo': <function__main__.Employee.foo(self)>, '__dict__': <attribute'__dict__'of'Employee'objects>, '__weakref__': <attribute'__weakref__'of'Employee'objects>, '__doc__': None})复制代码
继承与属性查找
目前为止,我们已经知道,所有的属性和方法都存储在两个__dict__字典中,现在我们来看看 Python 是如何进行属性查找的。
Python 3 中,所有类都隐式的继承自object,所以总会有一个继承关系,而且 Python 是支持多继承的:
>>>classA:... pass...>>>classB:... pass...>>>classC(B):... pass...>>>classD(A, C):... pass...>>> D.mro()
[<class'__main__.D'>, <class'__main__.A'>, <class'__main__.C'>, <class'__main__.B'>, <class'object'>]复制代码
mro()是一个特殊的方法,它返回类的线性解析顺序。
属性访问的默认行为是从对象的字典中获取、设置或删除属性,例如对于e.f的查找简单描述是:
e.f的查找顺序会从e.__dict__['f']开始,然后是type(e).__dict__['f'],接下来依次查找type(e)的基类(__mro__顺序,不包括元类)。 如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。
所以,要理解查找的顺序,你必须要先了解描述器协议。
简单总结,有两种描述器类型:数据描述器和和非数据描述器。
如果一个对象除了定义__get__()之外还定义了__set__()或__delete__(),则它会被视为数据描述器。仅定义了__get__()的描述器称为非数据描述器(它们通常被用于方法,但也可以有其他用途)
由于函数只实现__get__,所以它们是非数据描述器。
Python 的对象属性查找顺序如下:
- 类和父类字典的数据描述器
- 实例字典
- 类和父类字典中的非数据描述器
请记住,无论你的类有多少个继承级别,该类对象的实例字典总是存储了所有的实例变量,这也是super的意义之一。
下面我们尝试用伪代码来描述查找顺序:
def get_attribute(obj, name):
class_definition = obj.__class__
descriptor = None
forcls in class_definition.mro(): ifname in cls.__dict__:
descriptor = cls.__dict__[name] break
ifhasattr(descriptor,'__set__'): returndescriptor,'data descriptor'
ifname in obj.__dict__: returnobj.__dict__[name],'instance attribute'
ifdescriptor is not None: returndescriptor,'non-data descriptor'
else: raise AttributeError复制代码
>>> e = Employee('IT','bobo')>>> get_attribute(e,'outsource')
(False,'non-data descriptor')>>> e.outsource = True>>> get_attribute(e,'outsource')
(True,'instance attribute')>>> get_attribute(e,'name')
('bobo','instance attribute')>>> get_attribute(e,'inservice')
(<property at 0x10c966d10>,'data descriptor')>>> get_attribute(e,'foo')
(<function__main__.Employee.foo(self)>,'non-data descriptor')复制代码
由于这样的优先级顺序,所以实例是不能重载类的数据描述器属性的,比如property属性:
>>>classManager(Employee):... def __init__(self, *arg):... self.inservice = True... super().__init__(*arg)
...>>> m = Manager("HR","cici")
AttributeError: can't set attribute复制代码
发起描述器调用
上面讲到,在查找属性时,如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起描述器方法调用。
描述器的作用就是绑定对象属性,我们假设a是一个实现了描述器协议的对象,对e.a发起描述器调用有以下几种情况:
- 直接调用:用户级的代码直接调用e.__get__(a),不常用
- 实例绑定:绑定到一个实例,e.a会被转换为调用:type(e).__dict__['a'].__get__(e, type(e))
- 类绑定:绑定到一个类,E.a会被转换为调用:E.__dict__['a'].__get__(None, E)
在继承关系中进行绑定时,会根据以上情况和__mro__顺序来发起链式调用。
函数与方法
我们知道方法是属于特定类的函数,唯一的不同(如果可以算是不同的话)是方法的第一个参数往往是为类或实例对象保留的,在 Python 中,我们约定为cls或self, 当然你也可以取任何名字如this(只是最好不要这样做)。
上一节我们知道,函数实现了__get__()方法的对象,所以它们是非数据描述器。在 Python 访问(调用)方法支持中正是通过调用__get__()将调用的函数绑定成方法的。
在纯 Python 中,它的工作方式如下(示例来自描述器使用指南):
classFunction:
def __get__(self, obj, objtype=None):
ifobj is None: returnself returntypes.MethodType(self, obj) # 将函数绑定为方法复制代码
在 Python 2 中,有两种方法: unbound method 和 bound method,在 Python 3 中只有后者。
bound method 与它们绑定的类或实例数据相关联:
>>> Employee.coo
<bound method Employee.coo of <class'__main__.Employee'>>
>>> Employee.foo<function__main__.Employee.foo(self)>
>>> e = Employee('IT'
相关阅读 >>
更多相关阅读请进入《Python》频道 >>

Python编程 从入门到实践 第2版
python入门书籍,非常畅销,超高好评,python官方公认好书。