PEP 526 – 變數註解語法
- 作者:
- Ryan Gonzalez <rymg19 at gmail.com>, Philip House <phouse512 at gmail.com>, Ivan Levkivskyi <levkivskyi at gmail.com>, Lisa Roach <lisaroach14 at gmail.com>, Guido van Rossum <guido at python.org>
- 狀態:
- 最終 (Final)
- 類型:
- 標準軌跡 (Standards Track)
- 主題:
- 類型標註 (Typing)
- 建立日期:
- 2016年8月9日
- Python 版本:
- 3.6
- 公告歷史:
- 2016年8月30日, 2016年9月2日
- 決議:
- Python-Dev 訊息
狀態
本 PEP 已獲 BDFL 臨時接受。詳情請見接受訊息:https://mail.python.org/pipermail/python-dev/2016-September/146282.html
審閱者須知
本 PEP 草案存放於獨立儲存庫:https://github.com/phouse512/peps/tree/pep-0526。
初步討論曾於 python-ideas 以及 https://github.com/python/typing/issues/258 進行。
在公開論壇提出反對意見之前,請至少閱讀本 PEP 末尾列出的 被拒絕想法 摘要。
摘要
PEP 484 引入了型別提示(Type Hints),即型別註解。雖然其主要焦點在於函式註解,但也引入了使用型別註解來標註變數的概念。
# '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]
本 PEP 旨在為 Python 新增語法,以便為變數(包括類別變數與實例變數)標註型別,而非透過註解的方式表達。
primes: List[int] = []
captain: str # Note: no initial value!
class Starship:
stats: ClassVar[Dict[str, int]] = {}
PEP 484 明確指出型別註解意在協助複雜情況下的型別推論,本 PEP 並未改變此初衷。然而,由於實務上型別註解也已被用於類別變數與實例變數,本 PEP 也探討了針對這些變數的型別註解使用方式。
原理
儘管型別註解運作良好,但透過註解方式表達仍存在一些缺點:
- 文字編輯器通常對註解的強調顯示方式與型別註解不同。
- 無法為未定義的變數標註型別;必須先將其初始化為
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。 - typeshed 中大量使用了型別註解。將 typeshed 改為使用變數註解語法取代型別註解,將能提高 Stub 檔案的可讀性。
- 在同時使用普通註解與型別註解的情況下,兩者難以區分。
path = None # type: Optional[str] # Path to module source
- 除非嘗試找到模組的原始碼並在執行時期進行解析,否則無法在執行時期擷取註解,這種做法至少稱不上優雅。
將該語法納入語言核心可解決大部分問題。此外,為類別與實例變數提供專屬的註解語法(外加函式註解),將為靜態鴨子型別(static duck-typing)鋪路,作為對 PEP 484 所定義的名義型別(nominal typing)的補充。
非目標
雖然本提案伴隨著擴充標準函式庫 typing.get_type_hints 函式以供在執行時期擷取註解,但變數註解並非為執行時期型別檢查而設計。第三方套件需自行實作相關功能。
必須強調的是,Python 將維持為動態型別語言,作者亦無意使型別提示成為強制要求,即便透過慣例亦然。型別註解不應與靜態型別語言中的變數宣告混淆。註解語法的目標是提供一種簡單的方式,為第三方工具指定結構化的型別中繼資料。
本 PEP 不要求型別檢查器變更其檢查規則。它僅提供更具可讀性的語法來取代型別註解。
規範
型別註解可以增加在賦值語句或單一表達式中,用以向第三方型別檢查器指示目標的預期型別。
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 對於合規性的態度一致。)
全域與區域變數註解
區域與全域變數的型別可依下列方式註解:
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.
類別與實例變數註解
型別註解也可用於註解類別主體與方法中的類別與實例變數。特別是無值的標註 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
表達式註解
註解的目標可以是任何有效的單一賦值目標(至少在語法上如此;這取決於型別檢查器如何處理)。
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.
請注意,即使是用括號括起來的名稱也被視為表達式,而非簡單名稱。
(x): int # Annotates x with int, (x) treated as expression by compiler.
(y): int = 0 # Same situation here.
不允許註解的位置
在相同函式作用域中,對受 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:
...
Stub 檔案中的變數註解
由於變數註解比型別註解更具可讀性,因此建議在所有版本的 Python(包括 Python 2.7)的 Stub 檔案中使用。請注意,Stub 檔案不會被 Python 解譯器執行,因此使用變數註解不會導致錯誤。型別檢查器應支援所有版本 Python 的 Stub 中的變數註解。例如:
# file lib.pyi
ADDRESS: unicode = ...
class Error:
cause: Union[str, unicode]
變數註解的建議編碼風格
模組層級變數、類別與實例變數以及區域變數的註解應在冒號後有一個空格。冒號前不應有空格。如果賦值有右側值,則等號兩側應各有一個空格。範例如下:
- 是
code: int class Point: coords: Tuple[int, int] label: str = '<unknown>'
- 否
code:int # No space after colon code : int # Space before colon class Test: result: int=0 # No spaces around equality sign
對標準函式庫與文件的變更
- 新的共變型別
ClassVar[T_co]已新增至typing模組。它僅接受單一參數(必須是有效的型別),並用於註解不應在類別實例上設定的類別變數。此限制由靜態檢查器確保,而非在執行時期。請參閱 classvar 章節以獲取ClassVar的使用範例與說明,並參閱 rejected 章節以了解關於ClassVar背後的理由。 typing模組中的get_type_hints函式將會擴充,以便從模組、類別以及函式中擷取型別註解。註解會以字典形式回傳,對映變數或參數至其型別提示,並已對轉發參考(forward references)進行求值。對於類別,它會回傳一個從方法解析順序(MRO)中的註解所建構的對映(可能是collections.ChainMap)。- 關於使用註解的建議指引將被加入文件中,包含對本 PEP 和 PEP 484 中描述規範的教學性回顧。此外,一個用於將型別註解轉換為變數註解的輔助腳本將與標準函式庫分開發布。
型別註解的執行時期影響
註解區域變數會導致解譯器將其視為區域變數,即使它從未被賦值。區域變數的註解不會被求值。
def f():
x: NonexistentName # No error.
然而,如果它是在模組或類別層級,則型別將會被求值。
x: NonexistentName # Error!
class X:
var: NonexistentName # Error!
此外,在模組或類別層級,如果被註解的項目是一個簡單名稱,則它及其註解會作為從名稱到已求值註解的有序對映,儲存在該模組或類別的 __annotations__ 屬性中(若為私有屬性則進行 mangling)。範例如下:
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__ 字典。此外,在本地擁有可用註解的價值,並不足以抵銷每次呼叫函式時建立並填充註解字典的成本。因此,函式層級的註解不會被求值,也不會被儲存。
註解的其他用途
雖然採用本 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,我們明確建議將型別提示作為註解的首選用途。
被拒絕/推遲的提案
- 我們應該引入變數註解嗎?變數註解在 PEP 484 的認可下,以型別註解的形式存在了近兩年。它們已被第三方型別檢查器(mypy, pytype, PyCharm 等)以及使用這些型別檢查器的專案廣泛使用。然而,註解語法在「基本原理」中列出了許多缺點。本 PEP 並非關於是否需要型別註解,而是關於此類註解應具備何種語法。
- 引入新的關鍵字:選擇一個好的關鍵字很困難,例如不能是
var,因為它是太常見的變數名稱;如果我們想將其用於類別變數或全域變數,也不能是local。其次,無論我們選擇什麼,我們仍然需要一個__future__匯入。 - 使用
def作為關鍵字:該提案內容為:def primes: List[int] = [] def captain: str
這個問題在於,
def對幾代 Python 程式設計師(以及工具!)而言意味著「定義一個函式」,而將其也用於定義變數並不會增加清晰度。(當然這是主觀的。) - 使用基於函式的語法:曾有人建議使用
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 似乎在沒有區分類別變數和實例變數的情況下運作良好。但型別檢查器可以使用這些額外資訊做有用的事情,例如標記透過實例對類別變數的意外賦值(這會建立一個隱藏類別變數的實例變數)。它也可以標記具有可變預設值的實例變數,這是一個眾所周知的隱患。 - 使用
ClassAttr取代ClassVar:ClassVar更好的主要原因如下:許多東西都是類別屬性,例如方法、描述器等。但只有特定的屬性在概念上是類別變數(或常數)。 - 不要對註解求值,將它們視為字串:這將與始終會被求值的函式註解行為不一致。雖然未來可能會重新考慮這一點,但在 PEP 484 中已決定這必須是一個單獨的 PEP。
- 在類別文件字串(docstring)中註解變數型別:許多專案已經使用各種文件字串慣例,通常缺乏一致性,且通常尚未符合 PEP 484 的註解語法。此外,這需要特殊的複雜解析器。這反而會違背本 PEP 的目的——與第三方型別檢查工具協作。
- 將
__annotations__實作為描述器:有人提議禁止將__annotations__設定為非字典或非 None 的值。Guido 已拒絕此想法,認為是不必要的;相反地,若嘗試在__annotations__為非對映型別時進行更新,將會引發 TypeError。 - 將裸註解與 global 或 nonlocal 相同對待:被拒絕的提案傾向於在函式體中出現未賦值的註解時,不應涉及任何求值。相反地,本 PEP 暗示如果目標比單一名稱更複雜,其「左側部分」應該在出現在函式體的位置進行求值,以確保它已定義。例如,在此範例中:
def foo(self): slef.name: str
名稱
slef應該被求值,以便如果它未定義(如同本例中可能發生的情況 :-),錯誤將在執行時期被捕捉。這與存在初始值時發生的情況更一致,因此預計會導致較少的意外。(另請注意,如果目標是self.name(這次拼寫正確 :-),最佳化編譯器只要能證明它肯定會被定義,就沒有義務去對self求值。)
回溯相容性
本 PEP 完全向後相容。
實作
Python 3.6 的實作可以在 GitHub 上找到。
版權
此文件已歸入公有領域 (public domain)。
來源:https://github.com/python/peps/blob/main/peps/pep-0526.rst