wake-up-neo.com

Wie kann man ein Diktat "perfekt" überschreiben?

Wie kann ich eine Unterklasse von diktieren so "perfekt" wie möglich machen? Das Endziel ist ein einfaches Diktat , in dem die Tasten in Kleinbuchstaben geschrieben sind.

Es scheint, dass es eine kleine Menge von Primitiven geben sollte, die ich überschreiben kann, um diese Arbeit zu machen, aber nach all meinen Nachforschungen und Versuchen scheint dies nicht der Fall zu sein:

  • Wenn ich überschreibe __getitem__/__setitem__ , dann funktioniert get/set nicht. Wie kann ich sie arbeiten lassen? Sicherlich muss ich sie nicht einzeln implementieren?

  • Verhindere ich, dass das Beizen funktioniert, und muss ich __setstate__ Usw. implementieren?

  • Muss ich brauche repr, update und __init__ ?

  • Soll ich einfach Mutablemapping verwenden (anscheinend sollte man UserDict oder DictMixin nicht verwenden)? Wenn das so ist, wie? Die Dokumente sind nicht gerade aufschlussreich.

Hier ist mein erster Versuch, get() funktioniert nicht und es gibt zweifellos viele andere kleinere Probleme:

class arbitrary_dict(dict):
    """A dictionary that applies an arbitrary key-altering function
       before accessing the keys."""

    def __keytransform__(self, key):
        return key

    # Overridden methods. List from 
    # https://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict

    def __init__(self, *args, **kwargs):
        self.update(*args, **kwargs)

    # Note: I'm using dict directly, since super(dict, self) doesn't work.
    # I'm not sure why, perhaps dict is not a new-style class.

    def __getitem__(self, key):
        return dict.__getitem__(self, self.__keytransform__(key))

    def __setitem__(self, key, value):
        return dict.__setitem__(self, self.__keytransform__(key), value)

    def __delitem__(self, key):
        return dict.__delitem__(self, self.__keytransform__(key))

    def __contains__(self, key):
        return dict.__contains__(self, self.__keytransform__(key))


class lcdict(arbitrary_dict):
    def __keytransform__(self, key):
        return str(key).lower()
194
Paul Biggar

Mit ABC s (Abstrakte Basisklassen) aus dem Modul Sammlungen können Sie ein Objekt schreiben, das sich wie ein Diktat verhält. Es gibt sogar Auskunft darüber, ob Sie eine Methode verpasst haben. Nachfolgend finden Sie die Minimalversion, mit der das ABC beendet wird.

import collections


class TransformedDict(collections.MutableMapping):
    """A dictionary that applies an arbitrary key-altering
       function before accessing the keys"""

    def __init__(self, *args, **kwargs):
        self.store = dict()
        self.update(dict(*args, **kwargs))  # use the free update to set keys

    def __getitem__(self, key):
        return self.store[self.__keytransform__(key)]

    def __setitem__(self, key, value):
        self.store[self.__keytransform__(key)] = value

    def __delitem__(self, key):
        del self.store[self.__keytransform__(key)]

    def __iter__(self):
        return iter(self.store)

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

    def __keytransform__(self, key):
        return key

Sie erhalten ein paar kostenlose Methoden aus dem ABC:

class MyTransformedDict(TransformedDict):

    def __keytransform__(self, key):
        return key.lower()


s = MyTransformedDict([('Test', 'test')])

assert s.get('TEST') is s['test']   # free get
assert 'TeSt' in s                  # free __contains__
                                    # free setdefault, __eq__, and so on

import pickle
assert pickle.loads(pickle.dumps(s)) == s
                                    # works too since we just use a normal dict

Ich würde dict (oder andere eingebaute Objekte) nicht direkt in Unterklassen unterteilen. Es macht oft keinen Sinn, weil Sie eigentlich die Schnittstelle eines Diktats implementieren wollen. Und genau dafür sind ABCs da.

201
Jochen Ritzel

Wie kann ich eine Unterklasse von Diktaten so "perfekt" wie möglich machen?

Das Endziel ist ein einfaches Diktat, in dem die Tasten in Kleinbuchstaben geschrieben sind.

  • Wenn ich __getitem__/__setitem__ Überschreibe, funktioniert get/set nicht. Wie bringe ich sie zum Arbeiten? Sicherlich muss ich sie nicht einzeln implementieren?

  • Verhindere ich, dass das Beizen funktioniert, und muss ich __setstate__ Usw. implementieren?

  • Benötige ich repr, update und __init__?

  • Soll ich nur mutablemapping verwenden (anscheinend sollte man UserDict oder DictMixin nicht verwenden)? Wenn das so ist, wie? Die Dokumente sind nicht gerade aufschlussreich.

Die akzeptierte Antwort wäre mein erster Ansatz, aber da es einige Probleme gibt und niemand die Alternative angesprochen hat und tatsächlich ein dict untergeordnet wird, werde ich das hier tun.

Was ist mit der akzeptierten Antwort falsch?

Dies scheint mir eine ziemlich einfache Bitte zu sein:

Wie kann ich eine Unterklasse von Diktaten so "perfekt" wie möglich machen? Das Endziel ist ein einfaches Diktat, in dem die Tasten in Kleinbuchstaben geschrieben sind.

Die akzeptierte Antwort ist nicht wirklich Unterklasse dict, und ein Test hierfür schlägt fehl:

>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False

Im Idealfall wird jeder Code zur Typprüfung auf die von uns erwartete Schnittstelle oder eine abstrakte Basisklasse getestet, aber wenn unsere Datenobjekte an Funktionen übergeben werden, die auf dict testen, können wir dies nicht beheben "Diese Funktionen, dieser Code wird fehlschlagen.

Andere Streitigkeiten, die man machen könnte:

  • Der akzeptierten Antwort fehlt auch die Klassenmethode: fromkeys.
  • Die akzeptierte Antwort hat auch eine redundante __dict__ - daher nimmt mehr Speicherplatz in Anspruch:

    >>> s.foo = 'bar'
    >>> s.__dict__
    {'foo': 'bar', 'store': {'test': 'test'}}
    

Tatsächlich Unterklassen von dict

Wir können die Diktiermethoden durch Vererbung wiederverwenden. Wir müssen lediglich eine Schnittstellenebene erstellen, die sicherstellt, dass Schlüssel in Kleinbuchstaben an das Diktat übergeben werden, wenn es sich um Zeichenfolgen handelt.

Wenn ich __getitem__/__setitem__ Überschreibe, funktioniert get/set nicht. Wie bringe ich sie zum Arbeiten? Sicherlich muss ich sie nicht einzeln implementieren?

Nun, sie einzeln zu implementieren ist der Nachteil dieses Ansatzes und der Vorteil der Verwendung von MutableMapping (siehe die akzeptierte Antwort), aber es ist wirklich nicht viel mehr Arbeit.

Lassen Sie uns zuerst den Unterschied zwischen Python 2 und 3) ausklammern und einen Singleton (_RaiseKeyError) Erstellen, um sicherzustellen, dass wir wissen, ob wir tatsächlich ein Argument für dict.pop, und erstellen Sie eine Funktion, um sicherzustellen, dass die Zeichenfolgenschlüssel in Kleinbuchstaben geschrieben sind:

from itertools import chain
try:              # Python 2
    str_base = basestring
    items = 'iteritems'
except NameError: # Python 3
    str_base = str, bytes, bytearray
    items = 'items'

_RaiseKeyError = object() # singleton for no-default behavior

def ensure_lower(maybe_str):
    """dict keys can be any hashable object - only call lower if str"""
    return maybe_str.lower() if isinstance(maybe_str, str_base) else maybe_str

Jetzt implementieren wir - ich verwende super mit den vollständigen Argumenten, damit dieser Code für Python 2 und 3 funktioniert:

class LowerDict(dict):  # dicts take a mapping or iterable as their optional first argument
    __slots__ = () # no __dict__ - that would be redundant
    @staticmethod # because this doesn't make sense as a global function.
    def _process_args(mapping=(), **kwargs):
        if hasattr(mapping, items):
            mapping = getattr(mapping, items)()
        return ((ensure_lower(k), v) for k, v in chain(mapping, getattr(kwargs, items)()))
    def __init__(self, mapping=(), **kwargs):
        super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
    def __getitem__(self, k):
        return super(LowerDict, self).__getitem__(ensure_lower(k))
    def __setitem__(self, k, v):
        return super(LowerDict, self).__setitem__(ensure_lower(k), v)
    def __delitem__(self, k):
        return super(LowerDict, self).__delitem__(ensure_lower(k))
    def get(self, k, default=None):
        return super(LowerDict, self).get(ensure_lower(k), default)
    def setdefault(self, k, default=None):
        return super(LowerDict, self).setdefault(ensure_lower(k), default)
    def pop(self, k, v=_RaiseKeyError):
        if v is _RaiseKeyError:
            return super(LowerDict, self).pop(ensure_lower(k))
        return super(LowerDict, self).pop(ensure_lower(k), v)
    def update(self, mapping=(), **kwargs):
        super(LowerDict, self).update(self._process_args(mapping, **kwargs))
    def __contains__(self, k):
        return super(LowerDict, self).__contains__(ensure_lower(k))
    def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
        return type(self)(self)
    @classmethod
    def fromkeys(cls, keys, v=None):
        return super(LowerDict, cls).fromkeys((ensure_lower(k) for k in keys), v)
    def __repr__(self):
        return '{0}({1})'.format(type(self).__name__, super(LowerDict, self).__repr__())

Wir verwenden einen Fast-Boiler-Plate-Ansatz für jede Methode oder spezielle Methode, die auf einen Schlüssel verweist. Andernfalls erhalten wir durch Vererbung Methoden: len, clear, items, keys, popitem und values kostenlos. Während dies einige sorgfältige Überlegungen erforderte, ist es trivial zu sehen, dass dies funktioniert.

(Beachten Sie, dass haskey in Python 2, entfernt in Python 3) nicht mehr empfohlen wurde.)

Hier ist einige Verwendung:

>>> ld = LowerDict(dict(foo='bar'))
>>> ld['FOO']
'bar'
>>> ld['foo']
'bar'
>>> ld.pop('FoO')
'bar'
>>> ld.setdefault('Foo')
>>> ld
{'foo': None}
>>> ld.get('Bar')
>>> ld.setdefault('Bar')
>>> ld
{'bar': None, 'foo': None}
>>> ld.popitem()
('bar', None)

Verhindere ich, dass das Beizen funktioniert, und muss ich __setstate__ Usw. implementieren?

pickling

Und die Dikt-Unterklasse Pickles ist in Ordnung:

>>> import pickle
>>> pickle.dumps(ld)
b'\x80\x03c__main__\nLowerDict\nq\x00)\x81q\x01X\x03\x00\x00\x00fooq\x02Ns.'
>>> pickle.loads(pickle.dumps(ld))
{'foo': None}
>>> type(pickle.loads(pickle.dumps(ld)))
<class '__main__.LowerDict'>

__repr__

Benötige ich repr, update und __init__?

Wir haben update und __init__ Definiert, aber Sie haben standardmäßig ein schönes __repr__:

>>> ld # without __repr__ defined for the class, we get this
{'foo': None}

Es ist jedoch gut, ein __repr__ Zu schreiben, um die Debug-Fähigkeit Ihres Codes zu verbessern. Der ideale Test ist eval(repr(obj)) == obj. Wenn es für Ihren Code einfach ist, empfehle ich ihn nachdrücklich:

>>> ld = LowerDict({})
>>> eval(repr(ld)) == ld
True
>>> ld = LowerDict(dict(a=1, b=2, c=3))
>>> eval(repr(ld)) == ld
True

Sie sehen, es ist genau das, was wir brauchen, um ein äquivalentes Objekt wiederherzustellen - dies kann in unseren Protokollen oder in Backtraces erscheinen:

>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})

Fazit

Soll ich nur mutablemapping verwenden (anscheinend sollte man UserDict oder DictMixin nicht verwenden)? Wenn das so ist, wie? Die Dokumente sind nicht gerade aufschlussreich.

Ja, das sind noch ein paar Zeilen Code, aber sie sollen umfassend sein. Meine erste Neigung wäre, die akzeptierte Antwort zu verwenden, und wenn es Probleme damit gäbe, würde ich mir meine Antwort ansehen - da es etwas komplizierter ist und es kein ABC gibt, das mir hilft, meine Benutzeroberfläche richtig zu gestalten.

Vorzeitige Optimierung führt zu einer höheren Komplexität bei der Suche nach Leistung. MutableMapping ist einfacher - so erhält es einen unmittelbaren Vorteil, alle anderen sind gleich. Um alle Unterschiede herauszustellen, lassen Sie uns dennoch vergleichen und kontrastieren.

Ich sollte hinzufügen, dass es einen Push gab, um ein ähnliches Wörterbuch in das collections Modul zu schreiben, aber es wurde abgelehnt . Sie sollten dies stattdessen wahrscheinlich einfach tun:

my_dict[transform(key)]

Es sollte viel einfacher zu debuggen sein.

Vergleichen und gegenüberstellen

Es gibt 6 Schnittstellenfunktionen, die mit der MutableMapping (die fromkeys fehlt) und 11 mit der Unterklasse dict implementiert wurden. Ich muss weder __iter__ Noch __len__ Implementieren, sondern stattdessen get, setdefault, pop, update, copy, __contains__ und fromkeys - aber diese sind ziemlich trivial, da ich für die meisten dieser Implementierungen die Vererbung verwenden kann.

Die MutableMapping implementiert einige Dinge in Python), die dict in C implementiert - daher würde ich erwarten, dass eine dict -Unterklasse in einigen Fällen performanter ist Fälle.

Wir erhalten in beiden Ansätzen einen kostenlosen __eq__ - beide setzen Gleichheit nur voraus, wenn ein anderes Wort nur in Kleinbuchstaben geschrieben ist -, aber ich denke, die Unterklasse dict wird sich schneller vergleichen lassen.

Zusammenfassung:

  • das Unterklassen von MutableMapping ist einfacher mit weniger Möglichkeiten für Fehler, aber langsamer, benötigt mehr Speicher (siehe redundantes Diktat) und schlägt fehl isinstance(x, dict)
  • unterklassen dict sind schneller, belegen weniger Speicher und übergeben isinstance(x, dict), die Implementierung ist jedoch komplexer.

Welches ist perfekter? Das hängt von Ihrer Definition von perfekt ab.

83
Aaron Hall

Meine Anforderungen waren etwas strenger:

  • Ich musste die Fallinformationen beibehalten (die Zeichenfolgen sind Pfade zu Dateien, die dem Benutzer angezeigt werden, aber es ist eine Windows-App, sodass bei allen Vorgängen die Groß- und Kleinschreibung nicht berücksichtigt werden muss).
  • Ich brauchte Schlüssel, um so klein wie möglich zu sein (es machte einen Unterschied in der Speicherleistung, abgeschnitten von 110 MB von 370). Das bedeutet, dass das Zwischenspeichern von Schlüsseln in Kleinbuchstaben nicht möglich ist.
  • Ich musste die Datenstrukturen so schnell wie möglich erstellen (diesmal wieder ein Unterschied in der Leistung, Geschwindigkeit). Ich musste mit einem eingebauten gehen

Mein erster Gedanke war, unsere klobige Path-Klasse durch eine Unicode-Unterklasse ohne Berücksichtigung der Groß- und Kleinschreibung zu ersetzen - aber:

  • erwies sich als schwierig, dies richtig zu machen - siehe: Eine String-Klasse ohne Berücksichtigung der Groß- und Kleinschreibung in Python
  • es stellt sich heraus, dass die explizite Handhabung von Diktatschlüsseln den Code ausführlich und unübersichtlich macht - und fehleranfällig (Strukturen werden hin und her übertragen und es ist nicht klar, ob sie CIStr-Instanzen als Schlüssel/Elemente enthalten, die leicht zu vergessen sind, plus some_dict[CIstr(path)] ist hässlich)

Also musste ich endlich dieses fallunabhängige Diktat aufschreiben. Dank code von @AaronHall wurde das zehnmal einfacher.

class CIstr(unicode):
    """See https://stackoverflow.com/a/43122305/281545, especially for inlines"""
    __slots__ = () # does make a difference in memory performance

    #--Hash/Compare
    def __hash__(self):
        return hash(self.lower())
    def __eq__(self, other):
        if isinstance(other, CIstr):
            return self.lower() == other.lower()
        return NotImplemented
    def __ne__(self, other):
        if isinstance(other, CIstr):
            return self.lower() != other.lower()
        return NotImplemented
    def __lt__(self, other):
        if isinstance(other, CIstr):
            return self.lower() < other.lower()
        return NotImplemented
    def __ge__(self, other):
        if isinstance(other, CIstr):
            return self.lower() >= other.lower()
        return NotImplemented
    def __gt__(self, other):
        if isinstance(other, CIstr):
            return self.lower() > other.lower()
        return NotImplemented
    def __le__(self, other):
        if isinstance(other, CIstr):
            return self.lower() <= other.lower()
        return NotImplemented
    #--repr
    def __repr__(self):
        return '{0}({1})'.format(type(self).__name__,
                                 super(CIstr, self).__repr__())

def _ci_str(maybe_str):
    """dict keys can be any hashable object - only call CIstr if str"""
    return CIstr(maybe_str) if isinstance(maybe_str, basestring) else maybe_str

class LowerDict(dict):
    """Dictionary that transforms its keys to CIstr instances.
    Adapted from: https://stackoverflow.com/a/39375731/281545
    """
    __slots__ = () # no __dict__ - that would be redundant

    @staticmethod # because this doesn't make sense as a global function.
    def _process_args(mapping=(), **kwargs):
        if hasattr(mapping, 'iteritems'):
            mapping = getattr(mapping, 'iteritems')()
        return ((_ci_str(k), v) for k, v in
                chain(mapping, getattr(kwargs, 'iteritems')()))
    def __init__(self, mapping=(), **kwargs):
        # dicts take a mapping or iterable as their optional first argument
        super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
    def __getitem__(self, k):
        return super(LowerDict, self).__getitem__(_ci_str(k))
    def __setitem__(self, k, v):
        return super(LowerDict, self).__setitem__(_ci_str(k), v)
    def __delitem__(self, k):
        return super(LowerDict, self).__delitem__(_ci_str(k))
    def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
        return type(self)(self)
    def get(self, k, default=None):
        return super(LowerDict, self).get(_ci_str(k), default)
    def setdefault(self, k, default=None):
        return super(LowerDict, self).setdefault(_ci_str(k), default)
    __no_default = object()
    def pop(self, k, v=__no_default):
        if v is LowerDict.__no_default:
            # super will raise KeyError if no default and key does not exist
            return super(LowerDict, self).pop(_ci_str(k))
        return super(LowerDict, self).pop(_ci_str(k), v)
    def update(self, mapping=(), **kwargs):
        super(LowerDict, self).update(self._process_args(mapping, **kwargs))
    def __contains__(self, k):
        return super(LowerDict, self).__contains__(_ci_str(k))
    @classmethod
    def fromkeys(cls, keys, v=None):
        return super(LowerDict, cls).fromkeys((_ci_str(k) for k in keys), v)
    def __repr__(self):
        return '{0}({1})'.format(type(self).__name__,
                                 super(LowerDict, self).__repr__())

Implizite vs explizite sind immer noch ein Problem, aber sobald sich der Staub gelegt hat, halte ich die Umbenennung von Attributen/Variablen, um mit ci zu beginnen (und einen großen, fetten Doc-Kommentar, der erklärt, dass ci für case insensitive steht), für eine perfekte Lösung - als Leser des Codes Seien Sie sich voll und ganz bewusst, dass es sich um Datenstrukturen handelt, bei denen die Groß- und Kleinschreibung nicht berücksichtigt wird. Dies wird hoffentlich einige schwer zu reproduzierende Fehler beheben, von denen ich vermute, dass sie auf die Groß- und Kleinschreibung zurückzuführen sind.

Kommentare/Korrekturen erwünscht :)

4
Mr_and_Mrs_D

Alles was Sie tun müssen, ist

class BatchCollection(dict):
    def __init__(self, *args, **kwargs):
        dict.__init__(*args, **kwargs)

OR

class BatchCollection(dict):
    def __init__(self, inpt={}):
        super(BatchCollection, self).__init__(inpt)

Eine Beispielnutzung für meinen persönlichen Gebrauch

### EXAMPLE
class BatchCollection(dict):
    def __init__(self, inpt={}):
        dict.__init__(*args, **kwargs)

    def __setitem__(self, key, item):
        if (isinstance(key, Tuple) and len(key) == 2
                and isinstance(item, collections.Iterable)):
            # self.__dict__[key] = item
            super(BatchCollection, self).__setitem__(key, item)
        else:
            raise Exception(
                "Valid key should be a Tuple (database_name, table_name) "
                "and value should be iterable")

Anmerkung: nur in python3 getestet

3
ravi404

Nachdem ich beide obenzwei Vorschläge ausprobiert habe, habe ich mich für Python 2.7 für eine schattig aussehende Mittelroute entschieden. Vielleicht ist 3 vernünftiger, aber für mich:

class MyDict(MutableMapping):
   # ... the few __methods__ that mutablemapping requires
   # and then this monstrosity
   @classmethod
   def __class__(cls):
       return dict

was ich wirklich hasse, aber anscheinend meinen Bedürfnissen entspricht:

  • kann überschreiben **my_dict
    • wenn Sie von dict erben , wird Ihr Code umgangen . Versuch es.
    • das macht # 2 für mich zu jeder Zeit inakzeptabel , da dies in python-Code durchaus üblich ist
  • maskeraden als isinstance(my_dict, dict)
    • schließt MutableMapping alleine aus, also ist # 1 nicht genug
    • Ich empfehle von Herzen # 1 Wenn Sie dies nicht benötigen, ist es einfach und vorhersehbar
  • voll kontrollierbares Verhalten
    • also kann ich nicht von dict erben

Wenn Sie sich von anderen unterscheiden müssen, benutze ich persönlich so etwas (obwohl ich bessere Namen empfehlen würde):

def __am_i_me(self):
  return True

@classmethod
def __is_it_me(cls, other):
  try:
    return other.__am_i_me()
  except Exception:
    return False

Solange Sie sich nur intern erkennen müssen, ist es auf diese Weise schwieriger, __am_i_me Versehentlich aufzurufen, da Python einen Namen verwendet (dieser Name wird von allen Aufrufen außerhalb dieser Klasse in _MyDict__am_i_me Umbenannt). Etwas privater als _method, Sowohl in der Praxis als auch kulturell.

Bis jetzt habe ich keine Beschwerden, abgesehen von dem ernsthaft zwielichtig aussehenden __class__ Override. Ich würde mich freuen , von Problemen zu hören, die andere damit haben, aber ich verstehe die Konsequenzen nicht ganz. Bisher hatte ich jedoch keinerlei Probleme, und so konnte ich eine Menge Code mit mittlerer Qualität an vielen Orten migrieren, ohne Änderungen vornehmen zu müssen.


Als Beweis: https://repl.it/repls/TraumaticToughCockatoo

Grundsätzlich gilt: copy die aktuelle Option # 2 , füge jeder Methode print 'method_name' Zeilen hinzu, versuche dies und beobachte die Ausgabe:

d = LowerDict()  # prints "init", or whatever your print statement said
print '------'
splatted = dict(**d)  # note that there are no prints here

Sie sehen ein ähnliches Verhalten für andere Szenarien. Angenommen, Ihr falsches -dict ist ein Wrapper um einen anderen Datentyp. Es gibt also keine vernünftige Möglichkeit, die Daten im Hintergrunddiktat zu speichern. **your_dict Ist leer, unabhängig davon, was jede andere Methode tut.

Dies funktioniert korrekt für MutableMapping, aber sobald Sie von dict erben, wird es unkontrollierbar.

2
Groxx