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 [email protected], Philip House [email protected], Ivan Levkivskyi [email protected], Lisa Roach [email protected], Guido van Rossum [email protected]
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)
- 非本文目標(Non-goals)
- 規范(Specification)
- 全域和區域變數的注解(Global and local variable annotations)
- 類和實體變數的注解(Class and instance variable annotations)
- 運算式的注解(Annotating expressions)
- 注解允許出現的位置(Where annotations aren't allowed)
- 存根檔案中的變數注解(Variable annotations in stub files)
- 變數注解的推薦編碼風格(Preferred coding style for variable annotations)
- 標準庫和檔案的改動(Changes to Standard Library and Documentation)
- 型別注解的運行時效果(Runtime Effects of Type Annotations)
- 注解的其他用途(Other uses of 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)
本文已在公共領域發布,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/175714.html
標籤:Python
