Python类属性与实例属性自增行为差异?

我在编写时遇到下面情况

>>> class A:  
...     a = 0
...     def __init__(self):
...         self.a += 1
...         print(self.a)
... 
>>> a1 = A()  
1
>>> a1.a                    
1
>>> a2 = A() 
1
>>> a2.a
1
>>> id(a1.a)
140725769696040
>>> id(a2.a)
140725769696040

明明a1.a和a2.a指向的是同一个对象,且都执行了+1操作,为什么结果没变

下面代码完成了我预期的功能,但上面是什么情况

>>> class A:  
...     a = 0
...     def __init__(self):
...         A.a += 1    
...         print(self.a)
...                       
>>> a1 = A()
1
>>> a1.a
1
>>> a2 = A()
2
>>> a2.a
2
>>> id(a1.a)
140725769696072
>>> id(a2.a)
140725769696072

环境版本

python: 3.11.9
系统: windows10
阅读 1.3k
avatarAI BotBETA
### 回答

在第一个代码示例中,类属性 `a` 和实例属性 `self.a` 之间的行为差异导致了意外的结果。

1. **类属性 `a` 初始化为 0**:
   - `a` 是类 `A` 的一个类属性,所有实例共享这个属性。

2. **在 `__init__` 方法中**:
   - `self.a += 1` 实际上是在尝试修改实例属性 `a`,但是因为它之前没有在实例中定义,所以 Python 会首先查找类属性 `a`(值为 0),然后将其值赋给实例(即 `self.a = 0`),接着执行 `+= 1` 操作。
   - 这一步会在实例中创建一个新的属性 `a`,其值为 1,但不会影响类属性 `A.a` 的值,它仍然是 0。
   - 因此,每次创建新实例时,都会重复上述过程,类属性 `A.a` 始终为 0,而每个实例都会有自己的 `a` 属性,其值为 1。

3. **输出和 ID**:
   - 由于每个实例都有自己的 `a` 属性(值为 1),所以 `a1.a` 和 `a2.a` 看起来指向同一个值(1),但实际上它们是不同的属性,只是值相同。
   - `id(a1.a)` 和 `id(a2.a)` 相同(在你的环境中)是一个巧合,这取决于 Python 的内存管理,但不意味着它们指向同一个内存地址在整个程序生命周期中都有效或期望。

在第二个代码示例中:

1. **直接修改类属性**:
   - `A.a += 1` 直接修改类属性 `a` 的值。
   - 因此,每次创建新实例时,类属性 `A.a` 的值都会递增。

2. **实例属性 `self.a`**:
   - 由于在 `__init__` 方法中没有对 `self.a` 进行赋值操作(只是打印),所以它会默认引用类属性 `A.a` 的当前值(即递增后的值)。
   - 但是,这里有一个重要的点:虽然 `self.a` 访问的是类属性 `A.a` 的当前值,但 `self.a` 并不等同于 `A.a`。它只是读取类属性的值,并不创建一个新的实例属性。

3. **输出和 ID**:
   - `a1.a` 和 `a2.a` 实际上访问的是同一个类属性 `A.a` 的值,因此它们的值会递增,并且 `id` 也相同(因为它们确实是同一个对象)。

### 总结
- 在第一个示例中,每次创建实例时,都会创建一个新的实例属性 `a`,其值独立于类属性 `A.a`。
- 在第二个示例中,直接修改类属性 `A.a`,所有实例访问 `self.a` 时都会看到更新后的值。
2 个回答

你遇到的问题是因为你在类 A 中定义了一个类变量 a,而在 init 方法中,你使用了 self.a,这会创建一个实例变量 a,覆盖了类变量 a。
当你创建实例 a1 和 a2 时,每个实例都有自己的 a 实例变量,而不是共享类变量 a。因此,每次实例化时,self.a 都是从 0 开始,并且 self.a += 1 只影响当前实例的 a。

你新写的代码里的A.a 是一个类变量,所有实例共享它,因此每次实例化时,A.a 都会增加。当你打印 self.a 时,self.a 实际上是访问类变量 A.a,因为实例 a1 和 a2 没有自己的 a 实例变量。因此,print(self.a) 打印的是类变量 A.a 的值。

补充

使用 cls 参数来实现这一点,这样在继承时可以修改子类的属性,而不是父类的。可以通过在方法中使用 @classmethod 装饰器来实现。

class A:
    a = 0

    @classmethod
    def increment_a(cls):
        cls.a += 1
        print(cls.a)

    def __init__(self):
        self.increment_a()

class B(A):
    pass

a1 = A()  # 输出 1
a2 = A()  # 输出 2
b1 = B()  # 输出 1
b2 = B()  # 输出 2

print(A.a)  # 输出 2
print(B.a)  # 输出 2

补充一下,关于 id

在CPython 里(就是我们通常用的 python) id 输出的不是变量的地址,而是对象的地址。

对于小整数,相同值的对象是共享的。就是整个 python 中,只有一个 1 对象。你也可以打印 id(1)

所以,虽然 a1.a a2.a 是两个不同的类里的两个不同的实例变量,但是它们都引用了同一个 1 对象,所以 id 是一样的。

class A:
    a = 0
    def __init__(self):
        self.a += 1 # 这里会创建一个新变量 self.a ,覆盖了 A.a 
        print(self.a)

a1 = A()
a2 = A()
print(A.a, a1.a, a2.a, id(A.a), id(a1.a), id(a2.a), id(0), id(1))
# 0 1 1 140711496422656 140711496422688 140711496422688 140711496422656 140711496422688
A.a = 1
a1.a = 2
a2.a = 3
print(A.a, a1.a, a2.a, id(A.a), id(a1.a), id(a2.a), id(1), id(2), id(3))
# 1 2 3 140711496422688 140711496422720 140711496422752 140711496422688 140711496422720 140711496422752
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏