Создание упорядоченного счетчика

Я читал, как работает super(). Я наткнулся на этот рецепт, который демонстрирует, как создать Упорядоченный Счетчик:

from collections import Counter, OrderedDict

class OrderedCounter(Counter, OrderedDict):
     'Counter that remembers the order elements are first seen'
     def __repr__(self):
         return '%s(%r)' % (self.__class__.__name__,
                            OrderedDict(self))
     def __reduce__(self):
         return self.__class__, (OrderedDict(self),)

Например:

oc = OrderedCounter('adddddbracadabra')

print(oc)

OrderedCounter(OrderedDict([('a', 5), ('d', 6), ('b', 2), ('r', 2), ('c', 1)]))

Кто-то может объяснить, как это волшебным образом работает?

Это также появляется в документации по Python.

Ответ 1

OrderedCounter приведен в качестве примера в документации OrderedDict и работает без необходимости переопределения любых методов:

class OrderedCounter(Counter, OrderedDict):
    pass

Когда вызывается метод класса, Python должен найти правильный метод для выполнения. Существует определенный порядок поиска иерархии классов, который называется "порядок разрешения методов" или mro. Mro хранится в атрибуте __mro__:

OrderedCounter.__mro__

(<class '__main__.OrderedCounter'>, <class 'collections.Counter'>, <class 'collections.OrderedDict'>, <class 'dict'>, <class 'object'>)

Когда экземпляр OrderedDict вызывает __setitem__(), он ищет классы в следующем порядке: OrderedCounter, Counter, OrderedDict (где он найден). Таким образом, оператор наподобие oc['a'] = 0 конечном итоге вызывает OrderedDict.__setitem__().

Напротив, __getitem__ не переопределяется ни одним из подклассов в mro, поэтому count = oc['a'] обрабатывается dict.__getitem__().

oc = OrderedCounter()    
oc['a'] = 1             # this call uses OrderedDict.__setitem__
count = oc['a']         # this call uses dict.__getitem__

Более интересная последовательность вызовов происходит для такого оператора, как oc.update('foobar'). Сначала Counter.update(). Код для Counter.update() использует self [elem], который превращается в вызов OrderedDict.__setitem__(). И код для этого вызывает dict.__setitem__().

Если базовые классы меняются местами, это больше не работает. Потому что mro отличается, и вызываются неправильные методы.

class OrderedCounter(OrderedDict, Counter):   # <<<== doesn't work
    pass

Более подробную информацию о MRO можно найти в 2.3 Python документации.