Bagaimana cara "sempurna" menimpa sebuah dict?


218

Bagaimana saya bisa membuat subkelas dict sebagai "sesempurna" ? Tujuan akhirnya adalah memiliki dict sederhana di mana kuncinya adalah huruf kecil.

Tampaknya harus ada sekelompok kecil primitif yang bisa saya timpa untuk membuat ini berhasil, tetapi menurut semua penelitian dan upaya saya, sepertinya ini bukan masalahnya:

  • Jika saya mengganti __getitem__/__setitem__ , maka get/ settidak berfungsi. Bagaimana saya bisa membuatnya bekerja? Tentunya saya tidak perlu mengimplementasikannya secara individual?

  • Apakah saya mencegah acar tidak bekerja, dan apakah saya perlu menerapkan __setstate__dll?

  • Apakah saya perlu repr, updatedan__init__ ?

  • Haruskah saya menggunakan mutablemapping (sepertinya orang tidak boleh menggunakan UserDict atau DictMixin)? Jika ya, bagaimana caranya? Dokumen tidak sepenuhnya mencerahkan.

Inilah yang pertama saya lakukan, get()tidak bekerja dan tidak diragukan lagi ada banyak masalah kecil lainnya:

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

Saya pikir __keytransform __ () harus statis. Pendekatan yang bagus. (prepending @staticmethod)
Aiyion.Prime

Jawaban:


229

Anda dapat menulis objek yang berperilaku seperti dictcukup mudah dengan ABC (Abstract Base Classes) dari collections.abcmodul. Bahkan memberi tahu Anda jika Anda melewatkan suatu metode, jadi di bawah ini adalah versi minimal yang menutup 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

Anda mendapatkan beberapa metode gratis dari 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

Saya tidak akan subkelas dict(atau builtin lainnya) secara langsung. Seringkali tidak masuk akal, karena apa yang sebenarnya ingin Anda lakukan adalah mengimplementasikan antarmuka adict . Dan itulah tepatnya untuk apa ABC.


46
Saya akan menyarankan penggantian nama __keytransform__()karena melanggar panduan gaya PEP 8 yang menyarankan "Jangan pernah menemukan nama seperti itu; gunakan saja seperti yang didokumentasikan" di akhir bagian Deskriptif: Penamaan Gaya .
martineau

1
Pertanyaannya - tidakkah mengimplementasikan antarmuka ini dengan tipe yang ditentukan pengguna umumnya menghasilkan operasi yang lebih lambat seperti dict yang menggunakan tipe bawaan?
twneale

2
Apakah ada cara untuk melakukan ini sehingga isinstance (_, dict) == Benar? Atau apakah Anda hanya menggunakan Pemetaan Mutable untuk membangun kemudian subkelas?
Andy Hayden

5
@AndyHayden: Anda harus menulis if isinstance(t, collections.MutableMapping): print t, "can be used like a dict". Jangan periksa jenis objek, periksa antarmuka.
Jochen Ritzel

2
@NeilG Sayangnya ini termasuk JSONEncoder di pustaka standar python - github.com/python-git/python/blob/…
Andy Smith

97

Bagaimana saya bisa membuat subkelas dict sebagai "sesempurna"?

Tujuan akhirnya adalah memiliki dict sederhana di mana kuncinya adalah huruf kecil.

  • Jika saya mengganti __getitem__/ __setitem__, maka get / set tidak berfungsi. Bagaimana saya membuatnya bekerja? Tentunya saya tidak perlu mengimplementasikannya secara individual?

  • Apakah saya mencegah acar tidak bekerja, dan apakah saya perlu menerapkan __setstate__dll?

  • Apakah saya perlu repr, perbarui dan __init__?

  • Haruskah saya hanya menggunakan mutablemapping(sepertinya orang tidak boleh menggunakan UserDict atau DictMixin)? Jika ya, bagaimana caranya? Dokumen tidak sepenuhnya mencerahkan.

Jawaban yang diterima akan menjadi pendekatan pertama saya, tetapi karena memiliki beberapa masalah, dan karena tidak ada yang membahas alternatifnya, sebenarnya subklasifikasi a dict, saya akan melakukannya di sini.

Apa yang salah dengan jawaban yang diterima?

Ini sepertinya permintaan yang agak sederhana bagi saya:

Bagaimana saya bisa membuat subkelas dict sebagai "sesempurna"? Tujuan akhirnya adalah memiliki dict sederhana di mana kuncinya adalah huruf kecil.

Jawaban yang diterima sebenarnya bukan subkelas dict, dan tes untuk ini gagal:

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

Idealnya, kode pemeriksaan jenis apa pun akan menguji antarmuka yang kami harapkan, atau kelas dasar abstrak, tetapi jika objek data kami diteruskan ke fungsi yang sedang diuji dict- dan kami tidak dapat "memperbaiki" fungsi-fungsi itu, kode ini akan gagal.

Pertengkaran lain yang mungkin terjadi:

  • Jawaban yang diterima juga hilang classmethod yang: fromkeys.
  • Jawaban yang diterima juga memiliki redundan __dict__- karena itu mengambil lebih banyak ruang dalam memori:

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

Sebenarnya subklasifikasi dict

Kita dapat menggunakan kembali metode dikt melalui pewarisan. Yang perlu kita lakukan adalah membuat lapisan antarmuka yang memastikan kunci dilewatkan ke dalam dikt dalam bentuk huruf kecil jika mereka adalah string.

Jika saya mengganti __getitem__/ __setitem__, maka get / set tidak berfungsi. Bagaimana saya membuatnya bekerja? Tentunya saya tidak perlu mengimplementasikannya secara individual?

Ya, menerapkannya masing-masing secara perorangan adalah kelemahan dari pendekatan ini dan sisi baiknya untuk menggunakan MutableMapping(lihat jawaban yang diterima), tetapi sebenarnya tidak terlalu banyak pekerjaan.

Pertama, mari kita faktor perbedaan antara Python 2 dan 3, buat singleton ( _RaiseKeyError) untuk memastikan kita tahu jika kita benar-benar mendapatkan argumen dict.pop, dan membuat fungsi untuk memastikan kunci string kita huruf kecil:

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

Sekarang kita implementasikan - saya menggunakan superdengan argumen lengkap sehingga kode ini berfungsi untuk Python 2 dan 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__())

Kami menggunakan pendekatan hampir boiler-piring untuk setiap metode atau metode khusus yang referensi kunci, tetapi sebaliknya, oleh warisan, kita mendapatkan metode: len, clear, items, keys, popitem, dan valuesgratis. Sementara ini membutuhkan beberapa pemikiran yang cermat untuk mendapatkan yang benar, itu sepele untuk melihat bahwa ini berhasil.

(Catatan yang haskeysudah usang dalam Python 2, dihapus dalam Python 3.)

Inilah beberapa penggunaan:

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

Apakah saya mencegah acar tidak bekerja, dan apakah saya perlu menerapkan __setstate__dll?

acar

Dan acar subclass dict baik-baik saja:

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

Apakah saya perlu repr, perbarui dan __init__?

Kami mendefinisikan updatedan __init__, tetapi Anda memiliki cantik __repr__secara default:

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

Namun, ada baiknya menulis __repr__untuk meningkatkan kemampuan debug kode Anda. Tes yang ideal adalah eval(repr(obj)) == obj. Jika mudah dilakukan untuk kode Anda, saya sangat merekomendasikannya:

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

Anda tahu, itu persis apa yang kita butuhkan untuk membuat ulang objek yang setara - ini adalah sesuatu yang mungkin muncul di log kami atau di backtraces:

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

Kesimpulan

Haruskah saya hanya menggunakan mutablemapping(sepertinya orang tidak boleh menggunakan UserDict atau DictMixin)? Jika ya, bagaimana caranya? Dokumen tidak sepenuhnya mencerahkan.

Ya, ini adalah beberapa baris kode lagi, tetapi dimaksudkan untuk bersifat komprehensif. Kecenderungan pertama saya adalah menggunakan jawaban yang diterima, dan jika ada masalah dengannya, maka saya akan melihat jawaban saya - karena ini sedikit lebih rumit, dan tidak ada ABC untuk membantu saya mengatur antarmuka saya dengan benar.

Optimalisasi prematur akan meningkatkan kompleksitas dalam mencari kinerja. MutableMappinglebih sederhana - sehingga mendapat keunggulan langsung, semuanya sama. Namun demikian, untuk menjelaskan semua perbedaan, mari kita bandingkan dan kontraskan.

Saya harus menambahkan bahwa ada dorongan untuk memasukkan kamus serupa ke dalam collectionsmodul, tetapi ditolak . Anda mungkin harus melakukan ini sebagai gantinya:

my_dict[transform(key)]

Seharusnya jauh lebih mudah di-debug.

Membandingkan dan kontras

Ada 6 fungsi antarmuka diimplementasikan dengan MutableMapping(yang hilang fromkeys) dan 11 dengan dictsubclass. Saya tidak perlu untuk mengimplementasikan __iter__atau __len__, tetapi aku harus melaksanakan get, setdefault, pop, update, copy, __contains__, dan fromkeys- tetapi ini cukup sepele, karena saya bisa menggunakan warisan untuk sebagian dari mereka implementasi.

The MutableMappingalat beberapa hal di Python yang dictmengimplementasikan dalam C - jadi saya akan mengharapkan dictsubclass untuk lebih performant dalam beberapa kasus.

Kami mendapatkan gratis __eq__di kedua pendekatan - yang keduanya mengasumsikan kesetaraan hanya jika dict lain semua huruf kecil - tapi sekali lagi, saya pikir dictsubclass akan membandingkan lebih cepat.

Ringkasan:

  • subclassing MutableMappinglebih sederhana dengan peluang bug yang lebih sedikit, tetapi lebih lambat, membutuhkan lebih banyak memori (lihat redundant dict), dan gagalisinstance(x, dict)
  • subkelas dictlebih cepat, menggunakan lebih sedikit memori, dan lulus isinstance(x, dict), tetapi memiliki kompleksitas yang lebih besar untuk diterapkan.

Mana yang lebih sempurna? Itu tergantung pada definisi Anda tentang sempurna.


Bagaimana jawaban yang diterima menghapus dict berlebihan?
Seanny123

1
Dua cara yang langsung terlintas dalam pikiran adalah dengan mendeklarasikan atribut toko dalam __slots__atau mungkin menggunakan kembali __dict__sebagai toko, tetapi itu mencampur semantik, titik kritik potensial lainnya.
Aaron Hall

1
Bukankah lebih mudah untuk menulis dekorator yang mengambil metode dan menggunakan Anda ensure_lowerpada arguemtn pertama (yang selalu kuncinya)? Maka itu akan menjadi jumlah override yang sama, tetapi mereka semua akan menjadi bentuk __getitem__ = ensure_lower_decorator(super(LowerDict, self).__getitem__).
Graipher

1
Terima kasih untuk ini - dapatkan peringatan untuk pop dankeykey bahwa mereka tidak cocok dengan tanda tangan metode kelas dasar.
Mr_and_Mrs_D

1
@ Mr_and_Mrs_D Saya menambahkan implementasi copy- Saya pikir itu harus dilakukan, bukan? Saya pikir itu harus menguji untuk antarmuka - misalnya objek DataFrame panda bukan contoh pemetaan (pada pemeriksaan terakhir) tetapi memiliki item / iteritem.
Aaron Hall

4

Persyaratan saya sedikit lebih ketat:

  • Saya harus menyimpan informasi kasus (string adalah jalur ke file yang ditampilkan kepada pengguna, tetapi ini adalah aplikasi windows sehingga secara internal semua operasi harus peka terhadap huruf besar-kecil)
  • Aku butuh kunci untuk menjadi sekecil mungkin (itu memang membuat perbedaan dalam kinerja memori, dipotong 110 mb dari 370). Ini berarti bahwa cache versi huruf kecil dari kunci bukan opsi.
  • Saya membutuhkan pembuatan struktur data secepat mungkin (sekali lagi membuat perbedaan dalam kinerja, kecepatan kali ini). Saya harus pergi dengan builtin

Pikiran awal saya adalah mengganti kelas Path kami yang kikuk dengan subkelas unicode yang tidak sensitif - tetapi:

  • terbukti sulit untuk mendapatkan yang benar - lihat: Sebuah case string class dengan python
  • ternyata penanganan kunci dikt eksplisit membuat kode verbose dan berantakan - dan rawan kesalahan (struktur dilewatkan ke sana-sini, dan tidak jelas apakah mereka memiliki instance CIStr sebagai kunci / elemen, mudah dilupakan plus some_dict[CIstr(path)]jelek)

Jadi saya akhirnya harus menuliskan dict case yang tidak sensitif. Berkat kode oleh @AaronHall yang dibuat 10 kali lebih mudah.

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

Tersirat vs eksplisit masih menjadi masalah, tetapi begitu debu mengendap, penggantian nama atribut / variabel untuk memulai dengan ci (dan komentar dokter besar yang menjelaskan bahwa ci singkatan dari case-sensitive) Saya pikir ini adalah solusi sempurna - karena pembaca kode harus menyadari sepenuhnya bahwa kita sedang berhadapan dengan struktur data yang mendasari kasus sensitif. Mudah-mudahan ini akan memperbaiki beberapa mereproduksi bug yang sulit, yang saya duga bermuara pada sensitivitas case.

Komentar / koreksi diterima :)


CIstr __repr__harus menggunakan kelas induk __repr__untuk lulus evaluasi (repr (obj)) == tes obj (saya tidak berpikir itu benar sekarang) dan tidak bergantung pada __str__.
Aaron Hall

Juga periksa total_orderingdekorator kelas - yang akan menghilangkan 4 metode dari subkelas unicode Anda. Tapi subclass dict terlihat sangat pintar diimplementasikan. : P
Aaron Hall

Terima kasih @AaronHall - Anda yang menerapkannya: P Re: total pemesanan - Saya sengaja menulis metode yang diuraikan seperti yang disarankan oleh Raymond Hettinger di sini: stackoverflow.com/a/43122305/281545 . Re: repr: Saya ingat pernah membaca komentar (oleh beberapa dev inti IIRC) yang baik, itu tidak benar-benar layak kerumitan untuk mencoba dan membuat repr untuk lulus tes itu (ini merepotkan) - lebih baik fokus pada hal itu se informatif mungkin tetapi tidak lebih)
Mr_and_Mrs_D

Saya akan memungkinkan Anda metode perbandingan yang berlebihan (Anda harus membuat catatan tentang hal itu dalam jawaban Anda), tetapi CIstr.__repr__, dalam kasus Anda , dapat lulus tes repr dengan sangat sedikit kerumitan, dan itu akan membuat debugging jauh lebih baik. Saya juga menambahkan __repr__untuk dict Anda. Saya akan melakukannya dalam jawaban saya untuk menunjukkan.
Aaron Hall

@ Harunall: Saya menambahkan __slots__dalam CIstr - tidak membuat perbedaan dalam kinerja (CIstr tidak dimaksudkan untuk subkelas atau memang digunakan di luar LowerDict, harus menjadi kelas akhir bersarang statis). Masih tidak yakin bagaimana menyelesaikan masalah repr secara elegan (sengatan mungkin berisi kombinasi 'dan "kutipan)
Mr_and_Mrs_D

4

Yang harus Anda lakukan adalah

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

ATAU

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

Contoh penggunaan untuk penggunaan pribadi saya

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

Catatan : hanya diuji di python3


3

Setelah mencoba kedua atas dua saran, saya sudah menetap pada rute menengah tampak teduh untuk Python 2.7. Mungkin 3 lebih waras, tetapi bagi saya:

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

yang benar-benar saya benci, tetapi tampaknya sesuai dengan kebutuhan saya, yaitu:

  • dapat menimpa **my_dict
    • jika Anda mewarisi dari dict, ini mem-bypass kode Anda . coba itu.
    • ini membuat # 2 tidak dapat diterima untuk saya setiap saat , karena ini sangat umum dalam kode python
  • menyamar sebagai isinstance(my_dict, dict)
    • mengesampingkan MutableMapping sendirian, jadi # 1 tidak cukup
    • Saya sungguh-sungguh merekomendasikan # 1 jika Anda tidak membutuhkan ini, itu sederhana dan dapat diprediksi
  • perilaku yang sepenuhnya terkendali
    • jadi saya tidak bisa mewarisi dari dict

Jika Anda perlu membedakan diri dari orang lain, secara pribadi saya menggunakan sesuatu seperti ini (meskipun saya akan merekomendasikan nama yang lebih baik):

def __am_i_me(self):
  return True

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

Selama Anda hanya perlu mengenali diri Anda secara internal, cara ini lebih sulit untuk secara tidak sengaja memanggil __am_i_mekarena nama-python (ini diubah namanya menjadi _MyDict__am_i_medari apa pun yang memanggil di luar kelas ini). Sedikit lebih pribadi daripada _methods, baik dalam praktik maupun secara budaya.

Sejauh ini saya tidak memiliki keluhan, selain dari __class__penimpaan yang terlihat sangat teduh . Saya akan senang mendengar masalah yang orang lain hadapi dengan ini, saya tidak sepenuhnya mengerti konsekuensinya. Tapi sejauh ini saya tidak punya masalah apa pun, dan ini memungkinkan saya untuk memigrasi banyak kode berkualitas menengah di banyak lokasi tanpa perlu perubahan apa pun.


Sebagai bukti: https://repl.it/repls/TraumaticToughCockatoo

Pada dasarnya: salin opsi # 2 saat ini , tambahkan print 'method_name'baris ke setiap metode, lalu coba ini dan perhatikan hasilnya:

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

Anda akan melihat perilaku serupa untuk skenario lainnya. Katakanlah palsu Anda dictadalah pembungkus di sekitar jenis data lain, jadi tidak ada cara yang masuk akal untuk menyimpan data di dalam backing-dict; **your_dictakan kosong, terlepas dari apa yang dilakukan setiap metode lainnya.

Ini berfungsi dengan benar MutableMapping, tetapi begitu Anda mewarisinya dictmenjadi tidak terkendali.


Sunting: sebagai pembaruan, ini telah berjalan tanpa masalah tunggal selama hampir dua tahun sekarang, di beberapa ratus ribu (eh, mungkin beberapa juta) garis-garis rumit, warisan python yang ditunggangi. Jadi saya cukup senang dengan itu :)

Sunting 2: ternyata saya salah menyalin ini atau sesuatu yang sudah lama. @classmethod __class__tidak bekerja untuk isinstancecek - @property __class__tidak: https://repl.it/repls/UnitedScientificSequence


Apa sebenarnya yang Anda maksud dengan " **your_dictakan kosong" (jika Anda subkelas dari dict)? Saya belum melihat masalah dengan dikt membongkar ...
Matt P

Jika Anda benar-benar memasukkan data ke dalam dikte induk (seperti yang dilakukan oleh LowerDict), itu berfungsi - Anda akan mendapatkan data yang disimpan dict tersebut. Jika tidak (misalkan Anda ingin menghasilkan data dengan cepat, seperti {access_count: "tumpukan jejak akses"} yang mengisi setiap kali dibaca), Anda akan melihat bahwa **your_dicttidak menjalankan kode Anda, jadi tidak dapat menampilkan sesuatu yang "spesial". Misalnya Anda tidak dapat menghitung "membaca" karena tidak menjalankan kode penghitungan baca Anda. MutableMapping melakukan pekerjaan untuk ini (menggunakannya jika Anda bisa!), Tapi gagal isinstance(..., dict)jadi saya tidak bisa menggunakannya. perangkat lunak warisan yay.
Groxx

Oke, saya mengerti maksud Anda sekarang. Saya kira saya tidak mengharapkan eksekusi kode dengan **your_dict, tetapi saya merasa sangat menarik yang MutableMappingakan melakukan itu.
Matt P

Ya. Ini diperlukan untuk beberapa hal (misalnya saya sedang mengubah panggilan RPC menjadi apa yang dulunya adalah membaca dict lokal, dan harus melakukannya berdasarkan permintaan untuk Alasan ™), dan tampaknya sangat sedikit orang yang menyadarinya, bahkan jika **some_dictcukup umum. Setidaknya itu terjadi sangat sering di dekorator, jadi jika Anda memiliki apa , Anda segera berisiko perilaku yang tampaknya mustahil jika Anda tidak memperhitungkan itu.
Groxx

Mungkin saya kehilangan sesuatu, tetapi def __class__()triknya tampaknya tidak bekerja dengan Python 2 atau 3, setidaknya untuk contoh kode dalam pertanyaan. Bagaimana cara mendaftar implementasi abc.MutableMapping sebagai subclass dict? (dimodifikasi agar tidak berfungsi di dua versi). Saya ingin isinstance(SpreadSheet(), dict)kembali True.
martineau
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.