Jak „idealnie” zastąpić dyktando?

218

Jak mogę uczynić „perfekcyjną” podklasę dyktku, jak to możliwe? Ostatecznym celem jest prosty dyktando, w którym klawisze są pisane małymi literami.

Wydawałoby się, że powinien istnieć niewielki zestaw prymitywów, które mogę zastąpić, aby to zadziałało, ale według wszystkich moich badań i prób wydaje się, że tak nie jest:

Oto mój pierwszy krok, get()nie działa i bez wątpienia istnieje wiele innych drobnych problemów:

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 
    # /programming/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()
Paul Biggar
źródło
Myślę, że __keytransform __ () powinien być statyczny. Przyjemne podejście. (prepending @staticmethod)
Aiyion.Prime

Odpowiedzi:

229

Możesz napisać obiekt, który zachowuje się dictdość łatwo dzięki ABC (Abstract Base Classes) z collections.abcmodułu. Mówi nawet, jeśli przegapiłeś jakąś metodę, więc poniżej znajduje się minimalna wersja, która wyłącza ABC.

from collections.abc import MutableMapping


class TransformedDict(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

Otrzymujesz kilka bezpłatnych metod od 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
# works too since we just use a normal dict
assert pickle.loads(pickle.dumps(s)) == s

Nie dictpodklasowałbym (lub innych wbudowanych) bezpośrednio. Często nie ma to sensu, ponieważ tak naprawdę chcesz zaimplementować interfejsdict . I właśnie do tego służą ABC.

Jochen Ritzel
źródło
46
Sugerowałbym zmianę nazwy, __keytransform__()ponieważ narusza przewodnik po stylu PEP 8, który radzi: „Nigdy nie wymyślaj takich nazw; używaj ich tylko zgodnie z dokumentacją” na końcu sekcji Descriptive: Style nazw .
martineau,
1
Pytanie jednak - czy implementacja tego interfejsu z typem zdefiniowanym przez użytkownika nie spowoduje ogólnie wolniejszych operacji przypominających dyktowanie niż przy użyciu typu wbudowanego?
twneale
2
Czy jest na to sposób, aby isinstance (_, dict) == Prawda? A może po prostu używasz Zmiennego Mapowania do tworzenia podklasy?
Andy Hayden
5
@AndyHayden: Powinieneś pisać if isinstance(t, collections.MutableMapping): print t, "can be used like a dict". Nie sprawdzaj typu obiektu, sprawdź interfejs.
Jochen Ritzel
2
@NeilG To niestety obejmuje JSONEncoder w standardowej bibliotece pythona
Andy Smith
97

Jak mogę uczynić „perfekcyjną” podklasę dyktku, jak to możliwe?

Ostatecznym celem jest prosty dyktando, w którym klawisze są pisane małymi literami.

  • Jeśli zastąpię __getitem__/ __setitem__, wtedy get / set nie działa. Jak sprawić, by działały? Z pewnością nie muszę wdrażać ich indywidualnie?

  • Czy zapobiegam działaniu wytrawiania i czy muszę wdrażać __setstate__itp.?

  • Czy potrzebuję powtórzenia, aktualizacji i __init__?

  • Czy powinienem użyć mutablemapping(wydaje się, że nie należy używać UserDict lub DictMixin)? Jeśli tak to jak? Dokumenty nie są do końca pouczające.

Przyjęta odpowiedź byłaby moim pierwszym podejściem, ale ponieważ ma pewne problemy, a ponieważ nikt nie zajął się alternatywą, właściwie podklasą a dict, zrobię to tutaj.

Co jest nie tak z zaakceptowaną odpowiedzią?

Wydaje mi się to dość prostą prośbą:

Jak mogę uczynić „perfekcyjną” podklasę dyktku, jak to możliwe? Ostatecznym celem jest prosty dyktando, w którym klawisze są pisane małymi literami.

Przyjęta odpowiedź nie jest właściwie podklasą dict, a test na to się nie powiedzie:

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

Idealnie byłoby, gdyby każdy kod sprawdzający typ testowałby oczekiwany interfejs lub abstrakcyjną klasę podstawową, ale jeśli nasze obiekty danych są przekazywane do funkcji, które testują dict- i nie możemy „naprawić” tych funkcji, ten kod zawiedzie.

Inne sprzeczki, które można zrobić:

  • Odpowiedź Akceptowane jest również brakuje classmethod: fromkeys.
  • Przyjęta odpowiedź ma również nadmiar __dict__- dlatego zajmuje więcej miejsca w pamięci:

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

Właściwie podklasę dict

Możemy ponownie wykorzystać metody dict poprzez dziedziczenie. Wszystko, co musimy zrobić, to stworzyć warstwę interfejsu, która zapewni, że klucze będą przekazywane do nagrania małymi literami, jeśli są łańcuchami.

Jeśli zastąpię __getitem__/ __setitem__, wtedy get / set nie działa. Jak sprawić, by działały? Z pewnością nie muszę wdrażać ich indywidualnie?

Cóż, wdrażanie ich osobno jest wadą tego podejścia i zaletą używania MutableMapping(patrz zaakceptowana odpowiedź), ale tak naprawdę nie jest to dużo więcej pracy.

Po pierwsze, rozłóżmy różnicę między Pythonem 2 i 3, utwórz singleton ( _RaiseKeyError), aby upewnić się, że rzeczywiście otrzymujemy argument dict.pop, i utwórz funkcję, aby nasze klucze łańcuchowe były pisane małymi literami:

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

Teraz implementujemy - używam superz pełnymi argumentami, aby ten kod działał dla Pythona 2 i 3:

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__())

Używamy podejście niemal bojler-płyta dla dowolnej metody lub specjalną metodą, która odwołuje się kluczowym, ale w inny sposób, przez dziedziczenie, otrzymujemy metody: len, clear, items, keys, popitem, i valuesza darmo. Choć wymagało to starannego przemyślenia, nie jest łatwo dostrzec, że to działa.

(Uwaga: haskeyprzestarzała w Pythonie 2, usunięta w Pythonie 3)

Oto niektóre zastosowania:

>>> 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)

Czy zapobiegam działaniu wytrawiania i czy muszę wdrażać __setstate__itp.?

marynowanie

A pikle podklasy dict są w porządku:

>>> 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__

Czy potrzebuję powtórzenia, aktualizacji i __init__?

Zdefiniowaliśmy updatei __init__, ale __repr__domyślnie masz piękny :

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

__repr__Warto jednak napisać a, aby poprawić debugowanie kodu. Idealny test to eval(repr(obj)) == obj. Jeśli jest to łatwe do zrobienia dla twojego kodu, zdecydowanie go polecam:

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

Widzisz, dokładnie tego potrzebujemy do odtworzenia równoważnego obiektu - jest to coś, co może pojawić się w naszych dziennikach lub śladach:

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

Wniosek

Czy powinienem użyć mutablemapping(wydaje się, że nie należy używać UserDict lub DictMixin)? Jeśli tak to jak? Dokumenty nie są do końca pouczające.

Tak, to jeszcze kilka wierszy kodu, ale mają one być wyczerpujące. Moją pierwszą skłonnością byłoby użyć zaakceptowanej odpowiedzi, a jeśli byłyby z nią jakieś problemy, spojrzałbym na moją odpowiedź - ponieważ jest to trochę bardziej skomplikowane i nie ma ABC, który pomógłby mi poprawnie ustawić interfejs.

Przedwczesna optymalizacja dąży do większej złożoności w poszukiwaniu wydajności. MutableMappingjest prostszy - dzięki czemu uzyskuje natychmiastową przewagę, a wszystkie pozostałe są równe. Niemniej jednak, aby przedstawić wszystkie różnice, porównajmy i skontrastujmy.

Powinienem dodać, że pojawił się nacisk na umieszczenie podobnego słownika w collectionsmodule, ale został on odrzucony . Prawdopodobnie powinieneś po prostu zrobić to zamiast tego:

my_dict[transform(key)]

Powinno być znacznie łatwiejsze do debugowania.

Porównać i kontrastować

Zaimplementowano 6 funkcji interfejsu z MutableMapping(której brakuje fromkeys) i 11 z dictpodklasą. Nie muszę wdrożyć __iter__lub __len__, lecz muszę wdrożyć get, setdefault, pop, update, copy, __contains__, i fromkeys- ale są to dość trywialne, ponieważ mogę używać dziedziczenia dla większości z tych wdrożeń.

Te MutableMappingnarzędzia pewne rzeczy w Pythonie, które dictwdraża w C - więc spodziewałbym się dictpodklasa być bardziej wydajnych w niektórych przypadkach.

Dostajemy darmowe __eq__w obu podejściach - oba zakładają równość tylko wtedy, gdy inny dykt jest pisany małymi literami - ale znowu, myślę, że dictpodklasa porówna się szybciej.

Podsumowanie:

  • podklasowanie MutableMappingjest prostsze z mniejszą liczbą błędów, ale wolniejsze, zajmuje więcej pamięci (patrz redundantny dykt) i kończy się niepowodzeniemisinstance(x, dict)
  • podklasowanie dictjest szybsze, zużywa mniej pamięci i przechodzi isinstance(x, dict), ale ma większą złożoność do wdrożenia.

Który jest bardziej idealny? To zależy od twojej definicji ideału.

Aaron Hall
źródło
W jaki sposób zaakceptowana odpowiedź usunęłaby zbędny dyktat?
Seanny123
1
Dwa sposoby, które od razu przychodzą mi do głowy, to albo zadeklarować atrybut sklepu __slots__lub być może ponownie użyć go __dict__jako sklepu, ale to miesza semantykę, kolejny potencjalny punkt krytyki.
Aaron Hall
1
Czy nie byłoby łatwiej napisać dekorator, który przyjmuje metodę i używa twojego ensure_lowerpierwszego argumentu (który zawsze jest kluczem)? Wtedy byłaby taka sama liczba przesłonięć, ale wszystkie miałyby formę __getitem__ = ensure_lower_decorator(super(LowerDict, self).__getitem__).
Graipher 17.03.17
1
Dzięki za to - otrzymuję ostrzeżenia dla popu i klawiszy, że nie pasują one do sygnatury metody klasy bazowej.
Mr_and_Mrs_D
1
@Mr_and_Mrs_D Dodałem implementację copy- Myślę, że to powinno zrobić, nie? Myślę, że powinien przetestować interfejs - np. Obiekt DataFrame pandy nie jest instancją Mapping (przy ostatnim sprawdzeniu), ale ma elementy / iterity.
Aaron Hall
4

Moje wymagania były nieco bardziej rygorystyczne:

  • Musiałem zachować informacje o wielkości liter (ciągi są ścieżkami do plików wyświetlanych użytkownikowi, ale jest to aplikacja systemu Windows, więc wewnętrznie we wszystkich operacjach nie jest rozróżniana wielkość liter)
  • Potrzebowałem kluczy tak małych, jak to tylko możliwe ( miało to wpływ na wydajność pamięci, odciąłem 110 mb z 370). Oznaczało to, że buforowanie małych wersji kluczy nie jest opcją.
  • Potrzebowałem stworzenia struktur danych, aby było jak najszybciej (znowu zrobiłem różnicę w wydajności, tym razem szybszym). Musiałem iść z wbudowanym

Początkowo myślałem, aby zastąpić naszą niezgrabną klasę Path podklasą Unicode niewrażliwą na wielkość liter - ale:

  • okazało się trudne do zrobienia tego poprawnie - patrz: Klasa znaków bez rozróżniania wielkości liter w Pythonie
  • okazuje się, że jawne posługiwanie się kluczami dict sprawia, że ​​kod jest pełny i bałagan - i podatny na błędy (struktury są przekazywane tam i tam, i nie jest jasne, czy mają instancje CIStr jako klucze / elementy, łatwe do zapomnienia plus some_dict[CIstr(path)]jest brzydki)

Musiałem więc w końcu zanotować dyktandę bez uwzględniania wielkości liter. Dzięki kodowi @AaronHall, który był 10 razy łatwiejszy.

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__())

Implikowane kontra jawne nadal stanowi problem, ale gdy opadnie kurz, zmiana nazw atrybutów / zmiennych na początek z ci (i duży gruby komentarz doc wyjaśniający, że ci oznacza rozróżnianie wielkości liter) Myślę, że jest to idealne rozwiązanie - ponieważ czytelnicy kodu muszą być w pełni świadomym, że mamy do czynienia z bazowymi strukturami danych bez rozróżniania wielkości liter. Mam nadzieję, że to naprawi niektóre trudne do odtworzenia błędy, które, jak podejrzewam, sprowadzają się do rozróżnienia wielkości liter.

Komentarze / poprawki mile widziane :)

Mr_i_Mrs_D
źródło
CIstr __repr__powinien użyć klasy nadrzędnej, __repr__aby przekazać test eval (repr (obj)) == obj (nie sądzę, że teraz tak się dzieje) i nie polegać na nim __str__.
Aaron Hall
Sprawdź także total_orderingdekorator klas - który wyeliminuje 4 metody z podklasy Unicode. Ale podklasa dict wygląda bardzo sprytnie. : P
Aaron Hall
Dzięki @AaronHall - to Ty zaimplementowałeś: P Re: Total Ordering - Celowo napisałem metody przedstawione zgodnie z zaleceniami Raymonda Hettingera tutaj: stackoverflow.com/a/43122305/281545 . Re: repr: Pamiętam, że dobrze przeczytałem komentarz (autorstwa jakiegoś podstawowego dewelopera IIRC), naprawdę nie warto próbować sprawić, by repr zdał ten test (jest to problem) - lepiej skoncentruj się na tym, aby był jak najbardziej informacyjny ( ale nie więcej)
Mr_and_Mrs_D
Pozwolę ci na zbędne metody porównywania (powinieneś zanotować to w swojej odpowiedzi), ale CIstr.__repr__, w twoim przypadku, może przejść test repr z bardzo niewielkim kłopotem i powinno sprawić, że debugowanie będzie znacznie przyjemniejsze. Dodałbym również __repr__do twojego słownika. Zrobię to w mojej odpowiedzi, aby zademonstrować.
Aaron Hall
@AaronHall: Dodałem __slots__w CIstr - robi różnicę w wydajności (CIstr nie ma być podklasowany ani faktycznie używany poza LowerDict, powinien być statyczną zagnieżdżoną klasą końcową). Nadal nie jestem pewien, jak elegancko rozwiązać problem repr (żądło może zawierać kombinację znaków 'i "cytatów)
Mr_and_Mrs_D
4

Wszystko, co musisz zrobić, to

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

LUB

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

Przykładowe użycie do mojego osobistego użytku

### 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")

Uwaga : testowane tylko w python3

ravi404
źródło
3

Po wypróbowaniu obu dwóch najważniejszych propozycji zdecydowałem się na podejrzanie wyglądającą środkową trasę dla Pythona 2.7. Może 3 jest zdrowsze, ale dla mnie:

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

których naprawdę nienawidzę, ale wydaje się, że pasują do moich potrzeb, a mianowicie:

  • może zastąpić **my_dict
    • jeśli odziedziczysz po dict, pomija twój kod . Wypróbuj to.
    • sprawia to, że numer 2 jest dla mnie przez cały czas nie do przyjęcia , ponieważ jest to dość powszechne w kodzie Pythona
  • maskarady jako isinstance(my_dict, dict)
    • wyklucza MutableMapping sam, więc # 1 to za mało
    • Polecam serdecznie nr 1, jeśli nie jest to potrzebne, jest to proste i przewidywalne
  • w pełni kontrolowane zachowanie
    • więc nie mogę dziedziczyć dict

Jeśli chcesz się odróżnić od innych, osobiście używam czegoś takiego (chociaż zaleciłbym lepsze nazwy):

def __am_i_me(self):
  return True

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

Tak długo, jak musisz tylko rozpoznać siebie wewnętrznie, w ten sposób trudniej jest przypadkowo zadzwonić __am_i_meze względu na munging pythona (zmiana nazwy na _MyDict__am_i_medowolną wywołanie poza tą klasą). Nieco bardziej prywatne niż _methods, zarówno w praktyce, jak i kulturowo.

Jak dotąd nie mam żadnych skarg poza poważnie wyglądającym __class__obejściem. Byłbym podekscytowany, gdybym usłyszał o wszelkich problemach napotykanych przez innych, nie do końca rozumiem konsekwencje. Ale do tej pory nie miałem żadnych problemów, co pozwoliło mi na migrację dużej ilości kodu o średniej jakości w wielu lokalizacjach bez konieczności wprowadzania jakichkolwiek zmian.


Jako dowód: https://repl.it/repls/TraumaticToughCockatoo

Zasadniczo: skopiuj bieżącą opcję nr 2 , dodaj print 'method_name'wiersze do każdej metody, a następnie spróbuj tego i obserwuj wynik:

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

Zobaczysz podobne zachowanie dla innych scenariuszy. Powiedz, że twój fałszywy dictjest opakowaniem wokół innego typu danych, więc nie ma rozsądnego sposobu na przechowywanie danych w back-dict; **your_dictbędzie pusty, niezależnie od tego, co robi każda inna metoda.

Działa to poprawnie MutableMapping, ale gdy tylko odziedziczysz po dictnim, staje się niekontrolowane.


Edycja: jako aktualizacja, działa bez problemu od prawie dwóch lat, na kilkuset tysiącach (eh, może to być kilka milionów) linii skomplikowanego, opartego na dziedzictwie Pythona. Jestem z tego całkiem zadowolony :)

Edycja 2: najwyraźniej źle skopiowałem to lub coś dawno temu. @classmethod __class__nie działa dla isinstanceczeków - @property __class__robi: https://repl.it/repls/UnitedScientificSequence

Groxx
źródło
Co dokładnie rozumiesz przez **your_dictbędzie pusty” (jeśli podklasujesz dict)? Nie widziałem żadnych problemów z rozpakowywaniem dykt ...
Matt P
Jeśli faktycznie umieścisz dane w macierzystym dykcie (tak jak robi to LowerDict), to zadziała - dostaniesz dane przechowywane w dyktach. Jeśli tego nie zrobisz (powiedz, że chcesz generować dane w locie, np. {Access_count: „stos śledzenia dostępu”}, który wypełnia się za każdym razem, gdy jest czytany), zauważysz, że **your_dictnie wykonuje kodu, więc nie można wyprowadzić niczego „specjalnego”. Np. Nie można liczyć „odczytów”, ponieważ nie wykonuje on kodu liczącego odczyty. MutableMapping wykonuje pracę dla tego (użyj go, jeśli to możliwe!), Ale to nie isinstance(..., dict)tak, że nie mógł z niego korzystać. tak, starsze oprogramowanie.
Groxx
Ok, rozumiem co masz teraz na myśli. Przypuszczam, że nie spodziewałem się wykonania kodu **your_dict, ale uważam, że to bardzo interesujące MutableMapping.
Matt P
Tak. Jest to konieczne z wielu powodów (np. Odkładałem rozmowy RPC na coś, co kiedyś czytałem lokalny dykt, i musiałem to robić na żądanie dla Reasons ™), i wydaje się, że niewiele osób zdaje sobie z tego sprawę, nawet jeśli **some_dictjest dość powszechny. Przynajmniej zdarza się bardzo często w dekoratorów, więc jeśli masz jakikolwiek , jesteś natychmiast zagrożone pozornie niemożliwej złe zachowanie, jeśli nie stanowią dla niego.
Groxx
Być może coś mi brakuje, ale def __class__()wydaje się , że sztuczka nie działa z Pythonem 2 lub 3, przynajmniej dla przykładowego kodu w pytaniu Jak zarejestrować implementację abc.MutableMapping jako podklasę dict? (zmodyfikowany, by działał w dwóch wersjach). Chcę isinstance(SpreadSheet(), dict)wrócić True.
martineau,