Skip to content
This page has been auto-translated and may contain errors.View in English

类与对象 ​

你目前接触过的每一种类型(字符串、列表、字典)其实都是类。当你调用 "hello".upper() 时,你就是在调用一个字符串对象上的方法。类让你能定义自己的类型,拥有自己的数据和行为。一个 Player 类可以保存名字、分数和等级,还能知道如何展示自己。

类是实现用户自定义类型的机制。类定义了一个模板:每个实例持有的数据(它的属性,即存储在其上的值)和它支持的操作(它的方法,即绑定在其上的函数)。你不必用一堆并列的变量来跟踪各种值再到处传递,而是把它们打包成一个接口清晰的对象。

class 语句会构建一个新的类型(一种自定义的值类型,跟 str 和 int 是同一类东西),并把它绑定到一个名字上,就像其他任何赋值一样。你可以通过调用这个类来创建一个实例(该类型的一个具体对象):Player("Alice")。在这次调用背后,Python 会分配一个空对象,然后运行 __init__ 来填充它。这套模型从头到尾都是统一的:类本身也是一个对象,你可以把它存进变量、传给函数,或者在运行时(程序运行期间)检查它——正是这一点让本章后面讲到的替代构造方法和注册表模式变得可行。

蓝图与实例 ​

类就是一份蓝图。实例是根据这份蓝图制造出来的具体东西。你可以按需要制造任意多个实例,每个实例都有自己的数据,但共享类中定义的同一套方法。

类定义了结构和行为。实例是由类创建出来的对象:每个实例都携带自己的数据,但共享类的方法对象。创建实例就是像调用函数一样调用类:Dog() 会创建一个新的 Dog 实例。

调用一个类会依次做两件事:先分配一个空实例,然后在它上面运行 __init__ 来填充数据。构建好的对象会记住自己所属的类(你可以通过 type(rex) 读到这一点),而正是这层关联让 Python 在你调用 rex.bark() 时能找到对应的方法:它先在实例上查找,找不到再到类上查找。这里的实际意义是:方法只存一份(在类上,大家共享),而数据是按实例存储的,所以一千个 Dog 对象会花费你一千份数据的开销,但每个方法只有一份代码。

python
class Dog:
    def bark(self):
        print("Woof!")

rex = Dog()
luna = Dog()

rex.bark()    # "Woof!"
luna.bark()   # "Woof!"

Dog 是类。rex 和 luna 是实例:两只不同的狗,各自共享类中定义的同一套行为。

Juno蓝图与实例 类是蓝图,实例是照着它造出来的一个具体东西。像调用函数一样调用类,Dog(),你就会得到一个全新的实例。每个实例都共享类的方法,但保留自己的数据,所以 rex 和 luna 可以表现出相同的行为,同时又是两只独立的狗。
Juno蓝图与实例 调用 Dog() 会创建一个实例:拥有自己的数据,共享类的方法。这种打包正是整件事的意义所在——相关的值和作用于它们的代码一起移动,而不是变成一堆你要手动到处传递的散乱变量。
Juno蓝图与实例 调用一个类会分配一个空对象,然后在它上面运行 __init__。方法只在类上存一份、大家共享,数据则按实例存储,所以对 rex 调用方法时,会先查实例、再查类。正是这种划分让一万个实例的开销依然很低。

__init__ 与 self ​

__init__ 是你创建新实例时 Python 自动调用的方法。你就是在这里为对象设置初始数据。self 是方法用来指代它正在操作的那个具体实例的方式,它始终是方法的第一个参数。

__init__ 用来初始化一个刚分配好的实例。self 是每个实例方法第一个参数的约定名称;当你调用 alice.display() 时,Python 会自动把实例传进去。在 __init__ 内部设置在 self 上的属性叫实例属性:每个实例都有自己独立的一份。

__init__ 是在已经分配好的实例上运行的,用来完成初始设置。self 不是关键字,它只是第一个参数,Python 会替你把它填好:当你调用 alice.display() 时,Python 在幕后把 alice 作为 self 传进去。设置 self.attr = value 只写入这一个实例,所以两个 Player 对象永远不会互相踩到对方的数据。有一点会让习惯了更严格语言的人感到意外:Python 允许你在任何地方添加属性,不仅限于 __init__ 中,所以像 self.scor = 0 这样的手误会直接创建一个新属性,而不会报错。把一个实例将来会用到的所有属性都放在 __init__ 中设置好,即便是设成 None 也行,这样对象的结构就在一处声明清楚,别处出现的手误也就更容易露馅。

python
class Player:
    def __init__(self, name, score=0):
        self.name = name
        self.score = score

    def add_points(self, points):
        self.score += points

    def display(self):
        print(f"{self.name}: {self.score} points")

alice = Player("Alice")
bob = Player("Bob", score=50)

alice.add_points(30)
alice.display()   # "Alice: 30 points"
bob.display()     # "Bob: 50 points"

self.name 和 self.score 是实例属性:它们属于具体的对象,而不是类本身。每个 Player 实例都有自己的 name 和 score。

Juno__init__ 与 self__init__ 在你创建实例的那一刻就会运行,所以这正是你用 self.name = value 设置初始数据的地方。self 就是 Python 正在处理的那个实例,它始终是方法的第一个参数,并且是自动交给你的。当你调用 alice.display() 时,你自己从不需要传 self。
Juno__init__ 与 self__init__ 负责搭建一个全新的实例,你在里面存到 self 上的任何东西都是实例属性,每个对象各有一份。self 是每个实例方法的第一个参数;当你写 alice.display() 时,Python 会替你把实例传进去,你只需要在定义里给它命名。
Juno__init__ 与 selfself 就是那个实例,每次调用时都会替你自动传入,写到 self.attr 上的内容都落在这一个对象上。Python 不会阻止你在 __init__ 之外添加属性,所以拼错的 self.scor 会悄悄创建出一个新属性。把所有属性都在 __init__ 里声明好,即便设成 None,手误就无处可藏。

方法 ​

在类内部定义的任何函数都是方法。实例方法的第一个参数始终是 self,Python 会自动把它传进去。方法可以通过 self 读取和修改实例的数据。

实例方法本质上是存放在类上的普通函数。当你访问 instance.method 时,Python 会返回一个绑定方法:同一个函数,只是实例已经被预先接入作为 self,所以你只需要传剩下的参数。从方法中返回 self 可以实现链式调用:obj.scale(2).rotate(90)。

方法就是类上的一个函数,被每个实例共享。访问 c.scale 得到的不是那个原始函数本身,而是一个绑定方法:同一个函数,c 已经被预先接入作为 self——这正是 self 能被自动传入而不需要你亲自动手的原因。有两种写法在实际代码中很有用。从一个改变状态的方法中返回 self,可以得到一种流式接口(像 c.scale(2).scale(0.5) 这样的链式调用);这种写法留给构建器风格的对象比较合适,链式调用在那种场景下读起来自然,不要每个方法都这么写,因为返回 self 的方法和返回一个新值的方法在调用处看起来完全一样。而且由于绑定方法是一等值,你可以把它存起来(handler = obj.save)以后再调用,这正是回调和事件处理器所依赖的机制。

python
class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

    def scale(self, factor):
        self.radius *= factor
        return self    # 返回 self 可以实现链式调用:c.scale(2).scale(0.5)

c = Circle(5)
print(c.area())    # 78.53975
c.scale(2)
print(c.area())    # 314.159
Juno方法 方法是在类内部定义的函数,它的第一个参数始终是 self,也就是它正在操作的那个实例。Python 会替你把 self 传进去,所以你调用 c.area() 时不需要额外传任何东西。方法通过 self 读取和修改那个对象自己的数据。
Juno方法 方法本质上是类上的普通函数;通过实例来访问它们会自动绑定 self,所以你只需要传剩下的参数。想要像 c.scale(2).scale(0.5) 那样链式调用时就返回 self。否则就返回调用者需要的那个值。
Juno方法obj.method 给你的是一个绑定方法,也就是那个函数加上已经接入的 obj 作为 self——这正是你从不需要传它的原因。返回 self 能带来流式的链式调用,对构建器类的对象很值,但如果每个方法都这么做就会让人困惑。而且由于绑定方法是一个值,handler = obj.save 可以把它存起来供之后回调用。

类变量与实例变量 ​

直接定义在类上(不在 __init__ 内部)的变量是类变量。所有实例共享同一个类变量。在 __init__ 内部设置在 self 上的变量是实例变量,每个对象独有。

类变量只在类上存一份,被所有实例共享。实例变量存储在各个对象上。当你读取 self.attr 时,Python 会先查实例,再查类。当你写入 self.attr = value 时,它总是会为该实例创建或更新自己的一份,只在这一个对象上遮盖住那个类变量。

读取 self.attr 时会先查实例、再查类,所以类变量的作用相当于一个共享的默认值,直到某个实例写入自己的值把它遮盖住为止。在生产环境里真正会咬人的坑是可变类变量(可以就地改动的东西,比如列表或字典)。执行 self.x = ... 会创建一份私有副本,但 self.x.append(...) 不会:它会直接改动那唯一的共享对象,所有实例都会看到这次改动。如果你想要每个实例都有自己独立的列表,就在 __init__ 里用 self.history = [] 来创建它,永远不要用类级别的 history = []。类变量只留给那些真正需要共享的东西用,并且尽量保持不可变:常量、默认值、计数器。

python
class Player:
    max_lives = 3    # 类变量,所有 Player 都一样

    def __init__(self, name):
        self.name = name   # 实例变量,每个 Player 独有
        self.lives = Player.max_lives

    def die(self):
        self.lives -= 1

alice = Player("Alice")
bob = Player("Bob")

Player.max_lives = 5    # 对所有当前及未来的实例都生效

对所有实例共享的值使用类变量:常量、计数器、默认值。对每个对象各不相同的数据使用实例变量。

Juno类变量与实例变量 直接设在类上的变量被所有实例共享;在 __init__ 里设在 self 上的变量属于那一个对象。写 self.attr = value 总会创建或更新实例自己的那一份。所以当一个值对所有对象都一样时用类变量,当它因对象而异时用实例变量。
Juno类变量与实例变量 读取 self.attr 会先查实例、再查类,所以类变量相当于一个共享的默认值。写入 self.attr = value 总会落在实例上,并对该对象遮盖住类变量。常量和默认值用类变量,各对象自己的状态放在 self 上。
Juno类变量与实例变量 悄无声息的坑是可变的类变量:self.x = ... 会创建一份私有副本,但 self.x.append(...) 会改动那唯一的共享对象,所以每个实例都会看到变化。想要每个实例各有一份列表,就在 __init__ 里用 self.history = [] 创建,永远不要在类级别写 history = []。

__str__ 与 __repr__ ​

__str__ 控制 print() 和 f-string 展示你对象时的内容。__repr__ 控制在控制台和调试时显示的开发者视图。始终定义 __repr__。 当你想要一个和调试视图不同、面向用户的清爽展示时再定义 __str__。

__str__ 会被 str()、print(),以及输出章节讲到的 f-string 调用,用于面向用户的文本。__repr__ 会被 repr() 调用,也是对象在控制台中直接打印时展示的内容,属于开发者视图。如果只定义了 __repr__,Python 两者都会用它。约定是:__repr__ 返回的字符串看起来应该像是重建这个对象的代码;__str__ 返回的是一段可读的摘要。

这是两个魔术方法(dunder,double-underscore 的简称:名字前后带 __ 的方法,Python 会在恰当的时机自动帮你调用)。print(obj) 和 str(obj) 调用的是 __str__,如果没有 __str__ 则退回使用 __repr__。repr(obj),以及对象在控制台中裸露打印时,调用的是 __repr__。所以 __repr__ 是那个永远有活可干的方法,这也是这条规则的由来:始终定义 __repr__,只有当面向用户的文本需要和调试文本不同时才加上 __str__。让 __repr__ 看起来像是构建出这个对象的构造调用(Player(name='Alice', score=87)),并在字段上使用 !r,这样字符串会保留引号,多出的一个空格或换行符也会显示出来而不是被隐藏掉。一个好的 __repr__ 正是让凌晨两点看日志行或堆栈跟踪时依然能看懂的关键。

python
class Player:
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def __str__(self):
        return f"{self.name} ({self.score} pts)"

    def __repr__(self):
        return f"Player(name={self.name!r}, score={self.score})"

alice = Player("Alice", 87)
print(alice)        # "Alice (87 pts)"   (使用 __str__)
repr(alice)         # "Player(name='Alice', score=87)"  (使用 __repr__)

始终定义 __repr__。当你想要一个和调试视图不同、面向用户的清爽表示时再定义 __str__。如果只定义了 __repr__,Python 两者都会使用它。

Juno__str__ 与 __repr____str__ 是 print() 和 f-string 展示的内容,友好的那个版本。__repr__ 是你在控制台里看到的开发者视图。始终写 __repr__;就算你忘了写 __str__,它也照样能干活。只有当面向用户的文本需要读起来不一样时才加上 __str__。
Juno__str__ 与 __repr____str__ 是给 print() 和 f-string 用的可读版本;__repr__ 是调试版本,没有 __str__ 时 Python 会退回用它。始终定义 __repr__,把它写得像重建对象的调用一样,这样以后对象出现在日志里时你会感谢自己。
Juno__str__ 与 __repr____repr__ 永远有活可干,__str__ 只有在用户视图需要不同时才需要,所以每次都先定义 __repr__。让它的样子像构造调用,字段上加 !r,这样多出的一个空格或换行才会显示出来而不是被藏起来。正是这个习惯,让凌晨两点的堆栈跟踪依然可读。

私有约定 ​

Python 没有真正意义上的私有变量,但名字开头的单下划线(_balance)是一种约定,表示"这是内部实现,不要从类外部直接使用它"。语言本身不会强制这一点;它只是向其他开发者传递的一个信号。

单下划线(_attr)是表示内部使用的一种约定。Python 不会强制这一点,但所有的检查工具、IDE 和开发者都会遵守它。双下划线(__attr)会触发名称改写(name mangling):Python 会把它重写成 _ClassName__attr,从而避免在子类中意外冲突。这不是真正的私有性,而是一种避免冲突的机制。

单下划线纯粹是约定:没有什么强制它,但代码检查工具(用来扫描你的代码找出风格问题和潜在 bug 的工具)、编辑器和审查者都会把 _balance 读作"内部实现,不要从外部动它"。双下划线会触发名称改写:Python 把 __attr 重写成 _ClassName__attr,这样子类定义自己的 __attr 就不会跟父类的意外冲突。这是一种防冲突机制,不是私有性:如果有人执意要用改写后的名字,从外部依然能访问到它。只有在打算被广泛继承的基类中,需要一个子类不能碰的属性时才用它;在普通代码里,单下划线才是正确的默认选择,双下划线大多只会带来困惑。如果你想彻底禁止随意设置属性(比如前面提到的手误 self.scor),就用 __slots__ 定义一份固定的属性名列表:它告诉 Python 实例只能拥有这些属性,其他任何赋值都会报错而不是悄悄成功,附带的好处是还能省下每个实例的一些内存。

python
class BankAccount:
    def __init__(self, balance):
        self._balance = balance    # _ 表示"别碰"

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

    def balance(self):
        return self._balance

双下划线(__name)会触发名称改写;Python 会把这个属性重命名为 _ClassName__name 以避免子类中的冲突。这种情况很少需要用到。大多数代码中用的是单下划线的约定。

Juno私有约定 Python 没有真正的私有变量,但名字前的单下划线(_balance)是大家默认认可的信号,意思是"内部实现,从外部别碰"。没有什么能阻止你去碰它,这只是给其他开发者(包括未来的你)传递的一个信息。双下划线是更少见的工具,用来避免子类中的命名冲突;单下划线才是你日常会用到的。
Juno私有约定 单下划线表示"内部实现",即便 Python 不强制这一点,所有工具和审查者都会遵守它。双下划线会触发名称改写为 _ClassName__attr,这是为了防止子类命名冲突,而不是为了限制访问。默认用单下划线,双下划线很少用得上。
Juno私有约定 单下划线是约定,双下划线会改写成 _ClassName__attr 以防止子类冲突,两者都不是真正的私有性。__attr 留给会被广泛继承的基类用;用在别的地方只会添乱。想彻底禁止随意添加属性、揪出手误的 self.scor,就把 __slots__ 设成固定的属性名列表。

继承 ​

一个类可以从另一个类继承,自动获得它的所有属性和方法。然后你可以在子类中重写某些方法来改变它们的行为。这让你能复用一份通用的基础,同时在需要的地方做特化。

继承建模的是"是一个"(is-a)的关系:一只 Dog 是一个 Animal。子类会获得父类的所有方法和属性,并可以通过定义自己的版本来重写其中任何一个。当你调用一个方法时,Python 会先在子类上查找,找不到再向上走到父类,所以没被重写的方法会自动落到父类身上。这条查找路径就是方法解析顺序(MRO),也就是 Python 搜索的这一串有序类列表。

当你调用 obj.method 时,Python 会按一个固定顺序搜索各个类,这就是 MRO(方法解析顺序:把一个类和它的祖先展开成的那份列表,可以通过 Dog.__mro__ 读到),并使用第一个匹配到的结果。单继承很直白,先子类再父类。MRO 存在的原因是多重继承和菱形情况(两个父类都继承自同一个祖父类):Python 会安排这些类的顺序,让那个祖父类只出现一次,且出现在两个父类之后,从而确定性地解析调用。实际的建议是:优先选用层次浅的结构和单继承,因为层次深或多重继承的树会让"到底运行了哪个方法"变得很难推断。当你确实要混合多个基类时,要让每个 __init__ 都通过 super()(下一节会讲到)来调用,这样每个 __init__ 都会沿着那个顺序恰好运行一次。

python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

pets = [Dog("Rex"), Cat("Luna"), Dog("Max")]
for pet in pets:
    print(pet.speak())

Dog 和 Cat 从 Animal 继承了 __init__,所以它们不需要自己的版本。它们重写了 speak(),给出各自的具体行为。

Juno继承 子类继承父类拥有的一切,然后只重写自己想改变的那些方法。任何你没重写的东西都会免费落到父类身上,这正是继承的意义所在:共享行为只写一次,在有差异的地方做特化。Dog 和 Cat 复用了 Animal 的 __init__,只重新定义了 speak()。
Juno继承 继承是"是一个"的关系:子类获得父类的方法,并重写它需要改变的部分,没被重写的调用会落到父类身上。Python 通过沿着 MRO 走来找到一个方法,先子类再父类。复用共享的基类,只重新定义有差异的部分。
Juno继承 Python 通过沿着 MRO 走并取第一个匹配项来解析方法,这正是菱形情况能被确定性处理的原因。让层次结构保持浅、依赖单继承;层次深或多重继承的树会把"到底运行了哪个方法"变成一场猜谜游戏。当你必须混合基类时,要让每个 __init__ 都通过 super() 串联起来。

super() ​

super() 用来调用父类的方法。当你想要扩展父类的行为而不是完全替换掉它时就用它:调用父类的 __init__ 来运行它的初始化逻辑,然后再加上你子类需要的其他内容。

super() 返回一个代理对象,它会把方法调用委托给 MRO 中的下一个类。只要父类有自己的 __init__,子类的 __init__ 就应该始终调用 super().__init__()。省略它会导致父类的初始化代码不会运行,这可能让对象处于一种残缺的状态。

super() 的含义不是"我的直接父类",而是"沿着 MRO(上一节讲到的查找顺序)往下的下一个类"。在一个普通的子类里,这个下一个类恰好就是父类,所以这个区别看起来有点学究气,但正是它让多重继承能正常工作:如果树上的每个类都调用 super().__init__(...),每个 __init__ 就会按 MRO 的顺序恰好运行一次,没有哪个类需要硬编码父类的名字。这就是你要写 super().__init__() 而不是 Animal.__init__(self) 的原因:直接写出父类的名字,一旦有其他类被混入进来,这条调用链就会断掉,甚至可能让一个共享的基类被运行两次。不带参数的裸 super() 是现代写法;较旧的 super(Dog, self) 是把同一件事写得更明白,你在针对旧版本的代码里仍会看到它。

python
class Animal:
    def __init__(self, name, sound):
        self.name = name
        self.sound = sound

class Dog(Animal):
    def __init__(self, name):
        super().__init__(name, "Woof")   # 调用 Animal.__init__
        self.tricks = []                  # 再加点额外的东西

    def learn(self, trick):
        self.tricks.append(trick)

rex = Dog("Rex")
rex.learn("sit")
print(rex.tricks)   # ["sit"]

只要你的子类有自己的 __init__ 且父类也有,就一定要调用 super().__init__()。

Junosuper()super() 会接到父类,所以 super().__init__() 会先运行父类的初始化逻辑,然后你再加上自己的部分。当你的子类自己写了 __init__,而父类也有一个时就用它。省略它的话父类的初始化就永远不会运行,导致对象处于半成品状态。
Junosuper()super() 会委托给沿着继承链的下一个类,所以只要父类有 __init__,就应该在子类的 __init__ 里调用 super().__init__()。忘了它,父类的初始化就会被跳过,可能让对象处于一种残缺的状态。这一行正是把子类接到它所扩展的父类上的关键。
Junosuper()super() 的意思是"MRO 中的下一个类",不是"我的父类",这也是为什么硬编码 Animal.__init__(self) 会在有类被混入进来后弄断这条调用链。如果每个 __init__ 都调用 super(),每个都会按顺序恰好运行一次。裸 super() 是现代写法;super(Dog, self) 是把同一件事写得更明白的老写法。

类方法与静态方法 ​

@classmethod 创建的方法接收到的是类本身,而不是一个实例。它常用在替代构造方法上:从字符串、文件或其他格式创建一个实例。@staticmethod 是出于组织上的原因放在类内部的一个普通函数;它既不接收实例,也不接收类。

@classmethod 的第一个参数接收到的是 cls(也就是类本身),而不是一个实例。它最主要的用途是充当替代构造方法,从不同的输入格式创建实例。@staticmethod 是一个挂在类命名空间下的普通函数;它既不能访问类,也不能访问实例。构造方法用 @classmethod,逻辑上跟类相关的工具函数用 @staticmethod。

@classmethod 的第一个参数接收到的是类本身(cls),而不是一个实例,值得留意的细节是:cls 指的是这个方法被调用时所属的那个类,而不是它被定义时所在的那个类。所以如果 Player.from_string 用 cls(...) 来构建对象,而子类 ProPlayer 调用了 ProPlayer.from_string(...),构建出来的就是一个 ProPlayer,而不是 Player。这正是替代构造方法用 @classmethod 而不是硬编码类名的原因:这样它们在继承体系下依然能正常工作。@staticmethod 既不接收实例也不接收类;它只是出于命名空间归属的原因塞在类里的一个普通函数,适合逻辑上属于这个类、但不需要用到它任何数据的情况。当你要从其他格式构造实例时用类方法,需要一个相关的辅助函数时用静态方法,要碰某个具体对象的数据时用普通方法。

python
class Player:
    def __init__(self, name, score):
        self.name = name
        self.score = score

    @classmethod
    def from_string(cls, data):
        name, score = data.split(",")
        return cls(name, int(score))

alice = Player.from_string("Alice,87")
python
class Player:
    @staticmethod
    def is_valid_name(name):
        return name.isalpha() and len(name) >= 2

Player.is_valid_name("Alice")   # True
Player.is_valid_name("A1")      # False

替代构造方法用 @classmethod。逻辑上属于这个类、但不需要实例或类数据的工具函数用 @staticmethod。

Juno类方法与静态方法@classmethod 把类交给你,而不是一个实例,这让它成为替代构造方法的首选:从字符串、文件,或者你手上现有的任何格式构建一个 Player。@staticmethod 是塞在类里图个整洁的普通函数;它既拿不到类也拿不到实例。普通方法会碰某个对象的数据,这两者都不会。
Juno类方法与静态方法@classmethod 接收 cls,也就是类本身,所以用它来写返回 cls(...) 的替代构造方法。@staticmethod 什么隐式参数都不接收,是挂在类命名空间下的一个帮手。碰对象数据用普通方法,构建实例用类方法,相关的工具函数用静态方法。
Juno类方法与静态方法 类方法构造器的关键在于 cls 就是那个调用它的对象,所以 ProPlayer.from_string(...) 构建出来的是 ProPlayer,不是 Player。把类名硬编码进去就丢掉了这个好处。构造用类方法,不需要数据的相关帮手用静态方法,碰实例的一切都用普通方法。

@property ​

@property 让你能像访问属性一样访问一个方法,不需要加括号。用它来处理那些由其他属性计算得出、读起来更适合当作普通属性访问的值。

@property 把一个方法变成一个只读属性。当你访问这个属性时,方法就会运行。这对于从已存数据推算出来的计算值很有用,也可以用来在不改变公开接口的前提下给属性访问加上校验逻辑。配对使用 @name.setter 可以让这个属性变得可写。

@property 把一个方法变成你可以像属性那样读取的东西:c.area,不加括号,每次访问都会运行这个方法。加上 @area.setter,它也就能被写入了,你的代码会在写入的过程中运行——这正是做校验的地方(在把负半径存起来之前先拒绝它)。它真正的价值在于对调用者来说什么都没变:你可以先用一个普通的 self.radius 属性开始,之后再把它升级成带校验的属性,而不需要改动任何一行读写它的代码。这也是为什么 Python 没有那套 getter/setter 的繁琐流程——你直接暴露普通属性,只有当某个值需要计算或需要校验时才动用属性。生产环境里有两点要留意:访问看起来是免费的,但它其实在运行代码,所以要保持它的开销很小(缓存一个昂贵的结果,而不是每次读取都重新计算),而且一个悄悄干重活或抛出异常的属性,会让以为它只是个简单字段的人吃惊。

python
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

c = Circle(5)
print(c.area)      # 78.53975(看起来像属性,运行起来像方法)
print(c.diameter)  # 10

属性对计算值很有用:那些从其他属性推算出来、读起来自然不需要加 () 的东西。

Juno@property@property 让你能像读取属性一样读取一个方法,不用加括号:c.area 而不是 c.area()。它适合那些从其他属性算出来、读起来自然像普通数据的值。在背后它每次被访问时依然会运行你的方法。
Juno@property@property 把一个方法读起来像属性,配对的 @name.setter 让它可写,这样你就能在写入时做校验。它的好处是:你可以后续把一个普通属性变成属性对象,而不需要改动任何调用方。只在值是计算出来的或需要保护的时候用它,别当成每个字段都要加的习惯。
Juno@property 喜欢 @property 的原因在于:一个普通属性以后可以变成计算或校验过的属性,而不需要改动调用方——所以你可以先跳过 getter 和 setter,等真正需要时再用。访问看起来是免费的,但它在运行代码,所以要保持它的开销很小,把昂贵的结果缓存起来,别把重活或异常藏在一个看起来像字段的东西背后。

实战演练 ​

一个带有实例属性、方法、@property 和 __str__ 的 Player 类:

python
class Player:
    max_lives = 3

    def __init__(self, name: str):
        self.name = name
        self.score = 0
        self.lives = Player.max_lives

    def earn_points(self, amount: int) -> None:
        self.score += amount

    def take_hit(self) -> bool:
        self.lives -= 1
        return self.lives > 0

    @property
    def is_alive(self) -> bool:
        return self.lives > 0

    def __str__(self) -> str:
        return f"{self.name} | Score: {self.score} | Lives: {self.lives}"

alice = Player("Alice")
alice.earn_points(50)
alice.take_hit()
print(alice)            # "Alice | Score: 50 | Lives: 2"
print(alice.is_alive)   # True

一个 User 类,使用带 @property getter 的私有属性、一个 deactivate 方法,以及一个 to_dict 序列化方法:

python
class User:
    def __init__(self, user_id: int, username: str, email: str):
        self.id = user_id
        self.username = username
        self.email = email
        self._active = True

    @property
    def active(self) -> bool:
        return self._active

    def deactivate(self) -> None:
        self._active = False

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "username": self.username,
            "email": self.email,
            "active": self._active,
        }

    def __repr__(self) -> str:
        return f"User(id={self.id}, username={self.username!r})"

alice = User(1, "alice", "[email protected]")
print(alice.to_dict())
alice.deactivate()
print(alice.active)   # False

一个 DataSplit 类,把训练集/验证集的切分逻辑封装在属性背后,并配有 __repr__ 提供清爽的调试输出:

python
class DataSplit:
    def __init__(self, data: list, train_ratio: float = 0.8):
        split = int(len(data) * train_ratio)
        self._train = data[:split]
        self._val = data[split:]

    @property
    def train(self) -> list:
        return self._train

    @property
    def val(self) -> list:
        return self._val

    @property
    def sizes(self) -> tuple[int, int]:
        return len(self._train), len(self._val)

    def __repr__(self) -> str:
        return f"DataSplit(train={len(self._train)}, val={len(self._val)})"

data = list(range(100))
split = DataSplit(data, train_ratio=0.8)
print(split)         # DataSplit(train=80, val=20)
print(split.sizes)   # (80, 20)

_train 和 _val 前面的下划线表示调用者应该通过属性来访问它们,而不是直接改动那些原始列表。Python 不会强制这一点,但它划定了一份清晰的约定。