一起深入 Python 类的内部属性的存储继承与属性查找函数与方法__slots__补充

python视频教程栏目介绍认识Python类的内部。

这篇文章和大家一起聊一聊 Python 3.8 中类和对象背后的一些概念和实现原理,主要尝试解释 Python 类和对象属性的存储,函数和方法,描述器,对象内存占用的优化支持,以及继承与属性查找等相关问题。

让我们从一个简单的例子开始:

class Employee:

    outsource = False

    def __init__(self, department, name):
        self.department = department
        self.name = name    @property
    def inservice(self):
        return self.department is not None

    def __repr__(self):
        return f<Employee: {self.department}-{self.name}>employee = Employee('IT', 'bobo')复制代码

employee 对象是 Employee 类的一个实例,它有两个属性 departmentname,其值属于该实例。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 不会更改该类的类变量值,而是创建一个同名的实例属性,这是非常正确且安全的。在搜索属性值时,实例变量会优先于类变量,这将在继承与属性查找一节中详细解释。

值得特别注意的是,当类变量的类型是可变类型时,你是从实例对象中更改的它们的:

>>> class S:...     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)...     return instance
...>>> 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__ 的完全开放,所以我们可以向其中添加任何 hashableimmutable 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__ 方法。我们可以再添加几个来验证:

class Employee:
    # ...    @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 是支持多继承的:

>>> class A:...     pass...>>> class B:...     pass...>>> class C(B):...     pass...>>> class D(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 的对象属性查找顺序如下:

  1. 类和父类字典的数据描述器
  2. 实例字典
  3. 类和父类字典中的非数据描述器

请记住,无论你的类有多少个继承级别,该类对象的实例字典总是存储了所有的实例变量,这也是 super 的意义之一。

下面我们尝试用伪代码来描述查找顺序:

def get_attribute(obj, name):
    class_definition = obj.__class__

    descriptor = None
    for cls in class_definition.mro():        if name in cls.__dict__:
            descriptor = cls.__dict__[name]            break

    if hasattr(descriptor, '__set__'):        return descriptor, 'data descriptor'

    if name in obj.__dict__:        return obj.__dict__[name], 'instance attribute'

    if descriptor is not None:        return descriptor, '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 属性:

>>> class Manager(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 中,我们约定为 clsself, 当然你也可以取任何名字如 this(只是最好不要这样做)。

上一节我们知道,函数实现了 __get__() 方法的对象,所以它们是非数据描述器。在 Python 访问(调用)方法支持中正是通过调用 __get__() 将调用的函数绑定成方法的。

在纯 Python 中,它的工作方式如下(示例来自描述器使用指南):

class Function:
    def __get__(self, obj, objtype=None):
        if obj is None:            return self        return types.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', 'bobo')
>>> e.foo<bound method Employee.foo of <Employee: IT-bobo>>复制代码

我们可以从方法来访问实例与类:

>>> e.foo.__self__
<Employee: IT-bobo>>>> e.foo.__self__.__class__
__main__.Employee复制代码

借助描述符协议,我们可以在类的外部作用域手动绑定一个函数到方法,以访问类或实例中的数据,我将以这个示例来解释当你的对象访问(调用)类字典中存储的函数时将其绑定成方法(执行)的过程

现有以下函数:

>>> def f1(self):...     if isinstance(self, type):...         return self.outsource...     return self.name
...>>> bound_f1 = f1.__get__(e, Employee) # or bound_f1 = f1.__get__(e)>>> bound_f1
<bound method f1 of <Employee: IT-bobo>>>>> bound_f1.__self__
<Employee: IT-bobo>>>> bound_f1()'bobo'复制代码

总结一下:当我们调用 e.foo() 时,首先从 Employee.__dict__['foo'] 中得到 foo 函数,在调用该函数的 foo 方法 foo.__get__(e) 将其转换成方法,然后执行 foo() 获得结果。这就完成了 e.foo() -> f(e) 的过程。

如果你对我的解释感到疑惑,我建议你可以阅读官方的描述器使用指南以进一步了解描述器协议,在该文的函数和方法和静态方法和类方法一节中详细了解函数绑定为方法的过程。同时在 Python 类一文的方法对象一节中也有相关的解释。

__slots__

Python 的对象属性值都是采用字典存储的,当我们处理数成千上万甚至更多的实例时,内存消耗可能是一个问题,因为字典哈希表的实现,总是为每个实例创建了大量的内存。所以 Python 提供了一种 __slots__ 的方式来禁用实例使用 __dict__,以优化此问题。

通过 __slots__ 来指定属性后,会将属性的存储从实例的 __dict__ 改为类的 __dict__ 中:

class Test:
    __slots__ = ('a', 'b')    def __init__(self, a, b):
        self.a = a
        self.b = b复制代码
>>> t = Test(1, 2)>>> t.__dict__
AttributeError: 'Test' object has no attribute '__dict__'>>> Test.__dict__
mappingproxy({'__module__': '__main__',              '__slots__': ('a', 'b'),              '__init__': <function __main__.Test.__init__(self, a, b)>,              'a': <member 'a' of 'Test' objects>,              'b': <member 'b' of 'Test' objects>,              '__doc__': None})复制代码

关于 __slots__ 我之前专门写过一篇文章分享过,感兴趣的同学请移步理解 Python 类属性 __slots__ 一文。

补充

__getattribute__ 和 __getattr__

也许你还有疑问,那函数的 __get__ 方法是怎么被调用的呢,这中间过程是什么样的?

在 Python 中 一切皆对象,所有对象都有一个默认的方法 __getattribute__(self, name)

该方法会在我们使用 . 访问 obj 的属性时会自动调用,为了防止递归调用,它总是实现为从基类 object 中获取 object.__getattribute__(self, name), 该方法大部分情况下会默认从 self__dict__ 字典中查找 name(除了特殊方法的查找)。

话外:如果该类还实现了 __getattr__则只有 __getattribute__ 显式地调用或是引发了 AttributeError 异常后才会被调用__getattr__ 由开发者自己实现,应当返回属性值或引发 AttributeError 异常。

而描述器正是由 __getattribute__() 方法调用,其大致逻辑为:

def __getattribute__(self, key):
    v = object.__getattribute__(self, key)    if hasattr(v, '__get__'):        return v.__get__(self)    return v复制代码

请注意:重写 __getattribute__() 会阻止描述器的自动调用。

函数属性

函数也是 Python function 对象,所以一样,它也具有任意属性,这有时候是有用的,比如实现一个简单的函数调用跟踪装饰器:

def calltracker(func):    @wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper@calltrackerdef f():
    return 'f called'复制代码
>>> f.calls0>>> f()'f called'>>> f.calls1复制代码

相关免费学习推荐:python视频教程

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Python中的函数(二) 在上一篇文章中提到了Python中函数的定义和使用,在这篇文章里我们来讨论下关于函数的一些更深的话题。在学习C语言函数的时候,遇到的问题主要有形参实参的区别、参数的传递和改变、变量的作用域。同样在Python中,关于对函数的理解和使用也存在这些问题。下面来逐一讲解。一.函
Python中的字符串 可能大多数人在学习C语言的时候,最先接触的数据类型就是字符串,因为大多教程都是以&quot;Hello world&quot;这个程序作为入门程序,这个程序中要打印的&quot;Hello world&quot;就是字符串。如果你做过自然语言处理方面的研究,并且用Python
Python 面向对象编程(一) 虽然Python是解释性语言,但是它是面向对象的,能够进行对象编程。下面就来了解一下如何在Python中进行对象编程。一.如何定义一个类 在进行python面向对象编程之前,先来了解几个术语:类,类对象,实例对象,属性,函数和方法。 类是对现实世界中一些事物的封装,
Python面向对象编程(二) 在前面一篇文章中谈到了类的基本定义和使用方法,这只体现了面向对象编程的三大特点之一:封装。下面就来了解一下另外两大特征:继承和多态。 在Python中,如果需要的话,可以让一个类去继承一个类,被继承的类称为父类或者超类、也可以称作基类,继承的类称为子类。并且Pytho
Python中的函数(一) 接触过C语言的朋友对函数这个词肯定非常熟悉,无论在哪门编程语言当中,函数(当然在某些语言里称作方法,意义是相同的)都扮演着至关重要的角色。今天就来了解一下Python中的函数用法。一.函数的定义 在某些编程语言当中,函数声明和函数定义是区分开的(在这些编程语言当中函数声明
在windows下如何快速搭建web.py开发框架 用Python进行web开发的话有很多框架供选择,比如最出名的Django,tornado等,除了这些框架之外,有一个轻量级的框架使用起来也是非常方便和顺手,就是web.py。它由一名黑客所创建,但是不幸的是这位创建者于2013年自杀了。据说现在由
将Sublime Text 2搭建成一个好用的IDE 说起编辑器,可能大部分人要推荐的是Vim和Emacs,本人用过Vim,功能确实强大,但是不是很习惯,之前一直有朋友推荐SUblime Text 2这款编辑器,然后这段时间就试了一下,就深深地喜欢上这款编辑器了...
Python中的模块 有过C语言编程经验的朋友都知道在C语言中如果要引用sqrt这个函数,必须用语句&quot;#include&lt;math.h&gt;&quot;引入math.h这个头文件,否则是无法正常进行调用的。那么在Python中,如果要引用一些内置的函数,该怎么处理呢?在Python中
Python的基础语法 在对Python有了基础的认识之后,下面来了解一下Python的基础语法,看看它和C语言、java之间的基础语法差异。一.变量、表达式和语句 Python中的语句也称作命令,比如print &quot;hello python&quot;这就是一条语句。 表达式,顾名思义,是
Eclipse+PyDevʽjango+Mysql搭建Python web开发环境 Python的web框架有很多,目前主流的有Django、Tornado、Web.py等,最流行的要属Django了,也是被大家最看好的框架之一。下面就来讲讲如何搭建Django的开发环境。一.准备工作 需要下载的
在windows下安装配置Ulipad 今天推荐一款轻便的文本编辑器Ulipad,用来写一些小的Python脚本非常方便。 Ulipad下载地址: https://github.com/limodou/ulipad http://files.cnblogs.com/dolphin0520/u...
Python中的函数(三) 在前面两篇文章中已经探讨了函数的一些相关用法,下面一起来了解一下函数参数类型的问题。在C语言中,调用函数时必须依照函数定义时的参数个数以及类型来传递参数,否则将会发生错误,这个是严格进行规定的。然而在Python中函数参数定义和传递的方式相比而言就灵活多了。一.函数参数的
在Notepad++中搭配Python开发环境 Python在最近几年一度成为最流行的语言之一,不仅仅是因为它简洁明了,更在于它的功能之强大。它不仅能够完成一般脚本语言所能做的事情,还能很方便快捷地进行大规模的项目开发。在学习Python之前我们来看一下Python的历史由来,&quot;Pytho
Python中的条件选择和循环语句 同C语言、Java一样,Python中也存在条件选择和循环语句,其风格和C语言、java的很类似,但是在写法和用法上还是有一些区别。今天就让我们一起来了解一下。一.条件选择语句 Python中条件选择语句的关键字为:if 、elif 、else这三个。其基本形式如
关于raw_input( )和sys.stdin.readline( )的区别 之前一直认为用raw_input( )和sys.stdin.readline( )来获取输入的效果完全相同,但是最近在写程序时有类似这样一段代码:import sysline = sys.stdin.readline()
初识Python 跟学习所有的编程语言一样,首先得了解这门语言的编程风格和最基础的语法。下面就让我们一起来了解一下Python的编程风格。1.逻辑行与物理行 在Python中有逻辑行和物理行这个概念,物理行是指在编辑器中实际看到的一行,逻辑行是指一条Python语句。在Python中提倡一个物理行只
当我们的代码是有访问网络相关的操作时,比如http请求或者访问远程数据库,经常可能会发生一些错误,有些错误可能重新去发送请求就会成功,本文分析常见可能需要重试的场景,并最后给出python代码实现。
1.经典迭代器 2.将Sentence中的__iter__改成生成器函数 改成生成器后用法不变,但更加简洁。 3.惰性实现 当列表比较大,占内存较大时,我们可以采用惰性实现,每次只读取一个元素到内存。 或者使用更简洁的生成器表达式 4.yield from itertools模块含有大量生成器函数可
本文介绍简单介绍socket的常用函数,并以python-kafka中的源码socketpair为例,来讲解python socket的运用
python实践中经常出现编码相关的异常,大多网上找资料而没有理解原理,导致一次次重复错误。本文对常用Unicode、UTF-8、GB2312编码的原理进行介绍,接着介绍了python字符类型unicode和str以及常见编解码错误UnicodeEncodeError和UnicodeDEcodeEr