Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 3106 – Überarbeitung von dict.keys(), .values() und .items()

Autor:
Guido van Rossum
Status:
Final
Typ:
Standards Track
Erstellt:
19-Dez-2006
Python-Version:
3.0
Post-History:


Inhaltsverzeichnis

Zusammenfassung

Dieser PEP schlägt vor, die Methoden .keys(), .values() und .items() des integrierten dict-Typs zu ändern, um ein mengenähnliches oder ungeordnetes Containerobjekt zurückzugeben, dessen Inhalt vom zugrunde liegenden Dictionary abgeleitet ist, anstatt einer Liste, die eine Kopie der Schlüssel usw. ist; und die Methoden .iterkeys(), .itervalues() und .iteritems() zu entfernen.

Der Ansatz ist inspiriert von dem im Java Collections Framework gewählten [1].

Einleitung

Es war schon lange geplant, die Methoden .keys(), .values() und .items() des integrierten dict-Typs so zu ändern, dass sie ein leichteres Objekt als eine Liste zurückgeben und .iterkeys(), .itervalues() und .iteritems() loszuwerden. Die Idee ist, dass Code, der derzeit (in 2.x) liest

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

umgeschrieben werden sollte als

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

(und ähnlich für .itervalues() und .iterkeys(), außer dass letzteres redundant ist, da wir diese Schleife als for k in d schreiben können).

Code, der derzeit liest

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

(usw.) umgeschrieben werden sollte als

a = list(d.keys())

Es gibt (mindestens) zwei Möglichkeiten, dies zu erreichen. Der ursprüngliche Plan war, .keys(), .values() und .items() einfach einen Iterator zurückgeben zu lassen, d. h. genau das, was iterkeys(), itervalues() und iteritems() in Python 2.x zurückgeben. Das Java Collections Framework [1] schlägt jedoch vor, dass eine bessere Lösung möglich ist: Die Methoden geben Objekte mit Mengenverhalten (für .keys() und .items()) oder Multimengenverhalten (== Bag) (für .values()) zurück, die keine Kopien der Schlüssel, Werte oder Elemente enthalten, sondern sich auf das zugrunde liegende Dict beziehen und ihre Werte bei Bedarf aus dem Dict abrufen.

Der Vorteil dieses Ansatzes ist, dass man immer noch Code wie diesen schreiben kann

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

Effektiv wird iter(d.keys()) (usw.) in Python 3.0 das tun, was d.iterkeys() (usw.) in Python 2.x tut; aber in den meisten Kontexten müssen wir den iter()-Aufruf nicht schreiben, da er in einer for-Schleife impliziert ist.

Die von den Methoden .keys() und .items() zurückgegebenen Objekte verhalten sich wie Mengen. Das von der Methode values() zurückgegebene Objekt verhält sich wie eine viel einfachere ungeordnete Sammlung – es kann keine Menge sein, da doppelte Werte möglich sind.

Aufgrund des Mengenverhaltens wird es möglich sein zu prüfen, ob zwei Dictionaries die gleichen Schlüssel haben, indem man einfach testet

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

und ähnlich für .items().

Diese Operationen sind nur in dem Umfang threadsicher, dass ihre Verwendung auf eine nicht threadsichere Weise zu einer Ausnahme führen kann, aber nicht zu einer Beschädigung der internen Darstellung.

Wie in Python 2.x hat das Ändern eines Dictionaries während der Iteration darüber mit einem Iterator eine undefinierte Auswirkung und führt in den meisten Fällen zu einer RuntimeError-Ausnahme. (Dies ist ähnlich den Garantien des Java Collections Framework.)

Die von .keys() und .items() zurückgegebenen Objekte sind vollständig interoperabel mit Instanzen der integrierten Mengen- und frozenset-Typen; zum Beispiel

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

ist garantiert True (außer wenn d gleichzeitig von einem anderen Thread modifiziert wird).

Spezifikation

Ich verwende Pseudocode, um die Semantik zu spezifizieren

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

Anmerkungen

Die Ansichtsobjekte sind nicht direkt veränderbar, implementieren aber kein __hash__(); ihr Wert kann sich ändern, wenn das zugrunde liegende Dictionary geändert wird.

Die einzigen Anforderungen an das zugrunde liegende Dictionary sind, dass es __getitem__(), __contains__(), __iter__() und __len__() implementiert.

Wir implementieren .copy() nicht – die Existenz einer .copy()-Methode deutet darauf hin, dass die Kopie denselben Typ wie das Original hat, aber das ist ohne das Kopieren des zugrunde liegenden Dictionaries nicht machbar. Wenn Sie eine Kopie eines bestimmten Typs, wie list oder set, wünschen, können Sie einfach eine der obigen an den list()- oder set()-Konstruktor übergeben.

Die Spezifikation impliziert, dass die Reihenfolge, in der Elemente von .keys(), .values() und .items() zurückgegeben werden, dieselbe ist (genau wie in Python 2.x), da die Reihenfolge vom Dict-Iterator abgeleitet ist (der vermutlich willkürlich, aber stabil ist, solange ein Dict nicht modifiziert wird). Dies kann durch die folgende Invariante ausgedrückt werden

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

Offene Fragen

Brauchen wir mehr Motivation? Ich denke, die Möglichkeit, Mengenoperationen auf Schlüsseln und Elementen durchzuführen, ohne sie kopieren zu müssen, sollte für sich sprechen.

Ich habe die Implementierung verschiedener Mengenoperationen weggelassen. Diese könnten immer noch kleine Überraschungen bereiten.

Es wäre in Ordnung, wenn mehrere Aufrufe von d.keys() (usw.) dasselbe Objekt zurückgeben würden, da der einzige Zustand des Objekts das Dictionary ist, auf das es sich bezieht. Lohnt sich das zusätzliche Felder im Dictionary-Objekt? Sollte das eine schwache Referenz sein oder sollte das d_keys (usw.) Objekt für immer leben, sobald es erstellt wurde? Strohmann: Wahrscheinlich nicht wert die zusätzlichen Felder in jedem Dict.

Sollten d_keys, d_values und d_items eine öffentliche Instanzvariable oder Methode haben, über die man das zugrunde liegende Dictionary abrufen kann? Strohmann: Ja (aber wie sollte sie heißen?).

Ich hole bessere Namen als d_keys, d_values und d_items ein. Diese Klassen könnten öffentlich sein, damit ihre Implementierungen von den Methoden .keys(), .values() und .items() anderer Mappings wiederverwendet werden können. Oder sollten sie?

Sollten die Klassen d_keys, d_values und d_items wiederverwendbar sein? Strohmann: Ja.

Sollten sie unterklassbar sein? Strohmann: Ja (aber siehe unten).

Ein besonders kniffliges Problem ist, ob Operationen, die in Bezug auf andere Operationen spezifiziert sind (z. B. .discard()), wirklich in Bezug auf diese anderen Operationen implementiert werden müssen; dies mag irrelevant erscheinen, wird aber relevant, wenn diese Klassen jemals unterklassifiziert werden. Historisch gesehen hat Python eine sehr schlechte Erfolgsbilanz bei der klaren Spezifizierung der Semantik hoch optimierter integrierter Typen in solchen Fällen; mein Strohmann ist, diesen Trend fortzusetzen. Die Unterklassifizierung kann immer noch nützlich sein, um zum Beispiel neue Methoden *hinzuzufügen*.

Ich überlasse die Entscheidungen (insbesondere zur Namensgebung) demjenigen, der eine funktionierende Implementierung einreicht.

Referenzen


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

Zuletzt geändert: 2025-02-01 08:59:27 GMT