题目中的 return self
并不是我们常见的 self
参数,而本文的首要任务是需要了解什么是类型提示以及它们如何工作。类型提示我们可以显式地指明变量类型、函数参数和返回值。这可以使代码更具可读性和可维护性,尤其是当代码的规模和复杂性不断增加时。
我们可以使用冒号(:)
指定变量和函数参数类型,然后是数据类型,而返回值注释则使用破折号(->)
,然后是返回类型。
举例说明,我们可以编写一个函数,输入我们购买的馅饼数量和每个馅饼的价格,然后输出一个总结我们的交易的字符串:
>>> 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_pies
和price_per_pie
中推断出total_cost
的类型,因此total_cost
不需要进行类型注释。
Python 中的类型和注释通常不会影响代码的功能,但是许多静态类型检查器和 IDE 可以识别它们。例如,如果你在 VS Code 中悬停在 buy_pies()
上,那么你可以看到每个参数或返回值的类型:
在处理类时,我们还可以使用注释。这可以帮助其他开发人员了解方法的返回类型,在处理复杂的类层次结构时尤其有用。甚至可以对返回类实例的方法进行注释。
类型和注释可以用来注释返回类实例的方法。这对于 class
方法特别有用,可以防止在处理继承和方法链时出现的混乱。
不幸的是,对这些方法进行注释可能会造成混淆并导致意外错误。注释此类方法的一种自然方法是使用类名,但下面的方法行不通:
# 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异常:
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()
注释:
# stack.py
from typing import Any, Self
class Stack:
def __init__(self) -> None:
self.items: list[Any] = []
def push(self, item: Any) -> Self:
self.items.append(item)
return self
def pop(self) -> Any:
if self.__bool__():
return self.items.pop()
else:
raise ValueError("Stack is empty")
def __bool__(self) -> bool:
return len(self.items) > 0
在第3行中从类型中导入Self
类型,并在第9行中用 -> Self
注释.push()
。这将告诉静态类型检查器 .push()
返回一个 Stack 实例,从可以将多个 push
串联起来。注意,通常没有必要注释self
和cls
参数。
注意: 我们实现了.__bool__()
来检查堆栈是否为空。这个方法是 Python 数据模型的一部分,被称为 dunder 或特殊方法。在这种情况下,定义.__bool__()
从类内部或外部调用bool()
内置函数来检查堆栈是否为空。
.__bool__()
的加入使得该类可以在 Pythonic 条件句中使用,例如 if not stack:...
,因为 if
语句中的表达式在内部使用 bool()
进行评估。这构成了确定布尔值是True
还是False
的基础。
对于小于 3.11 的 Python 版本,可以使用 typing_extensions
模块来导入 Self
类型,其余的代码可以保持不变:
# 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()
方法链入堆栈实例,从而使代码更加简洁易读:
>>> 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()
将美元金额作为输入,增加账户的内部余额,并返回实例,这样我们就可以链入其他方法:
# 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实例化,并存入或取出任意次数的资金:
>>> 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
实例。然后,我们用多个方法链执行存款、取款和余额显示,每个方法都返回 Self
。REPL
将自动打印方法链中最后一个表达式 .display_balance()
的返回值。其输出 BankAccount(account_number=1534899324, balance=50)
,为该类提供了一个很好的表示。
注意:BankAccount是一个数据类。数据类是定义类的一种很好的方法,它们具有许多有用的特性。因为BankAccount是一个数据类,所以你不需要定义构造函数,并且该类可以通过默认的.__repr__()
方法得到一个很好的字符串表示。
Self
类型的其他用例包括类方法和继承层次结构。例如,如果父类和子类都有返回 Self
的方法,那么我们可以用 Self
类型来注释这两个方法。
有趣的是,当子类对象调用返回自身的父类方法时,类型检查器将指示该方法返回子类的实例。我们可以通过创建一个继承自BankAccount
的SavingsAccount
类来了解这一思想:
# 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对象,进行存款和取款,并增加利息:
>>> 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 的类型模块中导入它,并在构造函数中给我们的类型命名:
# stack.py
from typing import TypeVar
TStack = TypeVar("TStack", bound="Stack")
在上面的示例中,我们创建了TStack类型变量,我们可以用它来注释Stack类中的.push()
。在这种情况下,TStack 被 Stack 绑定,允许类型变量具体化为 Stack 或 Stack 的子类型。现在,我们可以使用 TStack 对方法进行注释:
# stack.py
from typing import Any, TypeVar
TStack = TypeVar("TStack", bound="Stack")
class Stack:
def __init__(self) -> None:
self.items: list[Any] = []
def push(self: TStack, item: Any) -> TStack:
self.items.append(item)
return self
# ...
在第 11 行中,我们用 TStack 类型注释了 .push()
。同时注意到你用TStack注释了 Self
参数。这是静态类型检查器正确地将TStack实体化为Stack所必需的。被类绑定的TypeVar可以具体化为任何子类。这在BankAccount和SavingsAccount示例中非常有用:
# accounts.py
from typing import TypeVar
# 创建由BankAccount类绑定的TBankAccount类型
TBankAccount = TypeVar("TBankAccount", bound="BankAccount")
这里,TBankAccount 被 BankAccount 绑定,我们可以正确地注释在 BankAccount 中返回 Self
的方法:
# 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 子类中的方法:
# 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 类中看到这一点:
# stack.py
from __future__ import annotations
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
# ...
在第 3 行,我们从 __future__
导入了注释,我们可以使用注释特性,这些特性在我们使用的 Python 版本中可能是不可用的。在第 11 行,我们直接使用类名作为 .push()
的注释。你可以在检查 .push()
时看到注释,就像前面一样。
注意: 你必须在脚本的顶部导入 __future__
模块。这是必需的,因为 __future__
改变了解析 Python 代码的方式,允许使用不兼容的特性。
在引擎盖下,注释不会被执行,而是存储为字符串,可以在以后执行。这种评估注释的方式引起了一些讨论,在未来的 Python 版本中可能会有更好的方法。
虽然 __future__
模块可以用类名注释方法,但这并不是最好的做法,因为 Self
类型更直观,更符合 Pythonic。另外,在脚本的顶部记住从 __future__
导入可能会很麻烦。更重要的是,当使用 __future__
进行注释时,继承并没有得到正确的支持。看看当你使用 __future__
注释时,SavingsAccount 方法会发生什么:
# 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 版本,或者当其它方法都不起作用时,应该使用字符串注释。字符串注释不需要任何导入,大多数静态类型检查器都能识别它:
# 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
中导入,你可以注释返回类实例的方法,使你的代码更易于维护和阅读。