Разбираемся как расширить 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

Вроде круто, вау. Но если задуматься, тут нет рокет сайнаса. Более того, вы можете использовать этот синтаксис уже сегодня, сделав свой полифил и начинать использовать такой синтаксис прямо сейчас в 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 будет реализовано на уровне языка в коде на С, а не через питон, и работать будет явно быстрее и безопаснее. Но вы можете пробовать добавлять новые фичи таким вот образом, пока ждете выхода новой версии языка.
Каждый ваш лайк - это мотивация мне продолжать писать статьи и рассказывать вам интересные вещи из мира программирования. Если статья была полезна, нажмите кнопочку, пожалуйста :)