前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python 中的 Return Self 到底是个啥?

Python 中的 Return Self 到底是个啥?

作者头像
数据STUDIO
发布2024-01-29 12:06:18
1220
发布2024-01-29 12:06:18
举报
文章被收录于专栏:数据STUDIO数据STUDIO

题目中的 return self 并不是我们常见的 self 参数,而本文的首要任务是需要了解什么是类型提示以及它们如何工作。类型提示我们可以显式地指明变量类型、函数参数和返回值。这可以使代码更具可读性和可维护性,尤其是当代码的规模和复杂性不断增加时。

我们可以使用冒号(:)指定变量和函数参数类型,然后是数据类型,而返回值注释则使用破折号(->),然后是返回类型。

举例说明,我们可以编写一个函数,输入我们购买的馅饼数量和每个馅饼的价格,然后输出一个总结我们的交易的字符串:

代码语言:javascript
复制
>>> def buy_pies(num_pies: int, price_per_pie: float) -> str:
...     total_cost = num_pies * price_per_pie
...     return f"Yum! You spent ${total_cost} dollars on {num_pies} pies!"
...

buy_pies() 中,num_pies 变量使用 int 类型,price_per_pie 使用 float 类型。因为返回值是字符串,所以用 str 类型注释返回值。

注意:可以将局部变量 total_cost 类型提示为 total_cost: float。虽然这看起来不错,但是类型检查器可以自动从 num_piesprice_per_pie 中推断出 total_cost 的类型,因此 total_cost 不需要进行类型注释。

Python 中的类型和注释通常不会影响代码的功能,但是许多静态类型检查器和 IDE 可以识别它们。例如,如果你在 VS Code 中悬停在 buy_pies() 上,那么你可以看到每个参数或返回值的类型:

在处理类时,我们还可以使用注释。这可以帮助其他开发人员了解方法的返回类型,在处理复杂的类层次结构时尤其有用。甚至可以对返回类实例的方法进行注释

类型和注释可以用来注释返回类实例的方法。这对于 class 方法特别有用,可以防止在处理继承和方法链时出现的混乱。

不幸的是,对这些方法进行注释可能会造成混淆并导致意外错误。注释此类方法的一种自然方法是使用类名,但下面的方法行不通:

代码语言:javascript
复制
# incorrect_self_type.py

from typing import Any

class Queue:
    def __init__(self):
        self.items: list[Any] = []

    def enqueue(self, item: Any) -> Queue:
        self.items.append(item)
        return self

在上面的示例中,Queue 中的 .enqueue() 将一个项目添加到队列中并返回类实例。用类名注释 .enqueue() 看起来很好,但这会导致静态类型检查和运行时错误。大多数静态类型检查器都会监测到在使用Queue前没有定义,如果运行该代码,则会抛出以下NameError异常:

代码语言:javascript
复制
Traceback (most recent call last):
  ...
NameError: name 'Queue' is not defined

从类 Queue 继承时也会出现问题。特别像 .enqueue() 这样的方法将返回 Queue,即使你在 Queue 的子类上调用它。

好消息!Python 的 Self 类型可以处理这些情况,它提供了一个非常出彩的注释,处理了注释返回外层类实例的方法的微妙之处

本文中,云朵君将和大家一起学习 Self 类型,并学习如何使用它来编写可读性和可维护性更强的代码。特别将学习如何用 Self 类型注释方法,并确保 IDE 能够识别它。我们还将研究注释返回类实例的方法的其他策略,并探讨为什么 Self 类型是第一选择。

如何在Python中使用Self类型来注释方法

Self 类型语法直观和简洁,成为注释返回类实例的首选方法。在 3.11 及以后的版本中,Self 类型可以直接从 Python 的类型模块中导入。对于小于 3.11 的 Python 版本,Self 类型可以在 typing_extensions 中使用。

例如,我们将注释一个堆栈数据结构。请特别注意 .push() 注释:

代码语言:javascript
复制
 1# stack.py
 2
 3from typing import Any, Self
 4
 5class Stack:
 6    def __init__(self) -> None:
 7        self.items: list[Any] = []
 8
 9    def push(self, item: Any) -> Self:
10        self.items.append(item)
11        return self
12
13    def pop(self) -> Any:
14        if self.__bool__():
15            return self.items.pop()
16        else:
17            raise ValueError("Stack is empty")
18
19    def __bool__(self) -> bool:
20        return len(self.items) > 0

在第3行中从类型中导入Self类型,并在第9行中用 -> Self注释.push()。这将告诉静态类型检查器 .push() 返回一个 Stack 实例,从可以将多个 push 串联起来。注意,通常没有必要注释selfcls参数。

注意: 我们实现了 .__bool__() 来检查堆栈是否为空。这个方法是 Python 数据模型的一部分,被称为 dunder 或特殊方法。在这种情况下,定义 .__bool__() 从类内部或外部调用 bool() 内置函数来检查堆栈是否为空。

.__bool__()的加入使得该类可以在 Pythonic 条件句中使用,例如 if not stack:...,因为 if 语句中的表达式在内部使用 bool() 进行评估。这构成了确定布尔值是True还是False的基础。

对于小于 3.11 的 Python 版本,可以使用 typing_extensions 模块来导入 Self 类型,其余的代码可以保持不变:

代码语言:javascript
复制
# stack.py

from typing import Any
from typing_extensions import Self

# ...

通过从 typing_extensions 导入 Self,你可以像在 Python 3.11 中使用类型模块一样使用 Self 来注释方法。

注意: typing_extensions 是使用 pip 安装的第三方库。因为 typing 是标准库的一部分,它只能在 Python 本身的定期版本中更新,而 typing_extensions 是将新特性反向移植到旧 Python 版本中。

.push()items追加到堆栈并返回更新的堆栈实例,因此需要使用 Self 注释。这样,我们就可以按顺序将 .push() 方法链入堆栈实例,从而使代码更加简洁易读:

代码语言:javascript
复制
>>> from stack import Stack
>>> stack = Stack()
>>> stack.push(1).push(2).push(3).pop()
3
>>> stack.items
[1, 2]

在上面的示例中,我们实例化了一个 Stack 实例,将三个元素依次推入堆栈,并弹出一个元素。通过包含 Self 作为注释,我们可以直接从实例化对象检查 .push(),查看它返回什么:

VS代码识别.push()的返回类型

当在 VS 代码中将鼠标悬停在.push()上时,可以看到返回类型是 Stack,正如注释所指出的那样。有了这个注释,其他人阅读我们的代码时就不必查看堆栈定义就能知道.push()返回的是类实例。

接下来,我们将看到一个表示银行账户状态和逻辑的类。BankAccount类支持多种操作,如存入和取出资金,这些操作更新账户状态并返回类实例。例如,.deposit()将美元金额作为输入,增加账户的内部余额,并返回实例,这样我们就可以链入其他方法:

代码语言:javascript
复制
# accounts.py

from dataclasses import dataclass
from typing import Self

@dataclass
class BankAccount:
    account_number: int
    balance: float

    def display_balance(self) -> Self:
        print(f"Account Number: {self.account_number}")
        print(f"Balance: ${self.balance:,.2f}\n")
        return self

    def deposit(self, amount: float) -> Self:
        self.balance += amount
        return self

    def withdraw(self, amount: float) -> Self:
        if self.balance >= amount:
            self.balance -= amount
        else:
            print("Insufficient balance")
        return self

在 BankAccount 中,我们可以用Self注释.display_balance().deposit().withdraw(),以返回该类的实例。我们可以将BankAccount实例化,并存入或取出任意次数的资金:

代码语言:javascript
复制
>>> from accounts import BankAccount
>>> account = BankAccount(account_number=1534899324, balance=50)
>>> (
...     account.display_balance()
...     .deposit(50)
...     .display_balance()
...     .withdraw(30)
...     .display_balance()
... )
Account Number: 1534899324
Balance: $50.00

Account Number: 1534899324
Balance: $100.00

Account Number: 1534899324
Balance: $70.00

BankAccount(account_number=1534899324, balance=70)

在这里,我们定义了一个具有帐号和初始余额的BankAccount实例。然后,我们用多个方法链执行存款、取款和余额显示,每个方法都返回 SelfREPL将自动打印方法链中最后一个表达式 .display_balance() 的返回值。其输出 BankAccount(account_number=1534899324, balance=50),为该类提供了一个很好的表示。

注意:BankAccount是一个数据类。数据类是定义类的一种很好的方法,它们具有许多有用的特性。因为BankAccount是一个数据类,所以你不需要定义构造函数,并且该类可以通过默认的.__repr__()方法得到一个很好的字符串表示。

Self类型的其他用例包括类方法和继承层次结构。例如,如果父类和子类都有返回 Self 的方法,那么我们可以用 Self 类型来注释这两个方法。

有趣的是,当子类对象调用返回自身的父类方法时,类型检查器将指示该方法返回子类的实例。我们可以通过创建一个继承自BankAccountSavingsAccount类来了解这一思想:

代码语言:javascript
复制
# accounts.py

import random
from dataclasses import dataclass
from typing import Self

# ...

@dataclass
class SavingsAccount(BankAccount):
    interest_rate: float

    @classmethod
    def from_application(
        cls, deposit: float = 0, interest_rate: float = 1
    ) -> Self:
        # Generate a random seven-digit bank account number
        account_number = random.randint(1000000, 9999999)
        return cls(account_number, deposit, interest_rate)

    def calculate_interest(self) -> float:
        return self.balance * self.interest_rate / 100

    def add_interest(self) -> Self:
        self.deposit(self.calculate_interest())
        return self

SavingsAccount有一个.from_application()方法,该方法通过申请人参数创建类实例,而不是通过常规的构造函数。它还有.add_interest()方法,用于将利息存入账户余额并返回类实例。我们可以使用Self类型提高这两个方法的可读性和可维护性。

接下来,从一个新的应用程序中创建一个SavingsAccount对象,进行存款和取款,并增加利息:

代码语言:javascript
复制
>>> from accounts import SavingsAccount
>>> savings = SavingsAccount.from_application(deposit=100, interest_rate=5)
>>> (
...     savings.display_balance()
...     .add_interest()
...     .display_balance()
...     .deposit(50)
...     .display_balance()
...     .withdraw(30)
...     .add_interest()
...     .display_balance()
... )
Account Number: 3631051
Balance: $100.00

Account Number: 3631051
Balance: $105.00

Account Number: 3631051
Balance: $155.00

Account Number: 3631051
Balance: $131.25

SavingsAccount(account_number=3631051, balance=131.25, interest_rate=5)

VS Code识别.from_application()返回一个SavingsAccount的实例:

VS代码识别.from_application()的返回类型

当你悬停在.from_application()上时,类型检查器显示返回类型是SavingsAccount。VS Code也识别出.deposit()的返回类型是SavingsAccount,尽管这个方法是在BankAccount父类中定义的:

VS代码识别继承方法的返回类型

总的来说,Self 类型是一个直观和 Pythonic 的选择,用于注释返回 Self 的方法,或者更广泛地说,返回类实例的方法。静态类型检查器可以识别 Self,你也可以导入这个符号,这样运行代码就不会导致名称错误。

在接下来的章节中,我们将探索 Self 类型的替代方法并查看它们的实现。Self 是一种相当新的类型,在添加 Self 之前已经存在几种替代方法。我们在阅读旧代码时可能会遇到这些其他注释,因此了解它们如何工作以及它们的局限性非常重要。

使用TypeVar注释

另一种注释返回类实例的方法是使用TypeVar。类型变量是一种类型,它可以在类型检查过程中作为特定类型的占位符。类型变量通常用于通用类型,例如特定对象的列表,如list[str]list[BankAccount]

TypeVar 允许你声明泛型类型和函数定义的参数,这使它成为注释返回类实例的方法的有效候选。要在这种情况下使用 TypeVar,我们可以从 Python 的类型模块中导入它,并在构造函数中给我们的类型命名:

代码语言:javascript
复制
# stack.py

from typing import TypeVar

TStack = TypeVar("TStack", bound="Stack")

在上面的示例中,我们创建了TStack类型变量,我们可以用它来注释Stack类中的.push()。在这种情况下,TStack 被 Stack 绑定,允许类型变量具体化为 Stack 或 Stack 的子类型。现在,我们可以使用 TStack 对方法进行注释:

代码语言:javascript
复制
 1# stack.py
 2
 3from typing import Any, TypeVar
 4
 5TStack = TypeVar("TStack", bound="Stack")
 6
 7class Stack:
 8    def __init__(self) -> None:
 9        self.items: list[Any] = []
10
11    def push(self: TStack, item: Any) -> TStack:
12        self.items.append(item)
13        return self
14
15    # ...

在第 11 行中,我们用 TStack 类型注释了 .push()。同时注意到你用TStack注释了 Self 参数。这是静态类型检查器正确地将TStack实体化为Stack所必需的。被类绑定的TypeVar可以具体化为任何子类。这在BankAccount和SavingsAccount示例中非常有用:

代码语言:javascript
复制
# accounts.py
from typing import TypeVar

# 创建由BankAccount类绑定的TBankAccount类型
TBankAccount = TypeVar("TBankAccount", bound="BankAccount")

这里,TBankAccount 被 BankAccount 绑定,我们可以正确地注释在 BankAccount 中返回 Self 的方法:

代码语言:javascript
复制
# accounts.py

from dataclasses import dataclass
from typing import TypeVar

TBankAccount = TypeVar("TBankAccount", bound="BankAccount")

@dataclass
class BankAccount:
    account_number: int
    balance: float

    def display_balance(self: TBankAccount) -> TBankAccount:
        print(f"Account Number: {self.account_number}")
        print(f"Balance: ${self.balance:,.2f}\n")
        return self

    # ...

我们可以用 TBankAccount 来注释 .display_balance() 以指定它将返回一个类实例。重要的是要记住 TBankAccount 与 BankAccount 并不相同。相反,它是一个类型变量,在类型检查时代表 BankAccount 类型。

TBankAccount 除了在不能直接使用 BankAccount 的注释中表示 BankAccount 类型外没有其他用途。我们还可以使用该类型来注释 SavingsAccount 子类中的方法:

代码语言:javascript
复制
# accounts.py

import random
from dataclasses import dataclass
from typing import TypeVar

TBankAccount = TypeVar("TBankAccount", bound="BankAccount")

# ...

@dataclass
class SavingAccount(BankAccount):
    interest_rate: float

    @classmethod
    def from_application(
        cls: type[TBankAccount], deposit: float = 0, interest_rate: float = 1
    ) -> TBankAccount:
        # Generate a random seven-digit bank account number
        account_number = random.randint(1000000, 9999999)
        return cls(account_number, deposit, interest_rate)

    # ...

我们在 SavingsAccount.from_application() 中注释了 TBankAccount 类型变量,并在 cls 参数中注释了 type[TBankAccount]。对于 BankAccount 和 SavingsAccount 来说,大多数静态类型检查程序应该能够识别出这是有效的类型提示。

主要的缺点是TypeVar是冗长的,开发者很容易忘记实例化一个TypeVar实例或正确的绑定实例到一个类。还需要注意的是,并不是所有的集成开发环境在检查方法时都能识别TypeVar。这些就是为什么Self类型比更受欢迎的主要原因。

在下一节中,我们将探索 Self 和 TypeVar 的另一种选择,__future__ 模块。你将看到这种方法如何克服 TypeVar 的冗长,但仍然不是 Self 类型的首选,因为它不能很好地支持继承。

使用 __future__ 模块

Python 的 __future__模块为注释返回外层类的方法提供了一种不同的方法。__future__ 模块有时被用来引入不兼容的变化,这些变化将成为未来 Python 版本的一部分。

对于大于 3.7 的 Python 版本,你可以在脚本的顶部从 __future__ 模块导入注释功能,并直接使用类名作为注释。我们可以在 Stack 类中看到这一点:

代码语言:javascript
复制
 1# stack.py
 2
 3from __future__ import annotations
 4
 5from typing import Any
 6
 7class Stack:
 8    def __init__(self) -> None:
 9        self.items: list[Any] = []
10
11    def push(self, item: Any) -> Stack:
12        self.items.append(item)
13        return self
14
15    # ...

在第 3 行,我们从 __future__ 导入了注释,我们可以使用注释特性,这些特性在我们使用的 Python 版本中可能是不可用的。在第 11 行,我们直接使用类名作为 .push() 的注释。你可以在检查 .push() 时看到注释,就像前面一样。

注意: 你必须在脚本的顶部导入 __future__ 模块。这是必需的,因为 __future__ 改变了解析 Python 代码的方式,允许使用不兼容的特性。

在引擎盖下,注释不会被执行,而是存储为字符串,可以在以后执行。这种评估注释的方式引起了一些讨论,在未来的 Python 版本中可能会有更好的方法。

虽然 __future__ 模块可以用类名注释方法,但这并不是最好的做法,因为 Self 类型更直观,更符合 Pythonic。另外,在脚本的顶部记住从 __future__ 导入可能会很麻烦。更重要的是,当使用 __future__ 进行注释时,继承并没有得到正确的支持。看看当你使用 __future__ 注释时,SavingsAccount 方法会发生什么:

代码语言:javascript
复制
# accounts.py

from __future__ import annotations

import random
from dataclasses import dataclass
from typing import Self

@dataclass
class BankAccount:
    account_number: int
    balance: float

    # ...

    def deposit(self, amount: float) -> BankAccount:
        self.balance += amount
        return self
    # ...

@dataclass
class SavingsAccount(BankAccount):
    interest_rate: float

    @classmethod
    def from_application(
        cls, deposit: float = 0, interest_rate: float = 1
    ) -> SavingsAccount:
        # Generate a random seven-digit bank account number
        account_number = random.randint(1000000, 9999999)
        
        return cls(account_number, deposit, interest_rate)

    def calculate_interest(self) -> float:
        return self.balance * self.interest_rate / 100

    def add_interest(self) -> SavingsAccount:
        self.deposit(self.calculate_interest())
        return self

上面的代码重新定义了SavingsAccount,它继承自BankAccount。请注意我们是如何在BankAccount中用BankAccount注释.deposit()的,又是如何在SavingsAccount中用SavingsAccount注释返回Self的方法的。一切看起来都很好,但是看看当你从SavingsAccount的实例中检查.deposit()的类型时会发生什么:

从SavingsAccount继承的方法被错误地注释为BankAccount。

.deposit() 的返回类型显示为 BankAccount,尽管该对象是 SavingsAccount 的实例。这是因为 SavingsAccount 继承自 BankAccount,而 future 注释并不正确地支持继承。当你检查 .add_interest() 时,这会产生更多的类型检查问题:

.add_interest()的类型检查失败是因为.deposit()的注释不正确。

.add_interest() 的类型检查失败是因为类型检查器认为 deposit() 返回一个 BankAccount 实例,但是 BankAccount 没有名为.add_interest() 的方法。这说明了 __future__ 注释最突出的缺陷。虽然 __future__ 注释可能适用于许多类,但在键入继承方法时却不合适。

在下一节中,我们将探索一个注释,它在功能上类似于__future__注释,但比__future__注释更直接。这将帮助你理解 __future__ 在引擎盖下的作用。请记住,返回类实例的方法的所有替代注释都不再被认为是最佳实践。你应该选择 Self 类型,但是理解这些替代注释是有好处的,因为你可能会在代码中遇到它们。

字符串类型提示

最后,你可以使用字符串来注释返回类实例的方法。对于小于 3.7 的 Python 版本,或者当其它方法都不起作用时,应该使用字符串注释。字符串注释不需要任何导入,大多数静态类型检查器都能识别它:

代码语言:javascript
复制
# stack.py

from typing import Any

class Stack:
    def __init__(self) -> None:
        self.items: list[Any] = []

    def push(self, item: Any) -> "Stack":
        self.items.append(item)
        return self

    # ...

在这种情况下,字符串注释应该包含类的名称。否则,静态类型检查器不会将返回类型识别为有效的 Python 对象。字符串注释直接完成类似于 __future__注释在幕后所做的事情。

字符串注释的一个主要缺点是它们不会随继承而保留。当子类从超类继承方法时,超类中指定为字符串的注释不会自动传播到子类中。这意味着,如果我们依赖字符串注释来进行类型提示或文档说明,那么我们需要在每个子类中重新声明注释,这可能会容易出错且耗时。

许多开发者还发现字符串注释的语法与 Python 的其它特性相比显得不寻常或不习惯。在 Python 3 的早期版本中,当类型提示被引入时,字符串注释是唯一可用的选项。然而,随着typing模块和类型提示语法的引入,我们现在有了一种更标准、更有表现力的方式来注释类型。

结论

在 Python 中使用类型提示注释可以使你的代码更具可读性和可维护性,尤其是当代码的大小和复杂性增加时。通过指明变量函数参数返回值的类型,我们可以帮助其他开发者理解变量的预期类型以及函数调用的预期。

Self类型是一种特殊的类型提示,我们可以使用它来注释返回类实例的方法。这使得返回类型显式化,有助于防止在处理继承和子类时可能出现的微妙 bug。虽然我们可以使用其它选项,如 TypeVar、__future__ 模块和字符串来注释返回类实例的方法,但在可能的情况下,我们应该使用 Self 类型。

通过从 typing 模块导入 Self 类型,或者在 Python 3.10 及更早版本中从 typing_extensions 中导入,你可以注释返回类实例的方法,使你的代码更易于维护和阅读。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-01-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据STUDIO 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 如何在Python中使用Self类型来注释方法
  • 使用TypeVar注释
  • 使用 __future__ 模块
  • 字符串类型提示
  • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档