Python一切皆是对象,但这和内存管理有什么关系?

本文深入探讨Python的内存管理机制,包括引用计数原理及其应用场景。解析循环引用问题及其实现,如在数据结构中如何避免内存泄露,并介绍弱引用的概念及weakref模块的使用。

Python的内存管理机制

对于工程师而言,内存管理机制非常重要,是绕不过去的一环。如果你是Java工程师,面试的时候一定会问JVM。C++工程师也一定会问内存泄漏,同样我们想要深入学习Python,内存管理机制也是绕不过去的一环。

不过好在Python的内存管理机制相对来说比较简单,我们也不用特别深入其中的细节,简单做个了解即可。

Python内存管理机制的核心就是引用计数,在Python当中一切都是对象,对象通过引用来使用。

我们看到的是变量名,但是变量名指向了内存当中的一块对象。这种关系在Python当中称为引用,我们通过引用来操作对象。所以根据这点,引用计数很好理解,也就是说我们会对每一个对象进行统计所有指向它的指针的数量。如果一个对象引用计数为0,那么说明它没有任何引用指向它,也就是说它已经没有在使用了,这个时候,Python就会将这块内存收回。

简单来说引用计数原理就是这些,但我们稍微深入一点,来简单看看哪些场景会引起对象引用的变化。

引用计数的变化显然只有两种,一种是增加,一种是减少,这两种场景都只有4种情况。我们先来看下增加的情况:

  1. 首先是初始化,最简单的就是我们用赋值操作给一个变量赋值。举个例子:

n = 123

这就是最简单的初始化操作,虽然123在我们来看是一个常数,但是在Python底层同样被认为是一个常数对象。n是它的一个引用。

  1. 第二种情况是引用的传递,最简单的就是我们将一个变量的值赋值给了另外一个变量。

m = n

比如我们将n赋值给m,它的本质是我们创建了一个新的引用,指向了同样一块内存。如果我们用id操作去查看m和n的id,会发现它们的id是一样的。也就是说它们并不是存储了两份相同的值,而是指向了同一份值。并不是有两个叫做王小二的人,而是王小二有两个不同的账号

  1. 第三种情况是作为元素被存储进了容器当中,比如被存储进了list当中。

a = [1, 2, 123]

虽然我们用到了一个容器,但是容器并不会拷贝一份这些对象,还是只是存储这些对象的引用

  1. 最后一种情况就是作为参数传给函数,在Python当中,所有的传参都是引用传递。这也是为什么,我们经常看到有人会这样写代码的原因:

def test(a):
  a.append(3)
  
a = []
test(a)
print(a)

我们根据上面列举的这四种引用计数增加的情况,不难推导出引用减少的情况, 其实基本上是对称的操作

  1. 和初始化对应的操作是销毁,比如我们创建的对象被del操作给销毁了,那么同样引用计数会-1

del n
  1. 和赋值给其他变量名的操作相反的操作是覆盖,比如之前我们的n=123,也就是n这个变量指向123,现在我们将n赋值成其他值,那么123这个对象的引用计数同样会减少。

n = 124
  1. 既然元素存储在容器当中会带来引用计数,那么同样元素从容器当中移除也会减少引用计数。这个也很好理解,最简单的就是list调用remove方法移除一个元素:

a.remove(123)
  1. 最后一个对应的就是作用域,也就是当变量离开了作用域,那么它对应的内存块的引用计数同样会减少。比如我们函数调用结束,那么作为参数的这些变量对应的引用计数都会减1。

如果一个对象的引用计数减到0,也就是没有引用再指向它的时候,那么当Python进行gc的时候,这块内存就会被释放,也就是这个对象会被清除,腾出空间来。

注意一下,引用计数减到0与内存回收之间并不是立即发生的,而是有一段间隔的。根据Python的机制,内存回收只会在特定条件下执行。在占用内存比较小还有很多富裕的情况下,往往是不会执行内存回收的。因为Python在执行gc(garbage collection)的时候也会stop the world,也就是暂停其他所有的任务,所以这是影响性能的一件事情,只会在有必要的时候执行。

我们费这么大劲来介绍Python中的内存机制,除了向大家科普一下这一块内容之外,更重要的一点是为了引出我们开发的时候经常遇见的一种情况——循环引用

循环引用

如果熟悉了Python的引用,来理解循环引用是非常容易的。说白了也很简单,就是你的一个变量引用我,我的一个变量引用你。

我们来写一段简单的代码,来看看循环引用:

class Test:
    def __init__(self):
        pass


if __name__ == '__main__':
    a = Test()
    b = Test()
    a.t = b
    b.t = a

如果你打个断点来看的话,会看到a和b之间的循环引用:

这里是无限展开的,因为这是一个无限循环。无限循环并不会导致程序崩溃, 也不会带来太大的问题,它的问题只有一个,就是根据前面介绍的引用计数法,a和b的引用永远不可能为0。

也就是说根据引用计数的原则,这两个变量永远不会被回收,这显然是不合理的。虽然Python当中专门建立了机制来解决引用循环的问题,但是我们并不知道它什么时候会被触发。

这个问题在Python当中非常普遍,尤其在我们实现一些数据结构的时候。举个最简单的例子就是树中的节点,就是引用循环的。因为父节点会存储所有的孩子,往往孩子节点也会存储父节点的信息。那么这就构成了引用循环。

class Node:
  def __init__(self, val, father):
    self.val = val
    self.father = father
    self.childs = []

弱引用

为了解决这个问题,Python中提供了一个叫做弱引用的概念。弱引用本质也是一种引用,但是它不会增加对象的引用计数。也就是说它不能保证它引用的对象一定不会被销毁,只要没有销毁,弱引用就可以返回预期的结果。

弱引用不用我们自己开发,这是Python当中集成的一个现成的模块weakref

这个模块当中的方法很多,用法也很多,但是我们基本上用不到,一般来说最常用的就是ref方法。通过weakref库中的ref方法,可以返回对象的一个弱引用。我们还是来看个例子:

import weakref


class Test:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name


if __name__ == '__main__':
    a = Test('a')
    b = Test('b')
    a.t = weakref.ref(b)
    b.t = weakref.ref(a)

    print(a.t())

其实还是之前的代码,只是做了一点简单的改动。一个是我们给Test加上了name这个属性,以及str方法。另一个是我们把直接赋值改成了使用weakref。

这一次我们再打断点进来看的话,就看不到无限循环的情况了:

ref返回的是一个获取引用对象的方法,而不是对象本身。所以我们想要获取这个对象的话,需要再把它当成函数调用一下。

当然这样很麻烦,我们还有更好的办法,就是使用property注解。通过property注解,我们可以把weakref封装掉,这样在使用的时候就没有感知了。

import weakref


class Test:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    @property
    def node(self):
        return None if self._node is None else self._node()

    @node.setter
    def node(self, node):
        self._node = weakref.ref(node)

总结

引用和循环引用都是基于Python本身的机制,如果对这块机制不了解,很容易采坑。因为可能会出现逻辑是对的,但是有一些意想不到的bug的情况。这种时候,往往很难通过review代码或者是测试发现,这也是我们学习的瓶颈所在。很容易发现代码已经写得很熟练了,但是一些进阶的代码还是看不懂或者是写不出来,本质上就是因为缺少了对于底层的了解和认知。

循环引用的问题在我们开发代码的时候还蛮常见的,尤其是涉及到树和图的数据结构的时候。由于循环引用的关系,很有可能出现被删除的树仍然占用着空间,内存不足的情况发生。这个时候使用weakref就很有必要了。

---来自TechFlow的分享

 

<think>我们正在讨论Python对象属性值的概念。根据用户的问题,他想要理解Python对象的定义及其属性值的理解。结合之前的对话历史,用户已经对对象属性有了一定的了解,现在希望更深入地理解对象本身属性值。 首先,我们需要明确Python对象的基本定义: - 在Python中,一切对象。每个对象都有三个基本特征:身份(identity)、类型(type)值(value)。 - 身份:对象的唯一标识,通常通过id()函数获取,可以理解为对象在内存中的地址。 - 类型:决定了对象支持的操作,通过type()函数获取。 - 值:对象存储的数据内容。 关于属性值: - 属性是对象状态的具体表现,通过点号(.)访问。 - 属性值可以是数据属性(存储数据)或方法属性(函数)。 接下来,结合引用[1]引用[2]的内容: - 引用[1]提到特殊属性__dict____class__,它们提供了对对象内部状态的访问。我们可以利用这些特殊属性来观察对象的属性存储。 - 引用[2]则介绍了属性(Property)的概念,这是一种控制属性访问的机制,允许我们在访问属性时添加额外逻辑。 因此,回答将分为以下几个部分: 1. 对象的定义(身份、类型、值) 2. 属性值的概念(数据属性方法属性) 3. 特殊属性(__dict__, __class__)的作用 4. 使用property控制属性访问 同时,为了清晰说明,我们将提供代码示例: - 创建一个简单的类并实例化对象 - 展示对象的基本特征 - 展示动态添加属性 - 展示__dict____class__的使用 - 展示property的基本用法 最后,生成相关问题。 注意:按照要求,所有数学表达式使用$...$格式,独立公式使用$$...$$格式。由于本问题不涉及数学公式,我们主要关注代码文字描述。 回答结构: 1. 对象的定义 2. 属性值的理解 3. 特殊属性 4. 属性控制(property) 5. 代码示例 6. 相关问题</think>在 Python 中,**对象****属性值**是面向对象编程的核心概念。以下从五个维度系统阐述其定义与关系: --- ### 1. 对象的本质定义 Python 中一切对象(Everything is an object)[^1]。每个对象包含: - **身份标识 (Identity)**:通过 `id(obj)` 获取的内存唯一地址 ```python a = [1,2]; b = [1,2] print(id(a) != id(b)) # True,不同对象 ``` - **类型 (Type)**:决定对象的操作集,通过 `type(obj)` 获取 ```python print(type(10)) # <class 'int'> print(type("text")) # <class 'str'> ``` - **值 (Value)**:对象存储的数据内容 - **属性 (Attributes)**:附加在对象上的状态或行为 > 对象 = 身份 + 类型 + 值 + 属性[^1] --- ### 2. 属性值的层次结构 #### (1) 类属性 (Class Attributes) - 定义在类作用域中,所有实例共享 - 修改会影响所有实例 ```python class Planet: count = 0 # 类属性 def __init__(self, name): self.name = name # 实例属性 Planet.count += 1 earth = Planet("Earth") mars = Planet("Mars") print(Planet.count) # 输出: 2 ``` #### (2) 实例属性 (Instance Attributes) - 通过 `self.attr = value` 绑定到具体对象 - 每个实例独立存储 ```python earth.mass = 5.97e24 # 动态添加实例属性 print(earth.__dict__) # {'name': 'Earth', 'mass': 5.97e24} print(mars.__dict__) # {'name': 'Mars'} ``` #### (3) 特殊属性 (Special Attributes) | 属性 | 作用 | 示例 | |-------------|-------------------------------|-----------------------| | `__dict__` | 存储对象所有属性的字典 | `obj.__dict__` | | `__class__` | 指向对象的类 | `obj.__class__` | | `__doc__` | 存储类的文档字符串 | `Class.__doc__` | --- ### 3. 属性访问机制 Python 访问属性时遵循 **MRO (Method Resolution Order)**: 1. 先在实例的 `__dict__` 中查找 2. 未找到则在类 `__dict__` 中查找 3. 沿继承链向上查找(通过 `__mro__` 属性) ```mermaid graph LR A[obj.attr] --> B{实例 __dict__?} B -- 存在 --> C[返回实例属性] B -- 不存在 --> D{类 __dict__?} D -- 存在 --> E[返回类属性] D -- 不存在 --> F{父类 __dict__?} ``` --- ### 4. 属性控制的高级特性 通过 `@property` 装饰器实现属性访问控制[^2]: ```python class Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): # 获取属性时触发 return self._celsius @celsius.setter def celsius(self, value): # 设置属性时触发 if value < -273.15: raise ValueError("绝对零度不可达") self._celsius = value temp = Temperature(25) temp.celsius = 30 # 调用setter方法 temp.celsius = -300 # 触发ValueError ``` --- ### 5. 动态属性管理 Python 允许运行时修改对象属性: ```python class Empty: pass obj = Empty() obj.new_attr = "动态添加" # 添加属性 del obj.new_attr # 删除属性 obj.__dict__["dynamic"] = 100 # 通过__dict__操作 ``` --- > **核心理解**:Python 对象是包含状态(属性值)行为(方法)的独立实体,属性通过 `__dict__` 实现动态绑定,不同对象维护独立的命名空间[^1]。属性访问机制 property 特性共同实现了封装与数据控制[^2]。
评论 51
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT·Null

没有人会去使用打赏这个功能。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值