PEP 3150 – 語句區域性名稱空間(又稱“given”子句)
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 狀態:
- 推遲
- 型別:
- 標準跟蹤
- 建立日期:
- 2010年7月9日
- Python 版本:
- 3.4
- 釋出歷史:
- 2010年7月14日,2011年4月21日,2011年6月13日
摘要
本PEP提議為Python中目前沒有相關程式碼套件的幾個語句新增一個可選的given子句。該子句將為附加名稱建立一個語句區域性名稱空間,這些名稱可在相關語句中訪問,但不會成為包含名稱空間的一部分。
提議採用一個新的符號?來表示對透過執行相關程式碼套件建立的名稱空間的前向引用。它將是對types.SimpleNamespace物件的引用。
主要動機是啟用更具宣告性的程式設計風格,即首先向讀者呈現要執行的操作,然後詳細介紹必要的子計算。作為一個關鍵示例,這將使普通賦值語句與class和def語句處於同等地位,在這些語句中,要定義項的名稱在計算該項值之前就呈現給讀者。它還允許以“多行lambda”方式使用命名函式,其中名稱僅用作當前表示式中的佔位符,然後在隨後的套件中定義。
次要動機是在模組和類級別程式碼中簡化中間計算,而不會汙染最終的名稱空間。
目的是,給定子句與執行指定操作的單獨函式定義之間的關係,將類似於顯式while迴圈與生成相同操作序列的生成器之間的現有關係。
本PEP中的具體提議受到了多年來對該概念及相關概念的各種探索(例如[1]、[2]、[3]、[6]、[8])的影響,並在一定程度上受到Haskell中where和let子句的啟發。它避免了過去提案中發現的一些問題,但其本身尚未經過實現的檢驗。
提案
本PEP提議在簡單語句的語法中新增一個可選的given子句,這些簡單語句可能包含表示式,或可能出於純粹的語法目的替代此類語句。受此新增影響的簡單語句當前列表如下:
- 表示式語句
- 賦值語句
- 增廣賦值語句
- del 語句
- return 語句
- yield 語句
- raise 語句
- assert 語句
- pass 語句
given子句將允許在標題行中按名稱引用子表示式,實際定義則在縮排子句中。例如:
sorted_data = sorted(data, key=?.sort_key) given:
def sort_key(item):
return item.attr1, item.attr2
新符號?用於引用給定名稱空間。它將是types.SimpleNamespace例項,因此?.sort_key作為對given子句中定義的名稱的前向引用。
given子句中允許使用文件字串,並將其作為__doc__屬性附加到結果名稱空間。
包含pass語句是為了提供一種一致的方式來跳過在標題行中包含有意義的表示式。雖然這不是預期的用例,但它也不能被阻止,因為即使禁止pass本身,也仍然可以使用多種替代方案(例如...和())。
given子句的主體將在新作用域中執行,使用正常的函式閉包語義。為了支援迴圈變數和全域性引用的早期繫結,以及允許訪問在類作用域中定義的其他名稱,given子句還將允許在標題行中進行顯式繫結操作
# Explicit early binding via given clause
seq = []
for i in range(10):
seq.append(?.f) given i=i in:
def f():
return i
assert [f() for f in seq] == list(range(10))
語義
以下語句
op(?.f, ?.g) given bound_a=a, bound_b=b in:
def f():
return bound_a + bound_b
def g():
return bound_a - bound_b
大致等同於以下程式碼(__var表示隱藏的編譯器變數或僅僅是直譯器堆疊上的一個條目)
__arg1 = a
__arg2 = b
def __scope(bound_a, bound_b):
def f():
return bound_a + bound_b
def g():
return bound_a - bound_b
return types.SimpleNamespace(**locals())
__ref = __scope(__arg1, __arg2)
__ref.__doc__ = __scope.__doc__
op(__ref.f, __ref.g)
given子句本質上是一個巢狀函式,它被建立然後立即執行。除非顯式傳入,否則名稱將使用正常的範圍規則進行查詢,因此在類範圍中定義的名稱將不可見。宣告為前向引用的名稱將被返回並在標題語句中使用,而不會在周圍的名稱空間中區域性繫結。
語法變更
當前
expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) |
('=' (yield_expr|testlist_star_expr))*)
del_stmt: 'del' exprlist
pass_stmt: 'pass'
return_stmt: 'return' [testlist]
yield_stmt: yield_expr
raise_stmt: 'raise' [test ['from' test]]
assert_stmt: 'assert' test [',' test]
新
expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) |
('=' (yield_expr|testlist_star_expr))*) [given_clause]
del_stmt: 'del' exprlist [given_clause]
pass_stmt: 'pass' [given_clause]
return_stmt: 'return' [testlist] [given_clause]
yield_stmt: yield_expr [given_clause]
raise_stmt: 'raise' [test ['from' test]] [given_clause]
assert_stmt: 'assert' test [',' test] [given_clause]
given_clause: "given" [(NAME '=' test)+ "in"]":" suite
(請注意,語法中的expr_stmt有點用詞不當,因為它除了簡單的表示式語句之外還涵蓋了賦值和增廣賦值)
注意
這些提議的語法更改尚未涵蓋用於訪問語句區域性名稱空間中定義的名稱的前向引用表示式語法。
新子句作為現有語句的可選元素新增,而不是作為一種新的複合語句,以避免在語法中產生歧義。它僅應用於列出的特定元素,因此不允許以下無意義的用法:
break given:
a = b = 1
import sys given:
a = b = 1
然而,上面描述的精確語法更改是不充分的,因為它為 simple_stmt 的定義帶來了問題(它允許用“;”而不是“\n”連結多個單行語句)。
因此,上述語法更改應被視為意圖宣告。任何實際提案都需要在認真考慮之前解決 simple_stmt 解析問題。這可能需要對語法進行非平凡的重組,分解 small_stmt 和 flow_stmt 以分離可能包含任意子表示式的語句,然後允許在 simple_stmt 級別使用其中一個帶 given 子句的語句。類似於以下內容:
stmt: simple_stmt | given_stmt | compound_stmt
simple_stmt: small_stmt (';' (small_stmt | subexpr_stmt))* [';'] NEWLINE
small_stmt: (pass_stmt | flow_stmt | import_stmt |
global_stmt | nonlocal_stmt)
flow_stmt: break_stmt | continue_stmt
given_stmt: subexpr_stmt (given_clause |
(';' (small_stmt | subexpr_stmt))* [';']) NEWLINE
subexpr_stmt: expr_stmt | del_stmt | flow_subexpr_stmt | assert_stmt
flow_subexpr_stmt: return_stmt | raise_stmt | yield_stmt
given_clause: "given" (NAME '=' test)* ":" suite
作為參考,以下是該級別當前的定義:
stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
除了上述更改之外,atom 的定義也將更改為允許 ?。此用法僅限於具有相關 given 子句的語句,這將由編譯過程的後期階段(可能是 AST 構造,它已經強制執行其他限制,因為語法為了簡化初始解析步驟而過於寬容)處理。
新的PEP 8指南
正如在 python-ideas ([7], [9]) 上討論的,還需要制定新的 PEP 8 指南,以提供關於何時使用 given 子句而非普通變數賦值的適當指導。
基於已經存在於try語句的類似指南,本PEP提議在PEP 8的“程式設計約定”部分中為given語句新增以下內容:
- 對於可以合理地分解為單獨函式,但目前未在任何地方重用的程式碼,請考慮使用
given子句。這清楚地表明瞭哪些變數僅用於定義另一個語句的子元件,而不是用於儲存演算法或應用程式狀態。當將多行函式傳遞給接受可呼叫引數的操作時,這是一種特別有用的技術。 - 保持
given子句簡潔。如果它們變得笨拙,要麼將其分解為多個步驟,要麼將細節移到單獨的函式中。
基本原理
Python中的函式和類語句相對於普通賦值語句具有獨特的屬性:在某種程度上,它們是宣告性的。它們在繼續詳細說明函式或類主體中的實際定義之前,向程式碼讀者提供關於即將定義的名稱的一些關鍵資訊。
被宣告物件的名稱是關鍵字之後首先說明的內容。其他重要資訊也享有優先於實現細節的殊榮
- 裝飾器(可以極大地影響建立物件的行為,並且出於實用性而非美學原因,甚至放在關鍵字和名稱之前)
- 文件字串(緊跟在標題行之後的第一行)
- 函式定義的引數、預設值和註解
- 類定義的父類、元類和可選的其他詳細資訊(取決於元類)
本PEP提議透過允許在任何簡單賦值語句之後包含“given”套件,為任意賦值操作提供類似的宣告式風格
TARGET = [TARGET2 = ... TARGETN =] EXPR given:
SUITE
按照慣例,套件主體中的程式碼應僅以正確定義標題行中執行的賦值操作為導向。標題行操作也應充分描述(例如,透過適當選擇變數名),以便讀者在不閱讀套件主體的情況下也能合理地瞭解操作的目的。
然而,儘管它們是最初的動機用例,但將此功能僅僅限制為簡單賦值會過於嚴格。一旦該功能被定義,阻止將其用於增廣賦值、返回語句、yield表示式、推導式和可能修改應用程式狀態的任意表達式將是相當武斷的。
given子句還可以作為對某些lambda表示式和類似構造的更具可讀性的替代,當將一次性函式傳遞給諸如sorted()之類的操作或在基於回撥的事件驅動程式設計中使用時。
在模組和類級別程式碼中,given子句將作為del語句使用的清晰可靠的替代品,以防止臨時工作變數汙染結果名稱空間。
一種可能有助於理解所提議子句的方式是將其視為傳統內聯程式碼與將操作分離到專用函式之間的中間地帶,就像內聯while迴圈最終可能被分解為專用生成器一樣。
設計討論
關鍵詞選擇
此提案最初使用 where,基於 Haskell 中類似構造的名稱。然而,有人指出,現有 Python 庫(例如 Numpy [4])已在 SQL 查詢條件意義上使用 where,這使得該關鍵字的選擇可能引起混淆。
雖然 given 也可以用作變數名(因此將透過引入新關鍵字的常規 __future__ 方式棄用),但它與新子句所需的“這裡有一些該表示式可能使用的額外變數”語義更強烈地關聯。
重用with關鍵字也曾被提議。這具有避免新增新關鍵字的優點,但也可能造成高度混淆,因為with子句和with語句看起來相似但功能完全不同。那條路通向C++和Perl :)
與PEP 403的關係
PEP 403(通用裝飾器子句)試圖透過受現有裝飾器語法啟發的、不那麼激進的語言更改來實現本PEP的主要目標。
儘管作者相同,但這兩個PEP彼此直接競爭。PEP 403代表了一種極簡主義方法,試圖以最小的現狀變化實現有用的功能。而本PEP則旨在實現更靈活的獨立語句設計,這需要對語言進行更大程度的更改。
請注意,儘管 PEP 403 更適合正確解釋生成器表示式的行為,但本 PEP 的當前版本能夠更好地解釋裝飾器子句的整體行為。這兩個 PEP 都支援對容器推導式語義的充分解釋。
解釋容器推導式和生成器表示式
所提議構造的一個有趣特性是,它可以作為解釋容器推導式作用域和執行順序語義的原始方法。
seq2 = [x for x in y if q(x) for y in seq if p(y)]
# would be equivalent to
seq2 = ?.result given seq=seq:
result = []
for y in seq:
if p(y):
for x in y:
if q(x):
result.append(x)
此擴充套件中的重點在於,它解釋了推導式在類作用域中為何表現異常:只有最外層迭代器在類作用域中求值,而所有謂詞、巢狀迭代器和值表示式都在巢狀作用域中求值。
請注意,與PEP 403不同,本PEP的當前版本無法為生成器表示式提供精確等效的擴充套件。它能達到的最接近程度是定義一個額外的作用域級別。
seq2 = ?.g(seq) given:
def g(seq):
for y in seq:
if p(y):
for x in y:
if q(x):
yield x
如果允許 given 子句成為生成器函式,則可以彌補這一限制,在這種情況下,問號將指代生成器-迭代器物件而不是簡單的名稱空間。
seq2 = ? given seq=seq in:
for y in seq:
if p(y):
for x in y:
if q(x):
yield x
然而,這將使“?”的含義變得相當模糊,甚至比def語句的含義(通常會有一個文件字串指示函式定義是否實際上是一個生成器)更模糊。
解釋裝飾器子句的求值和應用
裝飾器子句求值和應用的標準解釋必須處理隱藏編譯器變數的概念,才能按執行順序顯示步驟。given語句允許像下面這樣的裝飾函式定義:
@classmethod
def classname(cls):
return cls.__name__
可以大致解釋為等同於:
classname = .d1(classname) given:
d1 = classmethod
def classname(cls):
return cls.__name__
預期異議
兩種做法
現在很多程式碼可能會以兩種方式編寫:在表示式使用值之前定義值,或在given子句中定義值,這造成了兩種做法,可能沒有明確的選擇方式。
經過反思,我覺得這是對“一種顯而易見的方式”這句格言的誤用。Python已經提供了許多編寫程式碼的方式。我們可以使用for迴圈或while迴圈,函式式風格、命令式風格或面向物件風格。該語言通常旨在讓人們編寫符合他們思維方式的程式碼。由於不同的人有不同的思維方式,他們編寫程式碼的方式也會相應地改變。
程式碼庫中的此類風格問題理所當然地留給負責該程式碼的開發組。什麼時候表示式變得如此複雜,以至於子表示式應該被取出並賦值給變數,即使這些變數只會被使用一次?什麼時候應該用實現相同邏輯的生成器替換內聯 while 迴圈?觀點各不相同,這沒關係。
然而,CPython 和標準庫將需要明確的 PEP 8 指導,這在上面的提案中進行了討論。
亂序執行
given 子句使執行跳轉得有點奇怪,因為 given 子句的主體在子句頭部的簡單語句之前執行。Python 其他部分中最接近這一點的是列表推導式、生成器表示式和條件表示式中的亂序求值,以及裝飾器函式對其所裝飾函式的延遲應用(裝飾器表示式本身按其書寫順序執行)。
雖然這是事實,但這種語法旨在用於人們自己對問題進行亂序思考的情況(至少就語言而言)。舉個例子,考慮一個Python使用者腦海中的以下想法:
我想根據序列中每個項的attr1和attr2的值對這些項進行排序。
如果他們對Python的lambda表示式很熟悉,他們可能會選擇這樣寫:
sorted_list = sorted(original, key=(lambda v: v.attr1, v.attr2))
這確實完成了任務,但它遠未達到符合Python聲譽的可執行虛擬碼標準。
如果他們不喜歡 lambda 特別是,operator 模組提供了一種替代方案,仍然允許內聯定義關鍵函式
sorted_list = sorted(original,
key=operator.attrgetter(v. 'attr1', 'attr2'))
同樣,它完成了任務,但即使是最寬容的讀者也不會認為那是“可執行虛擬碼”。
如果他們認為上述兩種選項都醜陋且令人困惑,或者他們的關鍵函式需要無法表示為表示式的邏輯(例如捕獲異常),那麼 Python 目前會強迫他們顛倒他們最初的想法的順序,首先定義排序條件
def sort_key(item):
return item.attr1, item.attr2
sorted_list = sorted(original, key=sort_key)
“只需定義一個函式”多年來一直是針對多行 lambda 支援請求的機械式回應。與上述選項一樣,它完成了任務,但它確實代表了使用者思考方式與語言允許他們表達方式之間的斷裂。
我相信本PEP中的提議最終將使Python接近上述思想型別的“可執行虛擬碼”標準
sorted_list = sorted(original, key=?.key) given:
def key(item):
return item.attr1, item.attr2
一切都與使用者最初的想法保持相同的順序,他們甚至不需要為排序標準想出名稱:可以直接重用關鍵字引數名稱。
對此提案的可能增強是提供一種便捷的簡寫語法,表示“將給定子句內容用作關鍵字引數”。即使沒有專用語法,也可以簡單地寫成**vars(?)。
有害於自省
在模組和類內部進行探測是白盒測試和互動式除錯的寶貴工具。given子句將非常有效地阻止訪問計算過程中使用的臨時狀態(儘管在這方面,它不會比當前del語句的使用更有效)。
雖然這是一個合理的問題,但可測試性設計是一個涉及程式設計許多方面的問題。如果一個元件需要獨立測試,那麼given語句應該重構為單獨的語句,以便資訊暴露給測試套件。這與將隱藏在函式或生成器內部的操作重構到其自己的函式中,僅僅為了允許其獨立測試,並沒有顯著區別。
缺乏實際影響評估
當前PEP中的例子幾乎都是相對較小的“玩具”例子。本PEP中的提案需要透過應用於大型程式碼庫(例如標準庫或大型Twisted應用程式)的測試,以尋找實際程式碼的可讀性真正得到增強的例子。
然而,這更多是PEP的不足,而非想法本身的不足。如果這不是一個實際問題,我們就不會收到這麼多關於缺乏多行lambda支援的抱怨,Ruby的塊構造可能也不會如此受歡迎。
開放問題
前向引用的語法
提議使用?符號作為給定名稱空間的前向引用,因為它簡短、目前未使用且暗示“這裡缺少一些稍後會填補的東西”。
PEP 中的提案與任何現有 Python 特性都不完全並行,因此故意避免重用已使用的符號。
nonlocal 和 global 的處理
nonlocal和global在given子句套件中被明確禁止,如果出現將導致語法錯誤。如果它們出現在該套件內的def語句中,它們將正常工作。
或者,它們可以定義為像上面擴充套件中定義的匿名函式一樣操作。
break 和 continue 的處理
break和continue將按照上述擴充套件中定義的匿名函式那樣操作。如果它們出現在given子句套件中,它們將是語法錯誤,但如果它們作為該套件的一部分出現在for或while迴圈中,它們將正常工作。
return 和 yield 的處理
return和yield在given子句套件中被明確禁止,如果出現將導致語法錯誤。如果它們出現在該套件內的def語句中,它們將正常工作。
示例
為事件驅動程式設計定義回撥
# Current Python (definition before use)
def cb(sock):
# Do something with socket
def eb(exc):
logging.exception(
"Failed connecting to %s:%s", host, port)
loop.create_connection((host, port), cb, eb) given:
# Becomes:
loop.create_connection((host, port), ?.cb, ?.eb) given:
def cb(sock):
# Do something with socket
def eb(exc):
logging.exception(
"Failed connecting to %s:%s", host, port)
定義通常只有單個例項的“一次性”類
# Current Python (instantiation after definition)
class public_name():
... # However many lines
public_name = public_name(*params)
# Current Python (custom decorator)
def singleton(*args, **kwds):
def decorator(cls):
return cls(*args, **kwds)
return decorator
@singleton(*params)
class public_name():
... # However many lines
# Becomes:
public_name = ?.MeaningfulClassName(*params) given:
class MeaningfulClassName():
... # Should trawl the stdlib for an example of doing this
在不汙染區域性名稱空間的情況下計算屬性 (來自 os.py)
# Current Python (manual namespace cleanup)
def _createenviron():
... # 27 line function
environ = _createenviron()
del _createenviron
# Becomes:
environ = ?._createenviron() given:
def _createenviron():
... # 27 line function
替換預設引數技巧(來自 functools.lru_cache)
# Current Python (default argument hack)
def decorating_function(user_function,
tuple=tuple, sorted=sorted, len=len, KeyError=KeyError):
... # 60 line function
return decorating_function
# Becomes:
return ?.decorating_function given:
# Cell variables rather than locals, but should give similar speedup
tuple, sorted, len, KeyError = tuple, sorted, len, KeyError
def decorating_function(user_function):
... # 60 line function
# This example also nicely makes it clear that there is nothing in the
# function after the nested function definition. Due to additional
# nested functions, that isn't entirely clear in the current code.
可能的補充
- 當前提案僅允許為簡單語句新增
given子句。將此想法擴充套件到允許使用複合語句是完全可能的(透過將given子句作為獨立的套件附加在末尾),但這樣做會引發嚴重的可讀性問題(因為在given子句中定義的值可能在定義之前就被使用,這正是裝飾器和with語句等其他功能旨在消除的可讀性陷阱) - “顯式早期繫結”變體可能適用於 python-ideas 上關於如何消除預設引數技巧的討論。函式標題行中的
given子句(在返回型別註解之後)可能是這個問題的答案。
被拒絕的替代方案
- 此PEP的早期版本允許對尾隨套件中的名稱進行隱式前向引用,並且還使用了隱式早期繫結語義。這兩個想法都大大複雜了提案,而沒有提供足夠的表達能力提升。當前明確的前向引用和早期繫結提案使新構造與現有作用域語義保持一致,大大提高了該想法實際實現的可能性。
- 除了這裡提出的建議之外,還有人提出了兩種套件“按序”變體,它們提供有限的名稱作用域,而不支援亂序執行。我相信這些建議很大程度上忽略了人們在請求多行 lambda 支援時抱怨的重點——並不是為子表示式想出名稱特別困難,而是將函式在使用它的語句之前命名意味著程式碼不再符合開發人員思考手頭問題的方式。
- 我做了一些未發表的嘗試,以允許直接引用
given子句隱式建立的閉包,同時仍保留本 PEP 中定義的語法的一般結構(例如,允許在表示式中使用?given或:given等子表示式來表示對隱式閉包的直接引用,從而阻止其自動呼叫以建立區域性名稱空間)。所有這些嘗試與 PEP 403 中更簡單的裝飾器啟發式提案相比,都顯得不具吸引力且令人困惑。
參考實現
目前還沒有。如果你想快速瞭解 Python 名稱空間語義和程式碼編譯,請隨意嘗試 ;)
待辦事項
- 提及 PEP 359 和
given子句中 locals() 的可能用途 - 弄清楚這是否可以在內部使用,以使零引數super()呼叫的實現不那麼糟糕
參考資料
版權
本文件已置於公共領域。
來源:https://github.com/python/peps/blob/main/peps/pep-3150.rst
最後修改時間:2025-02-01 08:59:27 GMT