Blog

Python 中的 __init__ 和 self

self 是参数名,不是关键字。__init__ 并不创建对象。类属性由所有实例共享,直到你通过某个实例给它赋值的那一刻。

__init__self 有两点常让从其他语言转过来的人意外。看清 Python 实际在做什么之后,两点都好理解。

self 是参数,不是关键字

调用 d.speak() 时,Python 实际调用的是 Dog.speak(d)。实例作为第一个参数传进去,self 只是我们给这个参数起的名字。

# self is a parameter name, not a keyword
class Dog:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return f"{self.name} says woof"

d = Dog('Rex')
print(d.speak())
print(Dog.speak(d))          # the same call, written out

输出:

Rex says woof
Rex says woof

两行是同一个调用。d.speak() 只是简写。

可以证明 self 只是约定:

# proof that self is only a convention
class Cat:
    def __init__(whatever, name):
        whatever.name = name
    def speak(this):
        return f"{this.name} says meow"

print(Cat('Mia').speak())

输出:

Mia says meow

能跑。但千万别这么写——每个 Python 读者都默认看到 self,打破这个约定的代价远大于任何好处。不过知道它是参数,就能解释为什么每个方法签名里都得写上它,否则这看起来就像多余的噪音。

__init__ 并不创建对象

名字让人以为它是构造函数,其实不是。__init__ 运行时,对象已经存在了——__init__ 只负责往里填东西。

# __init__ does not create the object, it fills one in
class Tracked:
    def __new__(cls, *args):
        print('  __new__ ran, object does not exist yet')
        return super().__new__(cls)
    def __init__(self, value):
        print('  __init__ ran, self already exists:', type(self).__name__)
        self.value = value

t = Tracked(5)
print('value =', t.value)

输出:

  __new__ ran, object does not exist yet
  __init__ ran, self already exists: Tracked
value = 5

真正的构造函数是 __new__。你几乎不会去写它——只有不可变类型和元类才用得上,别处都用不到。但它解释了这个名字:__init__初始化,不是创建。

类属性是共享的

在类体里赋值的属性属于类本身。不管创建多少个实例,它都只有一份。

# instance attributes vs class attributes
class Counter:
    total = 0                      # shared by every instance
    def __init__(self):
        self.count = 0             # one per instance
    def tick(self):
        self.count += 1
        Counter.total += 1

a, b = Counter(), Counter()
a.tick(); a.tick(); b.tick()
print('a.count =', a.count, '| b.count =', b.count, '| Counter.total =', Counter.total)

输出:

a.count = 2 | b.count = 1 | Counter.total = 3

count 每个实例各有一份。total 是整个类共用的一个数。

容易踩坑的地方

通过实例读取类属性没问题。但通过实例赋值不会更新类属性——它会新建一个实例属性,把类属性遮住。

# assigning through the instance shadows the class attribute
a.total = 99
print('a.total =', a.total, '| b.total =', b.total, '| Counter.total =', Counter.total)
print('a has its own:', 'total' in a.__dict__, '| b does not:', 'total' in b.__dict__)

输出:

a.total = 99 | b.total = 3 | Counter.total = 3
a has its own: True | b does not: False

a 现在有了自己的 totalb 和类仍然共用原来那个。这就是为什么 tick 里写的是针对类的 Counter.total += 1,而不是 self.total += 1——后者会读出类上的值,加一,然后悄悄把结果存到实例上。

同样的规则也说明了为什么可变的类属性是个陷阱。类体里的列表由所有实例共享,而 self.items.append(x) 修改的是这个共享列表,并不会遮住它。可变属性要在 __init__ 里赋值。

要点

  • d.speak() 就是 Dog.speak(d)self 是第一个参数,名字只是约定。
  • __init__ 初始化一个已经存在的对象。创建对象的是 __new__,你很少需要写它。
  • 类属性是共享的。通过实例读取能找到它;通过实例赋值会遮住它。
  • 可变的类属性既共享又可变——把它们放进 __init__

这篇文章对你有帮助吗?

点一颗爱心来评分!

平均评分 0 / 5. 投票总数: 0

还没有人投票。来做第一个评分的人吧。