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

类和对象

docs.scrimba.com

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

类是用户定义类型的机制。一个类定义了一个模板:每个实例持有的数据(它的 属性,存储在它上面的值)和它支持的操作(它的 方法,附加到它的函数)。不是用并行变量跟踪值并到处传递它们,而是把它们打包到一个对象中,有一个清晰的接口。

一个 class 语句构建一个新的 类型(一种自定义的值,和 strint 是一样的东西)并将其绑定到一个名字,像任何其他赋值一样。你通过调用类来创建一个 实例(该类型的一个具体对象):Player("王五")。在引擎盖下,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 是这个类。rexluna 是实例:两只不同的狗,每只都共享类中定义的相同行为。

Juno蓝图和实例 一个类是蓝图,一个实例是从它制作的一件东西。像函数一样调用类,Dog(),你就得到一个新实例。每个实例共享类的方法,但保持自己的数据,所以 rexluna 可以表现得相同,同时是单独的狗。
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("王五")
bob = Player("张三", score=50)

alice.add_points(30)
alice.display()   # "王五: 30 points"
bob.display()     # "张三: 50 points"

self.nameself.score实例属性:它们属于特定的对象,不是类本身。每个 Player 实例有它自己的 namescore

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("王五")
bob = Player("张三")

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 字符串为你的对象显示什么。__repr__ 控制在控制台中显示的开发者视图和用于调试。总是定义 __repr__ 当你想要一个干净的面向用户显示与调试视图分开时定义 __str__

__str__str()print() 和来自输出章节的 f 字符串 调用,面向用户的文本。__repr__repr() 调用并在对象在控制台中打印时显示,开发者视图。如果只定义了 __repr__,Python 对两者都使用它。习俗:__repr__ 返回一个看起来像重新创建对象的代码的字符串;__str__ 返回一个可读的摘要。

这些是两个 双下划线 方法(简称双下划线:用前导和尾随 __ 命名的方法,Python 在正确的时刻为你调用)。print(obj)str(obj) 调用 __str__,在没有 __str__ 时回退到 __repr__repr(obj) 和在控制台中显示的一个对象调用 __repr__。所以 __repr__ 是总是有工作的那个,这就是为什么规则是:总是定义 __repr__,只在用户面向的文本应该与调试文本不同时添加 __str__。使 __repr__ 看起来像构造函数调用,该调用构建对象(Player(name='王五', score=87)),在字段上使用 !r,所以字符串保持它们的引号,一个流落的空格或新行出现而不是隐藏。一个好的 __repr__ 是什么在凌晨 2 点使一个日志行或堆栈跟踪可读。

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("王五", 87)
print(alice)        # "王五 (87 pts)"   (使用 __str__)
repr(alice)         # "Player(name='王五', score=87)"  (使用 __repr__)

总是定义 __repr__。当你想要一个干净的用户面向的表示与调试视图分开时定义 __str__。如果只定义了 __repr__,Python 对两者都使用它。

Juno__str__ 和 __repr____str__print() 和 f 字符串显示的,友好版本。__repr__ 是你在控制台中看到的开发者视图。总是写 __repr__;即使你忘记 __str__ 时它也是有工作的那个。仅在用户面向的文本应该不同地读取时添加 __str__
Juno__str__ 和 __repr____str__print() 和 f 字符串的可读版本;__repr__ 是调试版本,当没有 __str__ 时 Python 回退到它。总是定义 __repr__,使它看起来像构建对象的调用,你稍后在一个日志中看到对象时省吃俭用自己。
Juno__str__ 和 __repr____repr__ 总是有工作,__str__ 只在用户视图应该不同时,所以总是定义 __repr__ 每次。塑造它像构造函数,在字段上放 !r,所以流落的空格或新行显示而不是隐藏。那一个习惯是什么在凌晨 2 点使堆栈跟踪可读。

私人惯例

Python 没有真正的私人变量,但一个单一的下划线在一个名字的开始(_balance)是一个惯例,信号 "这是内部的,不要从类外直接使用它"。它不是由语言强制的;它是对其他开发者的沟通。

一个单一的下划线(_attr)是一个信号内部使用的惯例。Python 不强制它,但所有 linters、IDEs 和开发者都尊重它。一个双下划线(__attr)触发 名称修饰:Python 将其重写为 _ClassName__attr,它在子类中阻止偶然的碰撞。它不是真正的隐私;它是一个碰撞避免机制。

一个单一的下划线仅仅是惯例:没有什么强制它,但 linters(扫描你的代码以寻找风格和可能的虫子的工具)、编辑器和审阅者都读 _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()(下一节)所以每个恰好运行一次沿着那个顺序。

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("北京"), Cat("上海"), Dog("天津")]
for pet in pets:
    print(pet.speak())

DogCatAnimal 继承 __init__,所以它们不需要它们自己的。它们用它们的特定行为覆盖 speak()

Juno继承 一个子类继承父类有的一切,然后仅覆盖它想要改变的方法。你不覆盖的任何东西掉向父类免费,这整个就是要点:一次写共享的行为,在它不同的地方特殊化。DogCat 重用 Animal__init__ 并仅重新定义 speak()
Juno继承 继承是 "is-a":子类获得父类的方法并覆盖它需要的,未覆盖的调用掉下。Python 通过走 MRO、子类首先、然后向父类来找到方法。重用共享的基础,仅重新定义不同的。
Juno继承 Python 通过走 MRO 并取第一个匹配来解析方法,这是什么使菱形案件确定的。保留层级浅和靠单个继承;深或多个继承树转变 "哪个方法运行了" 为一个猜测游戏。当你必须混合基础时,链每个 __init__ 通过 super()

super()

super() 从父类调用一个方法。当你想要扩展父类的行为而不是完全替代它时使用它:调用父类的 __init__ 以运行它的设置,然后在顶部添加你的子类需要的任何东西。

super() 返回一个代理对象,将方法调用代理给 MRO 中的下一个类。从子类 __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.learn("sit")
print(rex.tricks)   # ["sit"]

总是调用 super().__init__() 当你的子类有它自己的 __init__ 和父类也有一个时。

Junosuper()super() 到达父类,所以 super().__init__() 运行父类的设置在你添加你自己的之前。当你的子类写它自己的 `__init__` 并且父类有一个时使用它。跳过它并且父类的设置永远不运行,留下对象半建。
Junosuper()super() 代理给下一个类向上,所以从子类 `__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_stringcls(...) 构建,并一个子类 ProPlayer 调用 ProPlayer.from_string(...),它构建一个 ProPlayer,不是 Player。那就确实是为什么替代构造函数用 @classmethod 而不是硬编码类名:它们保持在继承下工作。@staticmethod 既不取实例也不取类;它是一个普通函数停泊在类内为命名空间,当逻辑属于类但需要它的数据没有时。当你从某些其他格式制作实例时到达一个 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("王五,87")
python
class Player:
    @staticmethod
    def is_valid_name(name):
        return name.isalpha() and len(name) >= 2

Player.is_valid_name("王五")   # True
Player.is_valid_name("王5")      # False

@classmethod 对替代构造函数。用 @staticmethod 对逻辑相关到类但不需要实例或类数据的实用函数。

Juno类方法和静态方法@classmethod 手给你类而不是实例,这使它成为替代构造函数的首选:从字符串、文件构建 Player,无论你有什么格式。@staticmethod 是一个普通函数塞在类内为整洁;它既不获得类也不获得实例。普通方法接触一个对象的数据,这两个不。
Juno类方法和静态方法@classmethodcls,类本身,所以用它对返回 cls(...) 的替代构造函数。@staticmethod 取什么都没有隐含,一个帮手命名空间在类下。对每对象数据的方法,对构造实例的 classmethod,对一个相关实用函数的 staticmethod。
Juno类方法和静态方法 一个 classmethod 构造函数的要点是 `cls` 是无论谁调用了它,所以 ProPlayer.from_string(...) 构建一个 ProPlayer,不是 Player。硬编码类名并且你丧失那个。Classmethod 对构建,staticmethod 对一个相关的帮手,需要没有数据,正常的方法对所有接触实例的东西。

@property

@property 让你像一个属性一样访问一个方法,没有括号需要的。对被从其他属性计算的值和感觉自然地读作简单属性访问的值使用它。

@property 转变一个方法为一个只读属性。方法在属性被访问时运行。这对被存储数据派生的计算值很有用,并为添加验证到属性访问,没有改变公共接口。一个配对的 @name.setter 使属性可写。

@property 转变一个方法为你像属性一样读的东西:c.area,没有括号,每次运行方法。添加 @area.setter 并且它变得可写也,用你的代码在方式中运行,这就是验证去的地方(在它被存储之前拒绝一个负半径)。真实的值是这改变对于调用者没有东西:你可以开始用一个普通的 self.radius 属性,并稍后提升它到一个用验证的属性,没有接触一条线,读或写它。那就是为什么 Python 没有获取器/设置器仪式,你暴露普通属性并仅当一个需要计算或防卫时到达一个属性。两个生产的注意:访问看起来自由但运行代码,所以保留它便宜(缓存一个昂贵的结果而不是在每个读上重新计算),并一个安静地做重工作或提起的属性惊讶无论谁假设了它是一个简单的字段。

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 的原因:一个普通属性可以稍后变为计算或验证的,没有改变调用者,所以你跳过获取器和设置器直到你实际需要一个。访问看起来自由但运行代码,所以保留它便宜,缓存昂贵的,不在什么读起来像一个字段后隐藏重工作或一个提起。

实际

一个 Player 类,有实例属性、方法、一个 @property__str__

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.earn_points(50)
alice.take_hit()
print(alice)            # "王五 | Score: 50 | Lives: 2"
print(alice.is_alive)   # True

一个 User 类,用一个私人属性和一个 @property 获取器,一个 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 不会强制这,但它设置一个清晰的合同。