目录

Python还债日记之对象(一)

Python中对象的概念与分类,包括对象与引用、变量之间的联系,可变对象与不可变对象的区分。

1. 关于python中的对象

Python中万物皆对象

这是一个很通俗的说法,但却十分准确,python用对象的概念来解释程序中的元素,比如整数、字符串、元组等。甚至,表示对象类型的方法type也是一个对象。

2. 对象、引用与变量

对象在创建时,Python解释器会为其分配内存空间,如果对象的值被传递给了一个变量,那么该变量就会通过引用的方式使用该对象,这时也可以称为,变量是对对象的引用。

https://raw.githubusercontent.com/shmilywh/PicturesForBlog/master/2021/03/28-17-13-17-2021-03-28-17-13-13-image.png

3. 对象的内存地址

其实在python中,变量的地位很尴尬,因为python动态语言的特性以及万物皆对象的特性,导致变量往往都是对对象的引用。这时变量与对象可以被理解成两部分,在存储时,二者也是分开的。变量代表着对象的地址,存储在栈区,而对象的实际内容存储在堆区。我们可以通过id()来获取对象或者说变量在堆区中的内存地址。也可以理解成返回当前对象在其生命周期内独一无二的标识符。

下图可以很好地说明在python中对象与变量的存储方式,以及堆区和栈区的内存分配。

  • 栈区:

    • 栈区存放真实的对象,当新对象创建时,堆区中就会被开辟出一块新的内存地址分配给这个新的对象。

    • 此外,python还为一些特殊的数据独立分配了特定的空间,分别是小对象整数池、匿名列表、字典对象缓冲区以及短字符串缓冲区。这样做的原因在后文会详细说明,总结来说就是,为不可变对象(小整数、字符串)以及匿名对象(未赋值的字典和列表)提供一个固定的地址。

  • 栈区:

    • 栈区存放的是前面说的,地位很尴尬的变量,下图可以看到,变量A、B、C、D其实表示的是堆区对象的引用,其本身存放的也不是对象的值,而是对象在堆区的地址。可以通过id方法获取变量指向对象的地址
  • 栈区和堆区之间,从对象指向变量的,就是引用

https://raw.githubusercontent.com/shmilywh/PicturesForBlog/master/2021/04/25-22-39-37-2021-04-25-22-39-32-image.png

注意:

  • 变量在栈中的地址与变量自身存储的地址是不同的,注意区分

  • 可以思考下,变量与对象之间的关系是一一对应的么?

4. 对象的类型

python中的对象都存储在堆区,但是其类型却不尽相同,根据是否可变,我们将对象分为可变对象和不可变对象。

注意:这里的可变与不可变,指的是变量指向的内存地址是否可以改变,如果指向一个对象地址的变量可以被修改,那么该对象就是可变的

4.1 不可变对象

不可变对象,就是无法修改的对象,我们无法在内存中直接修改这个变量。必须断开原来的引用,才可以使这个变量拥有新的值。

所以在python中,当我们尝试修改不可变类型的值,比如初始化a = 5,然后再改变a的值,a = 4,这时,由于指向对象5的地址是不可变的,所以对象5a的引用会断开,然后一个新的对象4的引用会分配给该变量。这个过程前后,变量a虽然没有改变变量名称,但是其指向的对象地址发生了变化,如果调用id()方法也可以证明这一点。

1
2
3
4
5
6
>>> a = 5
>>> id(a), id(5)
10914624, 10914624
>>> a = 4
>>> id(a), id(4)
10914592, 10914592

在Python中, 常见的不可变对象有:

  • int

  • float

  • bool

  • tuple

  • string

再强调一遍,以上五种类型之所以被称为不可变对象,是因为一旦变量与对象通过引用绑定之后,再想修改变量的值,只能通过解除引用、重新引用的方式。所以我们可以发现,对于不可变对象,如果变量的值不同,其对应对象一定不同,同一个变量修改内容后,其指向的对象也一定是改变了的。

4.2 特殊不可变对象

如果两个变量的值相同,那么他们在堆中的地址一定一样么?答案是不一定

再放一下关于对象存储在堆区中的图

https://raw.githubusercontent.com/shmilywh/PicturesForBlog/master/2021/04/26-10-36-43-2021-04-26-10-36-38-image.png

python中关于对象的存储,有几处特殊的空间,这里只说两处,分别是短字符串缓冲区以及小对象整数池。二者的作用是,当字符串的长度较短或者是整数的数值较小时,python会将值相同的变量分配相同的引用,换句话说,值相同的变量指向相同的对象。

小对象整数池的作用范围是(-5, 256),在这范围内的整数,只要值相同,堆中的内存地址也相同

1
2
3
4
5
6
7
8
>>> a = 5
>>> b = 5
>>> id(a), id(b)
10914624, 10914624
>>> a = 555
>>> b = 555
>>> id(a), id(b)
140122211533680, 140122211533712

短字符串指的是没有空格的字符串,不带空格的字符串,只要内容相同,其在堆中的地址就一致

1
2
3
4
5
6
7
8
>>> a = 'dddddddddddddddddddddddddddddddddd'
>>> b = 'dddddddddddddddddddddddddddddddddd'
>>> id(a), id(b)
140599787760344, 140599787760344
>>> a = 'a b'
>>> b = 'a b'
>>> id(a), id(b)
140599787781456, 140599787781400

4.3 可变对象

明白了不可变对象的定义之后,可变对象的定义也就很清楚了,对于一个变量和其绑定的对象,如果是可变的,那么修改变量可以通过直接改变变量指向的堆中的内存地址来实现,不需要破坏对象与变量之间的引用

在Python中,常见的可变对象有:

  • list

  • dict

  • set

对于这三种可变对象,不管值是否相同,不同变量对应堆中的内存地址一定不同,同一变量对应的内存地址一定不变。

1
2
3
4
5
6
>>> a = [1]
>>> b = [1]
>>> id(a)
140176052670856
>>> id(b)
140176052699720

4.4 特殊可变对象

依旧是这张图

https://raw.githubusercontent.com/shmilywh/PicturesForBlog/master/2021/04/26-10-47-45-2021-04-26-10-47-42-image.png

对于可变对象,python中也有一些特殊的情况,比如匿名的可变对象。所以python同样给匿名的列表和字典对象准备了特殊待遇,对于这两类匿名对象,其在堆中的内存地址是一样的。(注,匿名表示的就是没有被赋值的对象,也可以理解为对象没有被变量引用)

1
2
3
4
>>> id([1, 2, 3]), id([1, 2, 3])
139883205481032, 139883205481032
>>> id({'a': 1}), id({'a': 1})
139883230991992, 139883230991992

5. 总结

本文内容总结如下:

  1. Python是一门动态语言

  2. Python中万物皆对象

  3. Python中变量存储的是对象的引用

  4. Python中对象可以分为可变对象与不可变对象

    1. 对于不可变对象,小整数、短字符串以及布尔值这三类,只要值相同,那么变量指向的地址就相同,其余类型则值相同、地址也不同

    2. 对于可变对象,单个变量的值无论怎么改变,其内存地址都不变,但是多个变量的值就算相同,其内存地址也不同

6. 参考

python3中各类变量的内存堆栈分配和函数传参区别实例详解

Python内存管理中的堆和栈以及id,is,== 的区别和使用