0x00 一切皆是对象
Python中一切皆对象,整型是一个对象,字符串是一个对象,字典是一个对象,甚至int、str、list等基本类型,自定义类型等是类型对象;类型对象实例化得到的对象是实例对象。
在面向对象理论中,存在着类和对象的概念,像int、dict、tuple以及使用class关键字自定义的类型对象实现了面向对象理论中以及使用class关键字定义的类型对象实现了面向对象理论中类的概念。在Python中,面向对象的类和对象在Python中是通过对象实现的。
int、str等类型对象是type对象,type称为元类,表示类型对象的类型。type本身的类型还是type,自己是自己的对象。实例对象,类型对象,元类,Python中任何任何一个对象都逃不过三种身份。Python中还有一个特殊的类型(对象),object,是所有类型对象的基类,不管是什么类,内置的类也好,自定义类都继承自object。object是所有类型对象的基类,或者父类。
python3中自定义的类即使不显示继承object,也会默认继承自object。python2中需要手动继承自object,继承于object的类称为新式类,未继承object的类称为旧式类。
class A: pass
// A 是旧式类,旧式类采用广度遍历的方式获取类的继承关系
// 旧式类不继承object 类型是 classobj
// 实例化对象的类型是instance
class B(object): pass
// 新式类采用C3规则,深度遍历的方式获取继承关系
// 类型是type
// 实例化对象是class B的对象
__base__ 可以查看基类
__bases__ 看所有父类列表
旧式类查看mro inspect.getmro(xxx)
新式类 xxx.__mro__
使用type和__class__查看一个对象的类型,并且可以通过isinstance判断对象是不是某个已知类型的实例化对象;
type的类型还是type,object的基类不是object是None,python在查找属性或方法时,会回溯继承链,自身没有属性的话,会按照__mro__指定的顺序去基类查找,所以继承链一定会有终点,否则会出现死循环。
0x01 变量只是个名字
Python中的变量只是个名字,从C的角度来说,python中的变量存储的是对象的内存地址,或者指针,指针指向的内存存储的才是对象。在python中说变量指向某个对象,其他静态语言中,变量相当于某块内存的别名,或缺变量等于获取内存存储的值,python中的变量代表的内存存储的不是对象是指针。
#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 = 0x7fffa94de03c
address of a = 0x7fffa94de03c
*/
a = 666
print(hex(id(a))) # 0x1b1333394f0
a = 667
print(hex(id(a))) # 0x1b133339510
python是变量之间的赋值传递,对象之间的引用传递。python中的变量是一个指针,当传递一个变量的时候,传递的是指针;但是操作一个变量的时候,会操作变量指向的内存。
- id(a)获取的不是a的地址,而是a指向的内存的地址
- b=a,将a本身或者说将a存储的指向某个具体对象的地址传递给了b
- type(a)查看的是变量a指向的内存的类型
- 解释器是通过靠猜的方式,通过你赋的值(或者说变量引用的值)来推断类型。
- 创建一个变量,必须在创建变量的时候同时赋值,否则解释器就不知道这个变量指向的数据是什么类型。
- Python是先创建相应的值,这个值在C中对应一个结构体,结构体里面有一个成员专门用来存储该值对应的类型。当创建完值之后,再让这个变量指向它,所以Python中是先有值后有变量。
- 但显然C中不是这样的,因为C中变量代表的内存所存储的就是具体的值,所以C中可以直接声明一个变量的同时不赋值。因为C要求声明变量的同时必须指定类型,所以声明变量的同时,其类型和内存大小就已经固定了。
- Python中变量代表的内存是个指针,它只是指向了某个对象,所以由于其便利贴的特性,可以贴在任意对象上面,但是不管贴在哪个对象,你都必须先有对象才可以,不然变量贴谁去?
- Python在创建变量的时候不需要指定类型,但Python是强类型语言,强类型语言,强类型语言
- 重要的事情说三遍。而且是动态强类型,因为类型的强弱和是否需要显示声明类型之间没有关系
0x02 可变对象与不可变对象
一个对象其实就是一片被分配的内存空间,内存中存储了相应的值。空间可以是连续的,也可以是不连续的。
不可变对象一旦创建,内存中存储的值就不可以再修改。如果想修改,只能创建一个新的对象,然后让变量指向新的对象,所以前后的地址会发生变化,而可变对象在创建之后,其存储的值可以动态修改。
每次都要创建新对象,销毁就对象,效率会低,锁一下python通过小整数对象池等手段进行优化,0-255
- 不可变对象 int,tuple
- 可变对象 list, dict
- python中对象本质上是C中malloc函数为结构体实例在堆区申请的一块内存。
- Python中任何对象在C中都会对应一个结构体,除了存放值以外,还会存放一些额外信息
- python中容器存放的元素不是具体的对象,是对象的指针
- C中的数组存放的所有元素类型必须一致,Python中列表中存放任意元素
- Python低层将所有对象的指针,都转成了PyObject指针
- PyListObject并没有存储数据,是通过二级指针存储了一个数组,所以python列表大小可变,底层PyListObject实例大小没有变化。
实现原因
遵循这样的规则可以使通过指针维护对象的工作变得非常简单。一旦允许对象的大小可在运行期改变,那么我们就可以考虑如下场景。在内存中有对象A,并且其后面紧跟着对象B。如果运行的某个时候,A的大小增大了,这就意味着必须将A整个移动到内存中的其他位置,否则A增大的部分会覆盖掉原本属于B的数据。只要将A移动到内存的其他位置,那么所有指向A的指针就必须立即得到更新。可想而知这样的工作是多么的繁琐,而通过一个指针去操作就变得简单多了
0x03 定长对象与变长对象
sys.getsizeof可以查看一个对象所占的内存大小,查看的是底层结构体占用的内存大小
import sys
print(sys.getsizeof(0)) # 24
print(sys.getsizeof(1)) # 28
print(sys.getsizeof(2 << 33)) # 32
print(sys.getsizeof(0.)) # 24
print(sys.getsizeof(3.14)) # 24
print(sys.getsizeof((2 << 33) + 3.14)) # 24
整型对象的大小不同,所占内存也不同,内存大小不固定的对象,称之为变长对象;内存大小固定的对象为定长对象,比如浮点数。
Python中通过C的32位整型数组来存储自身的整型对象,通过多个32位整型组合起来,支持存储更大的数值,所以整型越大,需要越多的32位整数。浮点数大小不变,python浮点数通过C中的double维护,固定类型是有范围的,所以浮点数据不断增大,会牺牲精度存储,实在过大会抛出OverFlowError。
>>> int(1000000000000000000000000000000000.) # 牺牲了精度
999999999999999945575230987042816
>>> 10 ** 1000 # 不会溢出
1000000000000000......
>>>
>>> 10. ** 1000 # 报错了
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OverflowError: (34, 'Result too large')
>>>
0x04 总结
- Python中一切皆对象,类型对象和实例对象都属于对象;
- 对象的种类,根据是否支持本地修改可以分为可变对象和不可变对象
- 根据占用的内存是否不变可以分为定长对象和变长对象;
- Python中变量的本质,Python中的变量本质上是一个指针,而变量的名字则存储在对应的名字空间