Разбираемся как расширить built-in классы

В Python 3.9 заявлена новая фича: новый синтаксис мерджа двух и более словарей. Если раньше мы писали:

# 1. Basic merge:
merged = d1.copy()
merged.update(d2)

# 2. Unpacking merge:
merged = {**d1, **d2}

То в Python 3.9 можно будет писать так:

merged = d1 | d2

# or

d1 |= d2 # d1 - merged result
PEP 584 -- Add Union Operators To dict
The official home of the Python Programming Language

Вроде круто, вау. Но если задуматься, тут нет рокет сайнаса. Более того, вы можете использовать этот синтаксис уже сегодня, сделав свой полифил и начинать использовать такой синтаксис прямо сейчас в Python 3.8

Как? Сейчас разберемся.

Перегрузка операторов

В Python есть крутая фича - перегрузка операторов, благодаря чему можно творить чудеса с языком и создавать свои DSL. Собственно как реализовать поведение для вышеописанных фич? Берем код из самого пепа:

def __or__(self, other):
    if not isinstance(other, dict):
        return NotImplemented
    new = dict(self)
    new.update(other)
    return new

def __ror__(self, other):
    if not isinstance(other, dict):
        return NotImplemented
    new = dict(other)
    new.update(self)
    return new

def __ior__(self, other):
    dict.update(self, other)
    return self

Пишем свой вариант словаря Python39Dict

 class Python39Dict(dict):
    def __or__(self, other):
        if not isinstance(other, dict):
            return NotImplemented
        new = dict(self)
        new.update(other)
        return new

    def __ror__(self, other):
        if not isinstance(other, dict):
            return NotImplemented
        new = dict(other)
        new.update(self)
        return new

    def __ior__(self, other):
        dict.update(self, other)
        return self

    # end MyDict #


def main():
    d1 = Python39Dict({
        "f1": 123,
        "f2": "abc"
    })
    d2 = Python39Dict({
        "b1": 456,
        "b2": "def"
    })

    d3 = d1 | d2
    print(d3)
    d1 |= d2
    print(d1)
    # end main #


if __name__ == "__main__":
    main()

Супер! Все работает, мы получили на выходе:

{'f1': 123, 'f2': 'abc', 'b1': 456, 'b2': 'def'}
{'f1': 123, 'f2': 'abc', 'b1': 456, 'b2': 'def'}

Но как-то не комильфо же писать каждый раз Python39Dict({}) да? А можем мы добавить эти методы в существующий класс словаря?

Как добавить метод к существующему классу?

Наченм разбираться с базовых вещей, добавим метод к существующему классу:

class A:
    pass

a = A()

def foo(self):
    print('hello world!')

setattr(A, 'foo', foo)
a.foo() # hello world!

Так, значит мы умеем добавлять методы к существующим классам. Супер, пробуем:


setattr(dict, '__or__', '__or__')

И тут нас ждет неудача, мы получим ошибку:


TypeError: can't set attributes of built-in/extension type 'dict'

Хм, а что же делать?

Builtins

Находим, что существует встроенный модуль builtins. Он позволяет модифицировать встроенные объекты.

import builtins


class Python39Dict(dict):
    def __or__(self, other):
        if not isinstance(other, dict):
            return NotImplemented
        new = dict(self)
        new.update(other)
        return new

    def __ror__(self, other):
        if not isinstance(other, dict):
            return NotImplemented
        new = dict(other)
        new.update(self)
        return new

    def __ior__(self, other):
        dict.update(self, other)
        return self

    # end MyDict #


__builtins__.dict = Python39Dict


def main():
    d1 = dict({
        "f1": 123,
        "f2": "abc"
    })
    d2 = dict({
        "b1": 456,
        "b2": "def"
    })

    d3 = d1 | d2
    print(d3)
    d1 |= d2
    print(d1)
    # end main #


if __name__ == "__main__":
    main()

Уже лучше, но есть но! Мы должны явно указывать при создании что мы используем словарь и приходится писать слово dict. А можем ли мы и это улучшить? Чтобы словари выглядели нативно?

Запретный плод - forbiddenfruit

Да, запретный плод сладок и его не следует трогать и злоупотреблять, но если очень надо...

Устанавливаем модуль forbiddenfruit и пишем следующий код:

from forbiddenfruit import curse


def dict_or(self, other):
    if not isinstance(other, dict):
        return NotImplemented
    new = dict(self)
    new.update(other)
    return new


def dict_ior(self, other):
    dict.update(self, other)
    return self


curse(dict, "__or__", dict_or)
curse(dict, "__ior__", dict_ior)


def main():
    d1 = {
        "f1": 123,
        "f2": "abc"
    }
    d2 = {
        "b1": 456,
        "b2": "def"
    }

    d3 = d1 | d2
    print(d3)
    d1 |= d2
    print(d1)
    # end main #


if __name__ == "__main__":
    main()

Воу воу! Работает! Оформляем как модуль, выносим код в файл python39dict и теперь можем подключить новый синтаксис слияния словарей к проекту:

import python39dict

d1 = {
    "f1": 123,
    "f2": "abc"
}
d2 = {
    "b1": 456,
    "b2": "def"
}

d3 = d1 | d2
print(d3)
d1 |= d2
print(d1)

Ну вот и все. Как видите, вы можете пробовать расширять язык, даже если в нем нет каких-то готовых фич и некоторые фичи даже предлагать сообществу, которые могут в итоге попасть в спецификацию и в язык. Конечно слияние объектов в Python 3.9 будет реализовано на уровне языка в коде на С, а не через питон, и работать будет явно быстрее и безопаснее. Но вы можете пробовать добавлять новые фичи таким вот образом, пока ждете выхода новой версии языка.

Каждый ваш лайк - это мотивация мне продолжать писать статьи и рассказывать вам интересные вещи из мира программирования. Если статья была полезна, нажмите кнопочку, пожалуйста :)