-
-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
《深度剖析CPython解释器》1. Python中一切皆对象,这里的对象究竟是什么?解密Python中的对象模型 - 古明地盆 - 博客园 #997
Comments
《深度剖析CPython解释器》1. Python中一切皆对象,这里的对象究竟是什么?解密Python中的对象模型 - 古明地盆 - 博客园《深度剖析CPython解释器》1. Python中一切皆对象,这里的对象究竟是什么?解密Python中的对象模型Python中一切皆对象关于Python,你肯定听过这么一句话:"Python中一切皆对象"。没错,在Python的世界里,一切都是对象。整型是一个对象、字符串是一个对象、字典是一个对象,甚至int、str、list等等,再加上我们使用class自定义的类,它们也是对象。
因此Python中面向对象的理念贯彻的非常彻底,面向对象中的"类"和"对象"在Python中都是通过"对象"实现的。
我们举个栗子:
因此可以用一张图来描述面向对象在Python中的体现: 类型、对象体系a是一个整数(实例对象),其类型是int(类型对象)。
但是问题来了,按照面向对象的理论来说,对象是由类实例化得到的,这在Python中也是适用的。既然是对象,那么就必定有一个类来实例化它,换句话说对象一定要有类型。至于一个对象的类型是什么,就看这个对象是被谁实例化的,被谁实例化那么类型就是谁。而我们说Python中一切皆对象,所以像int、str、tuple这些内置的类型也是具有相应的类型的,那么它们的类型又是谁呢? 我们使用type函数查看一下就好了。
我们看到类型对象的类型,无一例外都是type。type应该是初学Python的时候就接触了,当时使用type都是为了查看一个对象的类型,然而type的作用远没有这么简单,我们后面会说,总之我们目前看到类型对象的类型是type。 所以int、str等类型对象是type的对象,而type我们也称其为元类,表示类型对象的类型。至于type本身,它的类型还是type,所以它连自己都没放过,把自己都变成自己的对象了。
Python中还有一个特殊的类型(对象),叫做object,它是所有类型对象的基类。不管是什么类,内置的类也好,我们自定义的类也罢,它们都继承自object。因此,object是所有类型对象的"基类"、或者说"父类"。
因此,综合以上关系,我们可以得到下面这张关系图: 我们自定义的类型也是如此,举个栗子:
在Python3中,自定义的类即使不显式的继承object,也会默认继承自object。 那么我们自定义再自定义一个子类,继承自Female呢?
因此上面那张关系图就可以变成下面这样: 我们说可以使用type和__class__查看一个对象的类型,并且还可以通过isinstance来判断该对象是不是某个已知类型的实例对象;那如果想查看一个类型对象都继承了哪些类该怎么做呢?我们目前都是使用issubclass来判断某个类型对象是不是另一个已知类型对象的子类,那么可不可以直接获取某个类型对象都继承了哪些类呢? 答案是可以的,方法有三种,我们分别来看一下:
最后我们来看一下type和object,估计这两个老铁之间的关系会让很多人感到困惑。 我们说type是所有类的元类,而object是所有的基类,这就说明type是要继承自object的,而object的类型是type。
这就怪了,这难道不是一个先有鸡还是先有蛋的问题吗?其实不是的,这两个对象是共存的,它们之间的定义其实是互相依赖的。至于到底是怎么肥事,我们后面在看解释器源码的时候就会很清晰了。 总之目前记住两点:
最后将上面那张关系图再完善一下的话: 因此上面这种图才算是完整,其实只看这张图我们就能解读出很多信息。比如:实例对象的类型是类型对象,类型对象的类型是元类;所有的类型对象的基类都收敛于object,所有对象的类型都收敛于type。因此Python算是将一切皆对象的理念贯彻到了极致,也正因为如此,Python才具有如此优秀的动态特性。
Python中的变量只是个名字Python中的变量只是个名字,站在C语言的角度来说的话,Python中的变量存储的只是对象的内存地址,或者说指针,这个指针指向的内存存储的才是对象。
我们用两段代码,一段C语言的代码,一段Python的代码,来看一下差别。
我们看到前后输出的地址是一样的,再来看看Python的。
然而我们看到Python中变量a的地址前后发生了变化,我们分析一下原因。 首先在C中,创建一个变量的时候必须规定好类型,比如int a = 666,那么变量a就是int类型,以后在所处的作用域中就不可以变了。如果这时候,再设置a = 777,那么等于是把内存中存储的666换成777,a的地址和类型是不会变化的。 而在Python中,a = 666等于是先开辟一块内存,存储的值为666,然后让变量a指向这片内存,或者说让变量a存储这块内存的指针。然后a = 777的时候,再开辟一块内存,然后让a指向存储777的内存,由于是两块不同的内存,所以它们的地址是不一样的。
我们再来看看变量之间的传递,在Python中是如何体现的。
我们看到打印的地址是一样的,我们再用一张图解释一下。 我们说a = 666的时候,先开辟一份内存,再让a存储对应内存的指针;然后b = a的时候,会把a的地址拷贝一份给b,所以b存储了和a相同的地址,它们都指向了同一个对象。
**另外还有最关键的一点,我们说Python中的变量是一个指针,**当传递一个变量的时候,传递的是指针;但是在操作一个变量的时候,会操作变量指向的内存。 所以id(a)获取的不是a的地址,而是a指向的内存的地址(在底层其实就是a),同理b = a,是将a本身,或者说将a存储的、指向某个具体的对象的地址传递给了b。
最后提一下变量的类型 我们说变量的类型其实不是很准确,应该是变量指向(引用)的对象的类型,因为我们说Python中变量是个指针,操作指针会操作指针指向的内存,所以我们使用type(a)查看的是变量a指向的内存的类型,当然为了方便也会直接说变量的类型,理解就行。那么问题来了,我们在创建一个变量的时候,并没有显示的指定类型啊,但Python显然是有类型的,那么Python是如何判断一个变量指向的是什么类型的数据呢? 答案是:解释器是通过靠猜的方式,通过你赋的值(或者说变量引用的值)来推断类型。所以在Python中,如果你想创建一个变量,那么必须在创建变量的时候同时赋值,否则解释器就不知道这个变量指向的数据是什么类型。所以Python是先创建相应的值,这个值在C中对应一个结构体,结构体里面有一个成员专门用来存储该值对应的类型。当创建完值之后,再让这个变量指向它,所以Python中是先有值后有变量。但显然C中不是这样的,因为C中变量代表的内存所存储的就是具体的值,所以C中可以直接声明一个变量的同时不赋值。因为C要求声明变量的同时必须指定类型,所以声明变量的同时,其类型和内存大小就已经固定了。而Python中变量代表的内存是个指针,它只是指向了某个对象,所以由于其便利贴的特性,可以贴在任意对象上面,但是不管贴在哪个对象,你都必须先有对象才可以,不然变量贴谁去? 另外,尽管Python在创建变量的时候不需要指定类型,但Python是强类型语言,强类型语言,强类型语言,重要的事情说三遍。而且是动态强类型,因为类型的强弱和是否需要显示声明类型之间没有关系。 可变对象与不可变对象我们说一个对象其实就是一片被分配的内存空间,内存中存储了相应的值,不过这些空间可以是连续的,也可以是不连续的。 不可变对象一旦创建,其内存中存储的值就不可以再修改了。如果想修改,只能创建一个新的对象,然后让变量指向新的对象,所以前后的地址会发生改变。而可变对象在创建之后,其存储的值可以动态修改。 像整型就是一个不可变对象。
我们看到在对a执行+1操作时,前后地址发生了变化,所以整型不支持本地修改,因此是一个不可变对象; 原来a = 666,而我们说操作一个变量等于操作这个变量指向的内存,所以a+=1,会将a指向的整型对象666和1进行加法运算,得到667。所以会开辟新的空间来存储这个667,然后让a指向这片新的空间,至于原来的666所占的空间怎么办,Python解释器会看它的引用计数,如果不为0代表还有变量引用(指向)它,如果为0证明没有变量引用了,所以会被回收。 关于引用计数,我们后面会详细说,目前只需要知道当一个对象被一个变量引用的时候,那么该对象的引用计数就会加1。有几个变量引用,那么它的引用计数就是几。
而列表是一个可变对象,它是可以修改的。
首先Python中列表,当然不光是列表,还有元组、集合,这些容器它们的内部存储的也不是具体的对象,而是对象的指针。比如:lst = [1, 2, 3],你以为lst存储的是三个整型对象吗?其实不是的,lst存储的是三个整型对象的指针,当我们使用lst[0]的时候,拿到的是第一个元素的指针,但是操作(比如print)的时候会自动操作(print)指针指向的内存。 不知道你是否思考过,Python底层是C来实现的,所以Python中的列表的实现必然要借助C中的数组。可我们知道C中的数组里面的所有元素的类型必须一致,但列表却可以存放任意的元素,因此从这个角度来讲,列表里面的元素它就就不可能是对象,因为不同的对象在底层对应的结构体是不同的,所以这个元素只能是指针。 可能有人又好奇了,不同对象的指针也是不同的啊,是的,但C中的指针是可以转化的。Python底层将所有对象的指针,都转成了PyObject的指针,这样不就是同一种类型的指针了吗?关于这个PyObject,它是我们后面要剖析的重中之重,这个PyObject贯穿了我们的整个系列。目前只需要知道Python中的列表存储的值,在底层是通过一个PyObject *类型的数据来维护的。
我们看到列表在添加元素的时候,前后地址并没有改变。列表在C中是通过PyListObject实现的,我们在介绍列表的时候会细说。这个PyListObject内部除了一些基本信息之外,还有一个成员叫ob_item,它是一个PyObject的二级指针,指向了我们刚才说的PyObject *类型的数组的首个元素的地址。 结构图如下: 显然图中的指针数组是用来存储具体的对象的指针的,每一个指针都指向了相应的对象(这里是整型对象)。可能有人注意到,整型对象的顺序有点怪,其实我是故意这么画的。因为PyObject *数组内部的元素是连续且有顺序的,但是指向的整型对象则是存储在堆区的,它们的位置是任意性的。但是不管这些整型对象存储在堆区的什么位置,它们和数组中的指针都是一一对应的,我们通过索引是可以正确获取到指向的对象的。 另外我们还可以看到一个现象,那就是Python中的列表在底层是分开存储的,因为PyListObject结构体实例并没有存储相应的指针数组,而是存储了指向这个指针数组的二级指针。显然我们添加、删除、修改元素等操作,都是通过这个二级指针来间接操作这个指针数组。 为什么要这么做? 因为在Python中一个对象一旦被创建,那么它在内存中的大小就不可以变了。所以这就意味着那些可以容纳可变长度数据的可变对象,要在内部维护一个指向可变大小的内存区域的指针。而我们看到PyListObject正是这么做的,指针数组的长度、内存大小是可变的,所以PyListObject内部并没有直接存储它,而是存储了指向它的二级指针。但是Python在计算内存大小的时候是会将这个指针数组也算进去的,所以Python中列表的大小是可变的,但是底层对应的PyListObject实例的大小是不变的,因为可变长度的指针数组没有存在PyListObject里面。但为什么要这么设计呢?
定长对象与变长对象Python中一个对象占用的内存有多大呢?相同类型的实例对象的大小是否相同呢?试一下就知道了,我们可以通过sys模块中getsizeof函数查看一个对象所占的内存。
我们看到整型对象的大小不同,所占的内存也不同,像这种内存大小不固定的对象,我们称之为变长对象;而浮点数所占的内存都是一样的,像这种内存大小固定的对象,我们称之为定长对象。
而且我们知道Python中的整数是不会溢出的,而C中的整型显然是有最大范围的,那么Python是如何做到的呢?答案是Python在底层是通过C的32位整型数组来存储自身的整型对象的,通过多个32位整型组合起来,以支持存储更大的数值,所以整型越大,就需要越多的32位整数。而32位整数是4字节,所以我们上面代码中的那些整型,都是4字节、4字节的增长。 当然Python中的对象在底层都是一个结构体,这个结构体中除了维护具体的值之外,还有其它的成员信息,在计算内存大小的时候,它们也是要考虑在内的,当然这些我们后面会说。 而浮点数的大小是不变的,因为Python的浮点数的值在C中是通过一个double来维护的。而C中的值的类型一旦确定,大小就不变了,所以Python的float也是不变的。 但是既然是固定的类型,肯定范围是有限的,所以当浮点数不断增大,会牺牲精度来进行存储。如果实在过大,那么会抛出OverFlowError。
还有字符串,字符串毫无疑问肯定是可变对象,因为长度不同大小不同。
我们看到多了两个字符,多了两个字节,这很好理解。但是这些说明了一个空字符串要占49个字节,我们来看一下。
显然是的,显然这49个字节是用来维护其它成员信息的,因为底层的结构体除了维护具体的值之外,还要维护其它的信息,比如:引用计数等等,这些在分析源码的时候会详细说。 小结我们这一节介绍了Python中的对象体系,我们说Python中一切皆对象,类型对象和实例对象都属于对象;还说了对象的种类,根据是否支持本地修改可以分为可变对象和不可变对象,根据占用的内存是否不变可以分为定长对象和变长对象;还说了Python中变量的本质,Python中的变量本质上是一个指针,而变量的名字则存储在对应的名字空间(或者说命名空间)中,当然名字空间我们没有说,是因为这些在后续系列会详细说 名字空间分为:全局名字空间
怎么样,是不是有点神奇呢?所以名字空间是Python作用域的灵魂,它严格限制了变量的活动范围,当然这些后面都会慢慢的说,因为饭要一口一口吃。因此这一节算是回顾基础吧,虽说是基础但是其实也涉及到了一些解释器的知识,不过这一关我们迟早是要过的,所以就提前接触一下吧。 |
Python中一切皆对象 关于Python,你肯定听过这么一句话:"Python中一切皆对象"。没错,在Python的世界里,一切都是对象。整型是一个对象、字符串是一个对象、字典是一个对象,甚至int、str、list等等,再加上我们使用class自定义的
Tags:
via Pocket https://ift.tt/3jToOZ6 original site
February 16, 2021 at 10:02PM
The text was updated successfully, but these errors were encountered: