[python] dict을“완벽하게”재정의하는 방법은 무엇입니까?

가능한 한 dict 의 하위 클래스를 “완벽하게”만들려면 어떻게 해야합니까? 최종 목표는 단순한를 가지고있다 DICT 키가 소문자되는합니다.

이 작업을 수행하기 위해 재정의 할 수있는 몇 가지 작은 기본 요소가 있어야하는 것처럼 보이지만 모든 연구와 시도에 따르면 그렇지 않은 것처럼 보입니다.

  • 내가하면 오버라이드 (override) __getitem__/__setitem__ 다음 get/ set일을하지 않습니다. 어떻게 작동시킬 수 있습니까? 반드시 개별적으로 구현할 필요는 없습니까?

  • 피클 링이 작동하지 못하게하고 __setstate__있습니까? 등 을 구현해야 합니까?

  • 나는 마십시오 필요 repr, update그리고__init__ ?

  • 난 그냥해야 mutablemapping 사용 (이 사람이 사용해서는 안 보인다 UserDict
    거나 DictMixin)? 그렇다면 어떻게? 문서가 정확하게 깨달은 것은 아닙니다.

여기에 첫 번째 시도가 있으며 get()작동하지 않으며 다른 많은 사소한 문제가 있습니다.

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



답변

모듈 에서 ABC (Abstract Base Classes)를 사용하여 dict매우 쉽게 동작하는 객체를 작성할 수 있습니다 . 방법을 놓친 경우에도 알려주므로 ABC를 종료하는 최소 버전은 다음과 같습니다.collections.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

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

나는 dict직접 하위 클래스 (또는 다른 내장)를 사용 하지 않을 것 입니다. 실제로 원하는 것은 의 인터페이스를 구현dict 하기 때문에 종종 의미가 없습니다 . 이것이 바로 ABC의 목적입니다.


답변

가능한 한 dict의 하위 클래스를 “완벽하게”만들려면 어떻게해야합니까?

최종 목표는 키가 소문자로되어있는 간단한 설명을하는 것입니다.

  • __getitem__/을 재정의하면 __setitem__get / set이 작동하지 않습니다. 어떻게 작동하게합니까? 반드시 개별적으로 구현할 필요는 없습니까?

  • 피클 링이 작동하지 못하게하고 __setstate__있습니까? 등 을 구현해야
    합니까?

  • repr, update 및가 필요 __init__합니까?

  • 방금 사용해야합니까 mutablemapping( UserDict
    또는 사용하지 않아야하는 것 같습니다 DictMixin)? 그렇다면 어떻게? 문서가 정확하게 깨달은 것은 아닙니다.

허용되는 대답은 첫 번째 접근법이지만 문제가 있기 때문에 실제로 서브 클래스를 대체하는 대안을 아무도 다루지 않았으므로 dict여기서 할 것입니다.

허용되는 답변에 어떤 문제가 있습니까?

이것은 나에게 다소 간단한 요청처럼 보입니다.

가능한 한 dict의 하위 클래스를 “완벽하게”만들려면 어떻게해야합니까? 최종 목표는 키가 소문자로되어있는 간단한 설명을하는 것입니다.

허용 된 답변은 실제로 서브 클래스가 아니며이 dict테스트는 실패합니다.

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

이상적으로 모든 유형 검사 코드는 예상되는 인터페이스 또는 추상 기본 클래스를 테스트하지만 데이터 객체가 테스트중인 함수에 전달되고 dict해당 함수를 “수정”할 수없는 경우이 코드 실패합니다.

다른 퀴즈는 다음과 같습니다.

  • 허용 된 답변에도 classmethod가 없습니다 fromkeys.
  • 허용 된 답변에는 중복이 __dict__있으므로 메모리에서 더 많은 공간을 차지합니다.

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

실제로 서브 클래 싱 dict

상속을 통해 dict 메소드를 재사용 할 수 있습니다. 키가 문자열 인 경우 딕셔너리로 ​​키가 전달되도록하는 인터페이스 레이어를 작성하기 만하면됩니다.

__getitem__/을 재정의하면 __setitem__get / set이 작동하지 않습니다. 어떻게 작동하게합니까? 반드시 개별적으로 구현할 필요는 없습니까?

글쎄, 그것들을 각각 개별적으로 구현하는 것이이 접근법의 단점과 사용의 단점 MutableMapping(허용 된 답변 참조)이지만 실제로 그렇게 많은 것은 아닙니다.

먼저 파이썬 2와 3의 차이점을 배제하고 싱글 톤 ( _RaiseKeyError)을 만들어 실제로 인수를 얻는 지 dict.pop확인하고 문자열 키가 소문자인지 확인하는 함수를 만들어 봅시다 .

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

이제 우리는 구현합니다-나는 super이 코드가 파이썬 2와 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__())

우리는 어떤 방법 또는 특별한 방법에 대한 거의 보일러 플레이트 방식을 사용 참조하는 핵심하지만, 그렇지 않으면 상속에 의해, 우리는 방법을 얻을 : len, clear, items, keys, popitem, 및 values무료. 이를 위해서는 신중한 생각이 필요했지만 이것이 효과가 있다는 것은 사소한 일입니다.

( haskeyPython 2에서는 더 이상 사용되지 않으며 Python 3에서는 제거되었습니다.)

사용법은 다음과 같습니다.

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

피클 링이 작동하지 못하게하고 __setstate__있습니까? 등 을 구현해야
합니까?

산세

그리고 dict 서브 클래스 피클은 잘 작동합니다.

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

repr, update 및가 필요 __init__합니까?

우리는 update및을 정의 __init__했지만 __repr__기본적으로 아름답 습니다.

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

그러나 __repr__코드의 디버깅 가능성을 향상시키기 위해 를 작성하는 것이 좋습니다 . 이상적인 테스트는 eval(repr(obj)) == obj입니다. 코드를 작성하기 쉬운 경우 다음을 강력히 권장합니다.

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

보시다시피, 이는 동등한 객체를 재생성하는 데 필요한 것입니다. 이것은 로그 나 역 추적에 나타날 수 있습니다.

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

결론

방금 사용해야합니까 mutablemapping( UserDict
또는 사용하지 않아야하는 것 같습니다 DictMixin)? 그렇다면 어떻게? 문서가 정확하게 깨달은 것은 아닙니다.

예,이 코드는 몇 줄 더 있지만 포괄적으로 작성되었습니다. 내 첫 번째 성향은 받아 들여진 대답을 사용하는 것이며, 문제가 있으면 조금 더 복잡하고 내 인터페이스를 올바르게 얻는 데 도움이되는 ABC가 없기 때문에 내 대답을 살펴볼 것입니다.

조기 검색은 성능 검색의 복잡성을 증가시킵니다.
MutableMapping더 간단합니다. 따라서 즉각적인 우위를 점할 수 있습니다. 그럼에도 불구하고 모든 차이점을 설명하기 위해 비교하고 대조합시다.

비슷한 사전을 collections모듈 에 넣는 푸시가 있었지만 거부되었습니다 . 아마도 이것을 대신해야합니다.

my_dict[transform(key)]

훨씬 쉽게 디버깅 할 수 있어야합니다.

비교와 대조

MutableMapping(누락 된 fromkeys)으로 구현 된 6 개의 인터페이스 함수 와 dict서브 클래스로 11 개의 인터페이스 함수가 ​​있습니다 . 나는 구현할 필요가 없다 __iter__거나 __len__, 대신에 내가 구현해야한다 get, setdefault, pop, update, copy, __contains__, 그리고 fromkeys내가 그 구현의 대부분을 상속을 사용할 수 있기 때문에, 그러나 이들은 매우 사소한 -.

MutableMapping구현은 파이썬 dict에서 C로 구현 하는 것을 구현하므로 dict하위 클래스가 더 성능 이 좋을 것으로 기대합니다 .

우리 __eq__는 두 가지 접근 방식에서 모두 자유 를 얻습니다 . 두 가지 접근 방식은 다른 dict가 모두 소문자 인 경우에만 평등을 가정하지만 dict하위 클래스는 더 빨리 비교할 것이라고 생각합니다 .

요약:

  • MutableMapping버그에 대한 기회는 적지 만 서브 클래 싱 은 더 간단하지만 더 느리며 더 많은 메모리를 필요로하며 (중복 된 dict 참조) 실패isinstance(x, dict)
  • 서브 클래 싱 dict은 더 빠르고 메모리를 덜 사용하며 통과 isinstance(x, dict)하지만 구현하기가 더 복잡합니다.

어느 것이 더 완벽합니까? 그것은 당신의 완벽한 정의에 달려 있습니다.


답변

내 요구 사항은 조금 더 엄격했습니다.

  • 케이스 정보를 유지해야했습니다 (문자열은 사용자에게 표시되는 파일의 경로이지만 Windows 응용 프로그램이므로 내부적으로 모든 작업은 대소 문자를 구분하지 않아야 함)
  • 나는 (그것을 가능한 한 작게 할 키를 필요로 했다 (370)에서 1백10메가바이트 떨어져 다진 메모리 성능의 차이를 만들). 이는 소문자 버전의 키 캐싱이 옵션이 아님을 의미했습니다.
  • 가능한 빨리 데이터 구조를 만들어야했습니다 (이번에는 성능의 차이를 만들어 속도를 높였습니다). 나는 내장과 함께 가야했다

내 초기 생각은 대소 문자를 구분하지 않는 유니 코드 하위 클래스 대신 clunky Path 클래스를 대체하는 것이 었습니다.

  • 그것을 올바르게 얻기가 어렵다는 것을 알았습니다- 파이썬에서 대소 문자를 구분하지 않는 문자열 클래스를 참조하십시오
  • 명백한 dict 키 처리는 코드를 장황하고 어수선하게 만들고 오류가 발생하기 쉽습니다 (구조가 계속 전달되고 CIStr 인스턴스가 키 / 요소로 존재하는지 잊어 버릴 수 some_dict[CIstr(path)]있고 추악한 것이 확실하지 않음 )

그래서 나는 마침내 대소 문자를 구분하지 않는 dict을 적어야했습니다. @AaronHall의 코드 덕분에 10 배 더 쉬워졌습니다.

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

암시 적 대 명시 적 문제는 여전히 문제이지만 일단 먼지가 정착되면 ci로 시작하도록 속성 / 변수의 이름을 바꾸면 ci가 대소 문자를 구분하지 않는다는 것을 설명하는 큰 뚱뚱한 의사 의견이 있습니다. 대소 문자를 구분하지 않는 기본 데이터 구조를 처리하고 있음을 충분히 알고 있어야합니다. 이렇게하면 버그를 재현하기 어려운 일부 버그를 수정하여 대소 문자를 구분할 수 있습니다.

의견 / 수정 환영 🙂


답변

당신이 할 일은

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

또는

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

개인적 사용을위한 샘플 사용법

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

참고 : python3에서만 테스트되었습니다.


답변

상위 두 가지 제안을 모두 시도한 후 파이썬 2.7에 대한 그늘진 중간 경로를 정했습니다. 어쩌면 3은 더 신기하지만 나를 위해 :

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

정말 싫어하지만 내 요구에 맞는 것 같습니다.

  • 무시할 수있다 **my_dict
    • 당신이 상속하는 경우 dict, 이 코드를 무시합니다 . 사용해보십시오.
    • 이것은 파이썬 코드에서 매우 일반적 이기 때문에 # 2 를 항상 받아 들일 수 없게 만듭니다 .
  • 가장 무도회 isinstance(my_dict, dict)
    • MutableMapping 만 배제하므로 # 1 로는 충분하지 않습니다
    • 필요하지 않은 경우 # 1을 진심으로 추천합니다 . 간단하고 예측 가능합니다.
  • 완전히 제어 가능한 행동
    • 그래서 나는 상속받을 수 없다 dict

다른 사람들과 구별해야 할 필요가 있다면 개인적으로 다음과 같은 것을 사용합니다 (더 나은 이름을 추천하지만).

def __am_i_me(self):
  return True

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

내부적으로 만 자신을 인식 해야하는 한,이 방법 __am_i_me으로 파이썬의 이름 문명으로 인해 실수로 호출하기가 더 어렵습니다 ( _MyDict__am_i_me이 클래스 외부에서 호출되는 이름으로 변경됨 ). _method실제로 나 문화적으로 s 보다 약간 더 사적인 것 .

지금까지 나는 진지해 보이는 __class__재정의를 제외하고는 불만이 없습니다 . 나는 거라고 흥분 다른 사람이 생각 발생할 것을, 나는 충분히 결과를 이해하지 않는 문제 듣고. 그러나 지금까지 나는 아무런 문제가 없었기 때문에 변경하지 않고도 많은 위치에서 많은 중간 품질 코드를 마이그레이션 할 수있었습니다.


증거로 : https://repl.it/repls/TraumaticToughCockatoo

기본적으로 현재 # 2 옵션을 복사 하고print 'method_name' 모든 메소드 에 행을 추가 한 후 시도하여 출력을보십시오.

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

다른 시나리오에서도 비슷한 동작이 나타납니다. 가짜 dict가 다른 데이터 유형을 둘러싼 래퍼 라고 말하면 데이터를지지하는 데 사용할 합리적인 방법이 없습니다. **your_dict다른 모든 방법에 관계없이 비어 있습니다.

이것은에 대해 올바르게 작동 MutableMapping하지만 상속하자마자 dict제어 할 수 없게됩니다.


편집 : 업데이트로, 이것은 거의 2 년 동안 단일 문제없이 복잡하고 레거시 타기 된 파이썬 라인에서 수십만 라인에 실행되었습니다. 그래서 나는 그것에 매우 만족합니다 🙂

편집 2 : 분명히 나는 ​​이것 또는 오래 전에 잘못 복사했습니다. 확인을 @classmethod __class__위해 작동하지 않습니다-https : //repl.it/repls/UnitedScientificSequenceisinstance@property __class__


답변