Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案

PEP 3106 – 重新設計 dict.keys()、.values() 和 .items()

作者:
Guido van Rossum
狀態:
最終版
型別:
標準跟蹤
建立日期:
2006年12月19日
Python 版本:
3.0
釋出歷史:


目錄

摘要

本PEP提議更改內建 dict 型別的 .keys()、.values() 和 .items() 方法,使其返回一個類似集合或無序的容器物件,其內容從底層字典派生,而不是返回一個作為鍵等副本的列表;並刪除 .iterkeys()、.iterval() 和 .iteritems() 方法。

這種方法靈感來源於 Java 集合框架 [1] 中採用的方法。

引言

長期以來,計劃是更改內建 dict 型別的 .keys()、.values() 和 .items() 方法,使其返回比列表更輕量的物件,並廢棄 .iterkeys()、.itervalues() 和 .iteritems()。其思想是,當前(在 2.x 中)的程式碼:

for k, v in d.iteritems(): ...

應該重寫為:

for k, v in d.items(): ...

(對於 .itervalues() 和 .iterkeys() 類似,但後者是多餘的,因為我們可以將該迴圈寫成 for k in d。)

當前程式碼:

a = d.keys()    # assume we really want a list here

(等等)應該重寫為:

a = list(d.keys())

實現這一點至少有兩種方法。最初的計劃是簡單地讓 .keys()、.values() 和 .items() 返回一個迭代器,即在 Python 2.x 中 iterkeys()、itervalues() 和 iteritems() 返回的精確值。然而,Java 集合框架 [1] 建議有一種更好的解決方案:這些方法返回具有集合行為(對於 .keys() 和 .items())或多重集合(== 包)行為(對於 .values())的物件,這些物件不包含鍵、值或項的副本,而是引用底層字典並根據需要從字典中提取它們的值。

這種方法的優點是仍然可以這樣編寫程式碼:

a = d.items()
for k, v in a: ...
# And later, again:
for k, v in a: ...

實際上,Python 3.0 中的 iter(d.keys())(等等)將執行 Python 2.x 中 d.iterkeys()(等等)的操作;但在大多數情況下,我們不必編寫 iter() 呼叫,因為它隱含在 for 迴圈中。

.keys() 和 .items() 方法返回的物件行為類似於集合。values() 方法返回的物件行為類似於一個更簡單的無序集合——它不能是一個集合,因為可能存在重複的值。

由於集合行為,可以透過簡單地測試來檢查兩個字典是否具有相同的鍵:

if a.keys() == b.keys(): ...

對於 .items() 也是如此。

這些操作的執行緒安全性僅限於以非執行緒安全方式使用它們可能會導致異常,但不會導致內部表示的損壞。

與 Python 2.x 中一樣,在使用迭代器遍歷字典時修改字典具有未定義的效果,並且在大多數情況下會引發 RuntimeError 異常。(這類似於 Java 集合框架所做的保證。)

.keys() 和 .items() 返回的物件與內建 set 和 frozenset 型別的例項完全可互操作;例如:

set(d.keys()) == d.keys()

保證為 True(除非 d 同時被另一個執行緒修改)。

規範

我正在使用虛擬碼來指定語義:

class dict:

    # Omitting all other dict methods for brevity.
    # The .iterkeys(), .itervalues() and .iteritems() methods
    # will be removed.

    def keys(self):
        return d_keys(self)

    def items(self):
        return d_items(self)

    def values(self):
        return d_values(self)

class d_keys:

    def __init__(self, d):
        self.__d = d

    def __len__(self):
        return len(self.__d)

    def __contains__(self, key):
        return key in self.__d

    def __iter__(self):
        for key in self.__d:
            yield key

    # The following operations should be implemented to be
    # compatible with sets; this can be done by exploiting
    # the above primitive operations:
    #
    #   <, <=, ==, !=, >=, > (returning a bool)
    #   &, |, ^, - (returning a new, real set object)
    #
    # as well as their method counterparts (.union(), etc.).
    #
    # To specify the semantics, we can specify x == y as:
    #
    #   set(x) == set(y)   if both x and y are d_keys instances
    #   set(x) == y        if x is a d_keys instance
    #   x == set(y)        if y is a d_keys instance
    #
    # and so on for all other operations.

class d_items:

    def __init__(self, d):
        self.__d = d

    def __len__(self):
        return len(self.__d)

    def __contains__(self, (key, value)):
        return key in self.__d and self.__d[key] == value

    def __iter__(self):
        for key in self.__d:
            yield key, self.__d[key]

    # As well as the set operations mentioned for d_keys above.
    # However the specifications suggested there will not work if
    # the values aren't hashable.  Fortunately, the operations can
    # still be implemented efficiently.  For example, this is how
    # intersection can be specified:

    def __and__(self, other):
        if isinstance(other, (set, frozenset, d_keys)):
            result = set()
            for item in other:
                if item in self:
                    result.add(item)
            return result
        if not isinstance(other, d_items):
            return NotImplemented
        d = {}
        if len(other) < len(self):
            self, other = other, self
        for item in self:
            if item in other:
                key, value = item
                d[key] = value
        return d.items()

    # And here is equality:

    def __eq__(self, other):
        if isinstance(other, (set, frozenset, d_keys)):
            if len(self) != len(other):
                return False
            for item in other:
                if item not in self:
                    return False
            return True
        if not isinstance(other, d_items):
            return NotImplemented
        # XXX We could also just compare the underlying dicts...
        if len(self) != len(other):
            return False
        for item in self:
            if item not in other:
                return False
        return True

    def __ne__(self, other):
        # XXX Perhaps object.__ne__() should be defined this way.
        result = self.__eq__(other)
        if result is not NotImplemented:
            result = not result
        return result

class d_values:

    def __init__(self, d):
        self.__d = d

    def __len__(self):
        return len(self.__d)

    def __contains__(self, value):
        # This is slow, and it's what "x in y" uses as a fallback
        # if __contains__ is not defined; but I'd rather make it
        # explicit that it is supported.
        for v in self:
             if v == value:
                 return True
        return False

    def __iter__(self):
        for key in self.__d:
            yield self.__d[key]

    def __eq__(self, other):
        if not isinstance(other, d_values):
            return NotImplemented
        if len(self) != len(other):
            return False
        # XXX Sometimes this could be optimized, but these are the
        # semantics: we can't depend on the values to be hashable
        # or comparable.
        olist = list(other)
        for x in self:
            try:
                olist.remove(x)
            except ValueError:
                return False
        assert olist == []
        return True

    def __ne__(self, other):
        result = self.__eq__(other)
        if result is not NotImplemented:
            result = not result
        return result

備註

檢視物件不能直接修改,但它們不實現 __hash__();如果底層字典被修改,它們的值可能會改變。

對底層字典的唯一要求是它實現 __getitem__()、__contains__()、__iter__() 和 __len__()。

我們不實現 .copy() – .copy() 方法的存在表明副本與原始物件具有相同的型別,但如果沒有複製底層字典,這是不可行的。如果您想要特定型別(如 list 或 set)的副本,您可以將上述物件之一傳遞給 list() 或 set() 建構函式。

該規範意味著 .keys()、.values() 和 .items() 返回項的順序是相同的(就像在 Python 2.x 中一樣),因為該順序都源自字典迭代器(該迭代器可能具有任意性,但在字典未修改的情況下是穩定的)。這可以用以下不變式表示:

list(d.items()) == list(zip(d.keys(), d.values()))

未解決的問題

我們是否需要更多動力?我認為能夠在不復制鍵和項的情況下對它們進行集合操作本身就足夠說明問題了。

我省略了各種集合操作的實現。這些仍然可能帶來小驚喜。

如果多次呼叫 d.keys()(等等)返回相同的物件,那也沒關係,因為物件的唯一狀態是它引用的字典。這是否值得在字典物件中增加額外的槽位?這應該是弱引用還是 d_keys(等等)物件一旦建立就永遠存在?草案:可能不值得在每個字典中增加額外的槽位。

d_keys、d_values 和 d_items 是否應該有一個公共例項變數或方法,透過它我們可以檢索底層字典?草案:是的(但應該叫什麼?)。

我正在徵集比 d_keys、d_values 和 d_items 更好的名稱。這些類可以是公共的,以便它們的實現可以被其他對映的 .keys()、.values() 和 .items() 方法重用。或者應該嗎?

d_keys、d_values 和 d_items 類是否應該可重用?草案:是的。

它們是否應該可子類化?草案:是的(但請參見下文)。

一個特別棘手的問題是,以其他操作(例如 .discard())定義的那些操作是否必須真的以這些其他操作來實現;這可能看起來無關緊要,但如果這些類被子類化,它就會變得相關。從歷史上看,Python 在這種情況下清晰地指定高度最佳化內建型別的語義方面記錄不佳;我的草案是繼續這種趨勢。子類化可能仍然有助於*新增*新方法,例如。

我將把決定權(特別是關於命名)留給提交工作實現的人。

參考資料


來源: https://github.com/python/peps/blob/main/peps/pep-3106.rst

最後修改時間: 2025-02-01 08:59:27 GMT