PEP 526 -- 變量注解的語法(Syntax for Variable Annotations)
英文原文:https://www.python.org/dev/peps/pep-0526
采集日期:2020-02-27
PEP: 526
Title: Syntax for Variable Annotations
Author: Ryan Gonzalez rymg19@gmail.com, Philip House phouse512@gmail.com, Ivan Levkivskyi levkivskyi@gmail.com, Lisa Roach lisaroach14@gmail.com, Guido van Rossum guido@python.org
Status: Final
Type: Standards Track
Created: 09-Aug-2016
Python-Version: 3.6
Post-History: 30-Aug-2016, 02-Sep-2016
Resolution: https://mail.python.org/pipermail/python-dev/2016-September/146282.html
目錄
- 重要程度(Status)
- 評論者的注意事項(Notice for Reviewers)
- 摘要(Abstract)
- 原由(Rationale)
- 規范(Specification)
- 標准庫和文檔的改動(Changes to Standard Library and Documentation)
- 類型注解的運行時效果(Runtime Effects of Type Annotations)
- 被拒絕/擱置的提案(Rejected/Postponed Proposals)
- 向下兼容性(Backwards Compatibility)
- 實現代碼(Implementation)
- 版權(Copyright)
重要程度(Status)
本 PEP 暫時已被 BDFL 收錄。更多觀點請參閱收錄信息。
評論者的注意事項(Notice for Reviewers)
本 PEP 是在單獨的 repo 中起草的:https://github.com/phouse512/peps/tree/pep-0526。
初步的討論位於 python-ideas 和 https://github.com/python/typing/issues/258 上。
若要在公共論壇上提出異議,至少請先閱讀一下本 PEP 最后列出的被拒絕提議的主要內容。
摘要(Abstract)
PEP 484 引入了類型提示(type hint),又稱類型注解(type annotation)。盡管其重點是函數注解,但也引入了類型注釋(type comment)的概念用於注解變量:
# 'primes' is a list of integers
primes = [] # type: List[int]
# 'captain' is a string (Note: initial value is a problem)
captain = ... # type: str
class Starship:
# 'stats' is a class variable
stats = {} # type: Dict[str, int]
本文旨在為 Python 添加一種語法,用於對變量(包括類變量和實例變量)的類型做出注解,以取代通過注釋(comment)來表達類型的方式:
primes: List[int] = []
captain: str # Note: no initial value!
class Starship:
stats: ClassVar[Dict[str, int]] = {}
PEP 484 明確指出類型注釋旨在幫助復雜情況下的類型推斷,本 PEP 不會改變此意圖。但實際情況是類變量和實例變量也用到了類型注釋,因此本 PEP 還討論了為這些變量添加類型注解的用法。
原由(Rationale)
盡管類型注釋已經夠用了,但也表現出一些缺點:
-
文本編輯器經常會將注釋高亮顯示為類型注解不同的方式。
-
無法為未定義變量添加類型注釋,需將其初始化為
None
(例如a = None # type: int
) -
條件分支語句內的變量注釋可讀性不好:
if some_value:
my_var = function() # type: Logger
else:
my_var = another_function() # Why isn't there a type here?
-
因為類型注釋其實不是語言的組成部分,如果 Python 代碼要對其進行解析,就需要自定義解析程序,而不能只用
ast
(Abstract Syntax Tree,抽象語法樹) 解決。 -
類型注釋已大量應用於 typeshed 中。將 typeshed 遷移為采用變量注解的語法,替換掉類型注釋,可以提高存根文件的可讀性。
-
在混合使用普通注釋和類型注釋的場合,要做出區分是比較困難的:
path = None # type: Optional[str] # Path to module source
- 除了嘗試查看模塊的源代碼並在運行時進行解析,就再無他法在運行時讀取注解信息了。至少可以認為這種做法不夠優雅。
通過讓注解語法成為語言的核心內容,可以緩解上述大多數問題。此外,作為由 PEP 484 定義的名稱定型(nominal typing)的補充,專用於類和實例變量(方法注解)的注解語法將為靜態鴨子定型鋪平道路,
非本文目標(Non-goals)
雖然本提案和用於運行時讀取注解信息的標准庫函數 typing.get_type_hints
擴展一起出現,但變量注解並不是為運行時類型檢查而設計的。必須開發第三方軟件包才能實現該類型檢查功能。
還應該強調的是,**Python 仍將是一種動態定型語言,並且按慣例作者不希望讓類型提示成為強制要求。類型注解不應與靜態定型語言中的變量聲明相混淆。注解語法旨在為第三方工具提供一種簡便的方法,用於定義結構化類型的元數據。
本 PEP 不需要類型檢查程序改變其類型檢查規則。這里只是提供了一種可讀性更好的語法,以便替換類型注釋。
規范(Specification)
可以在一條賦值語句或某個表達式中加入類型注解,以向第三方類型檢查程序標示出被注解對象的應有類型:
my_var: int
my_var = 5 # Passes type check.
other_var: int = 'a' # Flagged as error by type checker,
# but OK at runtime.
上述表達式並沒有引入超過 PEP 484 范圍的新語義,因此以下三條語句是等效的:
var = value # type: annotation
var: annotation; var = value
var: annotation = value
下面給出各種上下文環境中的類型注解語法定義,以及運行時的效果。
同時給出了類型檢查程序對注釋的解析建議,但這些建議不是必須遵守的。這符合 PEP 484 對合規性的態度。
全局和局部變量的注解(Global and local variable annotations)
局部和全局變量的類型可以如下做出注解:
some_number: int # variable without initial value
some_list: List[int] = [] # variable with initial value
省略初始值能讓條件分支語句中的變量更容易定型:
sane_world: bool
if 2+2 == 4:
sane_world = True
else:
sane_world = False
注意,盡管語法上確實允許元組打包時帶上注解,但在采用元組解包寫法時不允許注解變量的類型。
# Tuple packing with variable annotation syntax
t: Tuple[int, ...] = (1, 2, 3)
# or
t: Tuple[int, ...] = 1, 2, 3 # This only works in Python 3.8+
# Tuple unpacking with variable annotation syntax
header: str
kind: int
body: Optional[List[str]]
header, kind, body = message
若省略初始值,則變量為未初始化狀態:
a: int
print(a) # raises NameError
如果給局部變量加上注解,則會讓解釋器一直將其視為局部變量:
def f():
a: int
print(a) # raises UnboundLocalError
# Commenting out the a: int makes it a NameError.
以下代碼也是一樣:
def f():
if False: a = 0
print(a) # raises UnboundLocalError
重復的類型注解將被忽略。但靜態類型檢查程序可以發出一條警告信息,提示同一個變量注解為不同類型:
a: int
a: str # Static type checker may or may not warn about this.
類和實例變量的注解(Class and instance variable annotations)
類型注解也可在類和方法內部用於為類和實例變量加上注解。特別是 a: int
這種不給出值的注解,使得應在 __init__
或 __new__
中進行初始化的實例變量也能加上注解。建議語法如下:
class BasicStarship:
captain: str = 'Picard' # instance variable with default
damage: int # instance variable without default
stats: ClassVar[Dict[str, int]] = {} # class variable
以上的 ClassVar
是一個由 typing 模塊定義的特殊類,向靜態類型檢查程序標示在實例中不允許對該變量進行賦值。
請注意,無論嵌套多少層,ClassVar
的參數中都不能包含任何類型變量:如果 T
是類型變量的話,ClassVar[T]
和 ClassVar[List[Set[T]]]
都是非法的。
用個更詳細的例子來演示一下吧。
class Starship:
captain = 'Picard'
stats = {}
def __init__(self, damage, captain=None):
self.damage = damage
if captain:
self.captain = captain # Else keep the default
def hit(self):
Starship.stats['hits'] = Starship.stats.get('hits', 0) + 1
在以上類中,stats
應該是一個類變量(用於記錄每局游戲的各種狀態),而 captain
則是一個默認值由類設置的實例變量。類型檢查程序可能發現不了這兩者的差異:兩者都在類中進行了初始化,但 captain
僅作為便於實例變量使用的默認值,而 stats
則真是打算讓所有實例共享的類變量。
由於兩個變量恰好都在類這個級別進行了初始化,因此將類變量標記為以 ClassVar[...]
包裹的類型注釋,對區分他們是很有用的。這樣若對實例中同名屬性發生意外賦值,類型檢查程序就可以做出標記。
比如對上述類加上以下注解:
class Starship:
captain: str = 'Picard'
damage: int
stats: ClassVar[Dict[str, int]] = {}
def __init__(self, damage: int, captain: str = None):
self.damage = damage
if captain:
self.captain = captain # Else keep the default
def hit(self):
Starship.stats['hits'] = Starship.stats.get('hits', 0) + 1
enterprise_d = Starship(3000)
enterprise_d.stats = {} # Flagged as error by a type checker
Starship.stats = {} # This is OK
為了方便使用和遵循慣例,實例變量可以在 __init__
或其他方法中進行注解,而不是在類中進行:
from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, content):
self.content: T = content
表達式的注解(Annotating expressions)
注解的對象可以是任一合法的可賦值物,至少在語法上是如此的(視類型檢查程序采取的對策而定):
class Cls:
pass
c = Cls()
c.x: int = 0 # Annotates c.x with int.
c.y: int # Annotates c.y with int.
d = {}
d['a']: int = 0 # Annotates d['a'] with int.
d['b']: int # Annotates d['b'] with int.
請注意,雖然帶括號的變量名也被視為表達式,但其不是簡單名稱(simple name):
(x): int # Annotates x with int, (x) treated as expression by compiler.
(y): int = 0 # Same situation here.
注解允許出現的位置(Where annotations aren't allowed)
在函數同級作用域內將變量注解為 global
或 nonlocal
是非法操作。
def f():
global x: int # SyntaxError
def g():
x: int # Also a SyntaxError
global x
原因就是這些變量並不歸屬於 global
或 nonlocal
,因此類型注解歸屬於擁有變量的作用域。
只允許存在一個賦值對象和一個右值。此外,不能對 for
或 with
語句中用到的變量進行注解,可以像元組解包那樣提前做出注解:
a: int
for a in my_iter:
...
f: MyFile
with myfunc() as f:
...
存根文件中的變量注解(Variable annotations in stub files)
因為變量注解的可讀性比類型注釋更好,所以推薦所有版本 Python(包括 Python 2.7)的存根文件使用。請注意,Python 解釋器不會執行存根文件,因此變量注解不會引發報錯。類型檢查程序應當支持所有版本 Python 存根文件中的變量注解。例如:
# file lib.pyi
ADDRESS: unicode = ...
class Error:
cause: Union[str, unicode]
變量注解的推薦編碼風格(Preferred coding style for variable annotations)
對於模塊級變量、類與實例變量、局部變量,類型注解的冒號后面應帶一個空格。冒號前則不應有空格。如果賦值有右值,則等號兩邊都應帶有一個空格。例如:
- Yes::
code: int
class Point:
coords: Tuple[int, int]
label: str = '<unknown>'
- No::
code:int # No space after colon
code : int # Space before colon
class Test:
result: int=0 # No spaces around equality sign
標准庫和文檔的改動(Changes to Standard Library and Documentation)
-
typing
中已新加入一個協變類型ClassVar[T_co]
。它只接受一個參數,應為一個合法的類型,它應用於不允許在類實例中賦值的類變量。這一約束由靜態檢查程序來保證,而不是運行時。有關ClassVar
用法的示例和說明,請參閱類和實例變量的注解部分;有關實施ClassVar
背后原因的更多信息,請參見被拒絕/擱置的提案部分。 -
typing
模塊中的函數get_type_hints
將會作出擴展,以便在運行時可由模塊、類和函數中讀取類型注解。注解以字典映射的形式返回,由變量或參數映射為類型提示,若有向前引用則會先解析求值(evaluate)。如果是類,則按方法的解析順序返回由注解構造的映射(或許是個collections.ChainMap
)。 -
文檔中將會加入注解的推薦使用指南,包括本 PEP 和 PEP 484 所介紹規范的內容摘要。此外,還將發布一款將類型注釋轉換為類型注解的助手代碼,其將與標准庫分開發布。
類型注解的運行時效果(Runtime Effects of Type Annotations)
即便某本地變量從未賦值,只要對其添加了注解,解釋器就將視其為本地變量。本地變量的注解不會被解析求值。
def f():
x: NonexistentName # No error.
但如果變量是模塊或類級別的,則類型注解會被解析求值。
x: NonexistentName # Error!
class X:
var: NonexistentName # Error!
此外在模塊或類級別,如果被注解對象是簡單名稱,則將其和注解一起存放於模塊或類的 __annotations__
屬性中,若為私有變量則信息會不全(mangle),形式為名稱和已解析注釋的有序字典。示例如下。
from typing import Dict
class Player:
...
players: Dict[str, Player]
__points: int
print(__annotations__)
# prints: {'players': typing.Dict[str, __main__.Player],
# '_Player__points': <class 'int'>}
__annotations__
是可寫入屬性,因此以下操作是允許執行的:
__annotations__['s'] = str
但如果試圖將 __annotations__
修改為有序映射之外的其他對象,則可能會引發 TypeError:
class C:
__annotations__ = 42
x: int = 5 # raises TypeError
注意:給 __annotations__
賦值是 Python 解釋器允許的操作,它不會過問。但隨后的類型注解應該是 MutableMapping
類型,於是才會失敗。
在運行時讀取注解的推薦方式是采用 typing.get_type_hints
函數。與所有雙下划線(dunder)屬性一樣,任何未在文檔注明的對 __annotations__
的使用都難免失敗,且不會發出警告:
from typing import Dict, ClassVar, get_type_hints
class Starship:
hitpoints: int = 50
stats: ClassVar[Dict[str, int]] = {}
shield: int = 100
captain: str
def __init__(self, captain: str) -> None:
...
assert get_type_hints(Starship) == {'hitpoints': int,
'stats': ClassVar[Dict[str, int]],
'shield': int,
'captain': str}
assert get_type_hints(Starship.__init__) == {'captain': str,
'return': None}
請注意,如果靜態檢查沒有找到注解信息,則 __annotations__
字典根本不會被創建。而且本地存儲注解獲得的好處,並不能抵消每次函數調用時都得創建並填充注解字典的開銷。因此,對函數級別的注解不會作解析求值和存儲。
注解的其他用途(Other uses of annotations)
因為 Python 並不在意類型注解的存在,而不是“未經加載即作解析求值”,所以支持本 PEP 的 Python 不會拒絕以下形式:
alice: 'well done' = 'A+'
bob: 'what a shame' = 'F-'
除非用 # type: ignore
或 @no_type_check
進行了禁用,否則類型檢查程序在讀到注解時就會做出標記。
但正因為 Python 不在乎什么“類型”,所以如果以上代碼段是全局級別或位於某個類中,則__annotations__
將會包含 {'alice': 'well done', 'bob': 'what a shame'}
。
這些存儲下來的注解可用作其他用途,但本 PEP 明確推薦將類型提示作為注解的首選用途。
被拒絕/擱置的提案(Rejected/Postponed Proposals)
-
是否該引入變量注解?
變量注解已經以類型注釋(comment)的形式存在了將近兩年,PEP 484 也已認可。在第三方類型檢查程序(mypy、pytype、PyCharm等)和運用類型檢查程序的項目中,注解已得以廣泛應用。但是注釋語法存在着很多缺點,這在原由部分已有列出。本 PEP 並不討論類型注解的必要性,而是介紹這種注解的語法。 -
引入新的關鍵字:
首先,要選出好的關鍵字非常困難。比如不能為var
,因為這在變量名中太常見了。如果要用於類變量或全局變量,則也不能為local
。其次,無論選擇什么,仍需要用到__future__
導入語句。 -
用
def
作為關鍵字:
這種提案可能如下所示:
def primes: List[int] = []
def captain: str
這里的問題是,對於幾代 Python 程序員(和工具!)而言,def
都表示“定義一個函數”,用它同時定義變量並不會增加清晰度。(盡管這確實是主觀意見。)
-
用語法表明函數的用意:
此條提案建議用var = cast(annotation[, value])
注釋變量類型。盡管這種語法緩解了類型注釋的某些問題(如 AST 中沒有注解),但其他問題還是沒有解決(如高可讀性、可能引入運行時開銷)。 -
元組解包格式中允許加入類型注解:
這會導致歧義。以下語句的含義就不明:
x, y: T
x
和 y
都是 T
類型?或者 T
是由 x
和 y
得來的元組類型?或者 x
的類型為 Any
而 y
的類型為 T
?(如果出現在函數簽名中,則就是這個意思。)至少到目前為止禁止如此,不能讓人去猜。
-
注解采用括號形式
(var: type)
:
這是為解決上述歧義而在 python-ideas 上提出的,但語法啰嗦、好處不多且可讀性差,因此被拒絕。 -
在連續賦值語句中允許使用注解:
與元組解包格式類似,這存在歧義和可讀性問題。比如:
x: int = y = 1
z = w: int = 1
這里就存在歧義,y
和 z
應該是什么類型呢?而且第二行還難以作語法解析。
-
在
with
和for
語句中允許使用注解:
因為這樣在for
語句中會讓真正的迭代過程難以被發現,而在with
語句中則會引起 CPython 的 LL(1) 語法分析程序發生混亂。 -
在函數定義階段對本地注解進行解析估值:
Guido 已拒絕此提案,因為注解的位置強烈表明其位於周圍代碼的相同作用域內。 -
在函數作用域內存儲變量注解:
注解可本地訪問的收益不足以顯著抵消每次函數調用時創建和填充字典的開銷。 -
對帶有注解的變量未經賦值即進行初始化
有人在 python-ideas 上建議,將x: int
中的x
初始化為None
或其他特殊常量(類似 Javascript 的undefined
)。但是,在語言中新增一個單例值需要代碼處處做出判斷。因此,Guido 干脆地予以拒絕。 -
在 typing 模塊中也加入
InstanceVar
:
純屬多余。因為實例變量比類變量更為常用。常用用法理應默認。 -
僅允許在方法內對實例變量進行注解:
問題在於,除了初始化實例變量之外,許多__init__
方法還會干很多活兒,而且人眼很難找齊所有實例變量的注釋。有時__init__
會分解為更多助手方法,因此注釋就更加難以追蹤了。將實例變量的注釋放到類中,找起來可以更加輕松,也會給第一次閱讀代碼的人帶來便利。 -
類型變量的注釋采用
x: class t = v
的語法:
這樣會要求語法解析器變得更為復雜,class
關鍵字也會把簡單的語法高亮顯示程序弄糊塗。無論如何,都需要ClassVar
把類變量存儲到__annotations__
中去,因此就選用了更簡單的語法。 -
完全不用
ClassVar
:
mypy 無法區分類變量和實例變量,可貌似也能混的不錯,因此才會有這個提案。但是類型檢查程序利用這些附加信息能夠干很多有用的工作,比如標記出由實例對類變量的意外賦值。這種賦值會創建實例變量,將類變量遮掩起來(shadow)。類型檢查程序還可以將實例變量標記為默認可修改,聲明眾所周知的風險。 -
用
ClassAttr
替換ClassVar
:
ClassVar 更為合適,主要是因為類的屬性可以有很多,如方法、描述符等。但是從概念上講,只有特定的屬性才是類變量(或常量)。 -
不對注解進行解析求值,只視其為字符串:
對函數注解始終都會進行解析求值,這樣就會與其表現不一。盡管未來可能會重新考慮,但在 PEP 484 中已決定應該將其作為單獨的 PEP 進行規范。 -
在類的文檔字符串中對變量類型進行注解
許多項目已經應用了各種文檔字符串規范,一致性往往不太好,通常還不符合 PEP 484 的注解語法。並且也還是需要比較復雜的特殊語法解析器。本 PEP 的目標正是要與第三方類型檢查工具協作,如此目標就會落空。 -
將
__annotations__
實現為描述符
這條提案是為了禁止將__annotations__
設為除字典和None
之外的東西。Guido 拒絕了這個提案,認為沒有必要。如果試圖將__annotations__
修改為字典映射之外的任何東西,都會引發 TypeError。 -
將純注解視同全局或非局部的:
這條提案希望,出現在函數體內的無賦值注解不應進行任何解析求值。與之相反,本 PEP 表明,如果注解目標比單個名稱復雜,則應在函數體內的目標出現位置對其“左值部分”進行解析求值,以強制確認其是否已經定義。例如在以下示例中:
def foo(self):
slef.name: str
slef
就應該被解析求值,這樣若是其尚未定義(本例中貌似就是如此:-)),運行時將會引發錯誤。這樣就與帶初值時的表現更為一致,因此應該能減少意外情況的發生。還有一點請注意,如果注解目標是 self.name
(這次拼寫正確了:-)),那么做過優化的編譯器並不保證會對 self
進行解析求值,只要能夠證明其一定是已定義的即可。
向下兼容性(Backwards Compatibility)
本 PEP 完全向下兼容。
實現代碼(Implementation)
適用於 Python 3.6 的已實現代碼可在以下 GitHub repo 中找到:https://github.com/ilevkivskyi/cpython/tree/pep-526。
版權(Copyright)
本文已在公共領域發布。