万物皆对象
初探对象
Python中一切皆对象,在Python的世界里面,所有东西都是对象。整型、字符串、字典、int、str、list、class自定义类都是对象。
像int、str、list等基本类型,以及我们自定义的类,由于它们可以表示类型,因此我们称之为类型对象;类型对象实例化得到的对象,我们称之为实例对象。不管是哪种对象,它们都属于对象。
所以在Python当中,不管是“类”还是“对象”都是由对象实现的。
对象是由类实例化得到的,所以对象一定有类型,对象被谁实例化决定了它的类型。那么int,tuple,str这些内置的类是什么类型的呢?
print(type(int))
print(type(dict))
print(type(str))
print(type(type))
<class ‘type’>
<class ‘type’>
<class ‘type’>
<class ‘type’>
我们可以发现不管是int,dict还是type,它们的类型都是type。而type我们称为元类,表示类型对的类型。
Python中还有一个特殊的对象object,他是所有类型对象的基类。在Python3中,自定义的类即使不显式的继承object,也会默认继承自object。
谈到继承,这里稍微提一下查看继承的三个方法
class A:
pass
class B:
pass
class C(A):
pass
class D(B, C):
pass
# 首先D继承自B和C, C又继承A, 我们现在要来查看D继承的父类
# 方法一: 使用__base__
print(D.__base__) # <class '__main__.B'>
# 方法二: 使用__bases__
print(D.__bases__) # (<class '__main__.B'>, <class '__main__.C'>)
# 方法三: 使用__mro__
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
好了,讲到这里相信有小伙伴心里会提出些许疑问,type是所有类的元类,而object是所有的基类,这就说明type是要继承自object的,而object的类型是type,那么type和object到底先有哪个。
这个问题是这样的,type和object其实是相互依赖的,在之后的文章中我会提到。目前只需要记住两点就是:
- 任何对象溯源之后类型都是type
- object是所有类的基类
这里有个点需要注意:
type的类型还是type,但是object的基类是None。 原因是当Python查找属性或者方法时,会回溯继承链,所以类的继承必须有个终点,不然就会出现死循环。
可变对象与不可变对象
Python中的变量
Python中变量名实际存储的是对象的地址,也就是指针。当我们修改Python中变量的值得时候,修改的是变量代表的指针,并没有修改指向内存里面存储的内容
下面和C语言比较一下就一目了然了
C:
#include <stdio.h>
void main()
{
int a = 123;
printf("address of a = %p\n", &a);
a = 456;
printf("address of a = %p\n", &a);
}
address of a = 0117FAC8
address of a = 0117FAC8
Python:
a = 123
print("address of a =", id(a))
a = 456
print("address of a =", id(a))
address of a = 2189885986992
address of a = 2189888882928
这样就能很明显看出两者的区别:
C语言是创建一个变量的时候必须规定好类型,比如int a = 123,那么变量a就是int类型,以后在所处的作用域中就不可以变了。如果这时候,再设置a = 456,那么等于是把内存中存储的123换成456,a的地址和类型是不会变化的。
Pythona = 123等于是先开辟一块内存,存储的值为123,然后让变量a指向这片内存,或者说让变量a存储这块内存的指针。然后a = 456的时候,再开辟一块内存,然后让a指向存储456的内存,由于是两块不同的内存,所以它们的地址是不一样的。
所以Python中的变量只是一个和对象关联的名字罢了,它代表的是对象的指针。换句话说Python中的变量就是个便利贴,可以贴在任何对象上,一旦贴上去了,就代表这个对象被引用了
Python变量的传递
a = 123
print("address of a =", id(a))
b = a
print("address of b =", id(b))
address of a = 1589969115312
address of b = 1589969115312
a = 123的时候,先开辟一份内存,再让a存储对应内存的指针;然后b = a的时候,会把a的地址拷贝一份给b,所以b存储了和a相同的地址,它们都指向了同一个对象。
所以说Python是值传递、或者引用传递都是不准确的,准确的说Python是变量之间的赋值传递,对象之间的引用传递。因为Python中的变量本质上就是一个指针,所以在b = a的时候,等于把a指向的对象的地址(a本身)拷贝一份给b,所以对于变量来说是赋值传递;然后a和b又都是指向对象的指针,因此对于对象来说是引用传递。
另外Python当传递一个变量的时候,传递的是指针;但是在操作一个变量的时候,会操作变量指向的内存。
我们说的Python变量类型是变量指向(引用)的对象的类型,这里有个问题是,我们在创建一个变量的时候,并没有显示的指定类型啊,但Python显然是有类型的,那么Python是如何判断一个变量指向的是什么类型的数据呢?
这个问题先留个坑,因为我也不是很确定。
PyObject
先看一下PyObject的源码
/* Nothing is actually declared to be a PyObject, but every pointer to
* a Python object can be cast to a PyObject*. This is inheritance built
* by hand. Similarly every pointer to a variable-size Python object can,
* in addition, be cast to PyVarObject*.
*/
typedef struct _object {
_PyObject_HEAD_EXTRA // 双向链表 垃圾回收 需要用到
Py_ssize_t ob_refcnt; // 引用计数
PyTypeObject *ob_type; // 指向类型对象的指针,决定了对象的类型
} PyObject;
PyObject的结构体里一个三个成员:
_PyObject_HEAD_EXTRA:这个宏是一个双向链表,Python会将程序中创建的所有对象都放入到这个双向链表中,用于跟踪所有活跃的堆对像。不过这个宏只有在debug模式下生效。
PS:带EXTRA 字样的宏 只有在debug模式下才存在
#ifdef Py_TRACE_REFS
/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
#define _PyObject_EXTRA_INIT 0, 0,
#else
# define _PyObject_HEAD_EXTRA
# define _PyObject_EXTRA_INIT
#endif
可变对象 不可变对象
对象其实就是一片被分配的内存空间,内存空间可以使连续的,也可以是不连续的,最典型的就是数组和链表。
不可变对象:创建后内存中的值不可以被修改。
可变对象:内存存储的值是可以动态修改的。
a = 123
print("address of a =", id(a))
a = a + 1
print("address of a =", id(a))
address of a = 2189885986992
address of a = 2189888882928
这个就是一个很好的例子,修改a的值实际上是创建了一个新的对象,a指向的内存地址已经发生了变化。操作一个变量其实就是操作这个变量指向的内存,a = a + 1就是开辟一个新的内存空间存储124,至于原来的666所占的空间怎么办,Python解释器会看它的引用计数,如果不为0代表还有变量引用(指向)它,如果为0证明没有变量引用了,所以会被回收。
而列表、元组、集合都是可变对象,它们是可以修改的。
原因是:
这些容器它们的内部存储的也不是具体的对象,而是对象的指针。比如:lst = [1, 2, 3],你以为lst存储的是三个整型对象吗?其实不是的,lst存储的是三个整型对象的指针,当我们使用lst[0]的时候,拿到的是第一个元素的指针,但是操作(比如print)的时候会自动操作(print)指针指向的内存。
换一个角度思考,Python底层是C来实现的,所以Python中的列表的实现必然要借助C中的数组。可我们知道C中的数组里面的所有元素的类型必须一致,但列表却可以存放任意的元素,因此从这个角度来讲,列表里面的元素它就就不可能是对象,因为不同的对象在底层对应的结构体是不同的,所以这个元素只能是指针。
lst = [1, 2, 3]
print(id(lst))
lst.append(4)
print(id(lst))
2041707447872
2041707447872
我们看到列表在添加元素的时候,前后地址并没有改变。列表在C中是通过PyListObject实现的,我们在介绍列表的时候会细说。这个PyListObject内部除了一些基本信息之外,还有一个成员叫ob_item,它是一个PyObject的二级指针,指向了我们刚才说的PyObject *类型的数组的首个元素的地址。

这个地方有两点需要注意:
1.PyObject *数组内部的元素是连续且有顺序的,但是指向的整型对象则是存储在堆区的,它们的位置是任意性的。但是不管这些整型对象存储在堆区的什么位置,它们和数组中的指针都是一一对应的,我们通过索引是可以正确获取到指向的对象的。
2.Python中的列表在底层是分开存储的,因为PyListObject结构体实例并没有存储相应的指针数组,而是存储了指向这个指针数组的二级指针。显然我们添加、删除、修改元素等操作,都是通过这个二级指针来间接操作这个指针数组。
因为在Python中一个对象一旦被创建,那么它在内存中的大小就不可以变了。所以这就意味着那些可以容纳可变长度数据的可变对象,要在内部维护一个指向可变大小的内存区域的指针。而我们看到PyListObject正是这么做的,指针数组的长度、内存大小是可变的,所以PyListObject内部并没有直接存储它,而是存储了指向它的二级指针。但是Python在计算内存大小的时候是会将这个指针数组也算进去的,所以Python中列表的大小是可变的,但是底层对应的PyListObject实例的大小是不变的,因为可变长度的指针数组没有存在PyListObject里面
这么做的好处是:针维护对象的工作变得非常简单。
如果允许对象的大小可在运行期改变,那么我们就可以考虑如下场景。在内存中有对象A,并且其后面紧跟着对象B。如果运行的某个时候,A的大小增大了,这就意味着必须将A整个移动到内存中的其他位置,否则A增大的部分会覆盖掉原本属于B的数据。只要将A移动到内存的其他位置,那么所有指向A的指针就必须立即得到更新。这样一来维护工作就变得非常复杂。
定长对象 变长对象
首先上一组代码
import sys
print("sys.getsize(0) = ", sys.getsizeof(0))
print("sys.getsize(1) = ", sys.getsizeof(1))
print("sys.getsize(2 << 33) = ", sys.getsizeof(2 << 33))
print("sys.getsize(0.) = ", sys.getsizeof(0.))
print("sys.getsize(3.14) = ", sys.getsizeof(3.14))
print("sys.getsize((2 << 33) + 3.14)) = ", sys.getsizeof((2 << 33) + 3.14))
sys.getsize(0) = 24
sys.getsize(1) = 28
sys.getsize(2 << 33) = 32
sys.getsize(0.) = 24
sys.getsize(3.14) = 24
sys.getsize((2 << 33) + 3.14)) = 24
从上面的代码运行结果可以明显看出,整形对象的大小不同,所占的内存大小也不同,所以我们称整形对象是变长对象。
而相反浮点数不管大小,所占的内存大小不变都是24,所以我们称浮点数是定常对象。
而且我们知道Python中的整数是不会溢出的,而C中的整型显然是有最大范围的,那么Python是如何做到的呢?答案是Python在底层是通过C的32位整型数组来存储自身的整型对象的,通过多个32位整型组合起来,以支持存储更大的数值,所以整型越大,就需要越多的32位整数。而32位整数是4字节,所以我们上面代码中的那些整型,都是4字节、4字节的增长。
浮点数的大小是不变的,因为Python的浮点数的值在C中是通过一个double来维护的。而C中的值的类型一旦确定,大小就不变了,所以Python的float也是不变的。
小结
这一节我们粗略介绍了Python中的对象体系,我们聊了“一切皆对象”,类型对象和实例对象都属于对象。
根据是否支持本地修改划分出了可变对象和不可变对象。
根据占用的内存大小是否可变划分出了定长对象和变长对象。
1017



被折叠的 条评论
为什么被折叠?



