🧩 第五章 理解内存结构:程序员的基本素养
📄 一句话概括

造 10000 个实例,类对象只有 1 个——方法代码只存一遍,每个实例只多自己那几个字段。

📝 本节和前面几节的关系

5.6 说数据搬到了堆上,这一节说类本身放在哪、有几份。

一、类信息放哪

类信息放在静态区(类信息区),程序一启动就加载,只加载一次。 它里面有三样:字段的定义、方法的代码、指向父类的指针。 实例里只放一个指向它的指针——这就是 5.1 说的【顺着类去找方法】。

二、实测

造 10000 个实例,set(id(type(t)) for t in ts) 只有 1 个。 类上定义的字段+方法是 2 项(kind、area),只有一份。 而一万个实例各自的数据是 10000 份。 一万比一——这就是 OOP 省内存的全部秘密。

三、省在哪

方法代码只存一遍:一万个实例共享同一段 area 的代码。 每个实例只多几个字段:w、h 各一格。 反过来说:如果每个实例都复制一份方法代码,一万个实例早就爆了。

四、这条的性质

它是【必须】,不是【优化】。没有它,类的概念就不成立。 它也解释了为什么改一个方法,所有实例立刻都变——因为只有一份代码。 同一件事从另一面看就是风险:父类改错,全家族一起错(2.4 讲过的代价)。

五、动手 Demo:每个类只加载一个类信息

造一万个实例,数一数有几个不同的类对象。 再对比类上定义了几项、实例各自带了什么。

ts = [Rect(i, i) for i in range(10000)]
print(len(ts))                                   # 10000 个实例
print(len(set(id(type(t)) for t in ts)))         # 几个类对象
print(len([k for k in Rect.__dict__
           if not k.startswith("__")]))          # 类上几项

实跑输出:

--------------------------------------------------------------------
每个类只加载一个类信息
--------------------------------------------------------------------

  造一万个实例,**类信息会加载几份?**实测:

      造出来的实例数              10000
      不同的类对象有几个          1
      类上定义的字段+方法        2 项
      一万个实例各自的数据        10000 份

  东西                             有几份            在哪
  --------------------------------------------------------------------
  类信息(字段定义、方法代码)     1 份              静态区/类信息区
  实例数据(self 上的字段)        每个实例 1 份     堆

  **一万个实例共享同一份类信息**——所以方法代码只存了一遍。
  **每个实例只多出【自己那几个字段】**,这是 OOP 省内存的关键。
  **反过来说:如果每个实例都复制一份方法代码,一万个实例就爆了。**

实测:一万个实例,类对象只有 1 个;类上 2 项,实例各自只多自己那几个字段。 方法代码只存一遍——如果每个实例都复制一份方法,一万个实例就爆了。

30 秒实验

  1. 给一个类加一个方法,看已经造好的实例是不是立刻能调(说明只有一份代码)。
  2. 改一个方法,看所有实例的行为是不是一起变。

常见误区

  1. 以为每个实例都有一份方法。方法在类上,只有一份。
  2. 以为类信息也放在堆上。它在静态区/类信息区,程序启动时加载。
自测三问(点开对答案) 1. 类信息放在哪、有几份? —— 放在静态区(类信息区),程序启动时加载一次,只有一份。 2. 造一万个实例后实测是什么样? —— 类对象只有 1 个,类上 2 项定义;实例各自 1 份数据,共 10000 份。 3. 这条性质的另一面是什么? —— 改一个方法所有实例立刻都变——好处是省内存、改一处全生效,风险是父类改错全家一起错。

相关笔记