Я столкнулся с интересной ситуацией, работая над проектом:
Я создаю класс, который мы можем назвать ValueContainer
, который всегда будет хранить одно значение под атрибутом value
. ValueContainer
, чтобы иметь пользовательскую функциональность, сохранить другие метаданные и т. Д. c., Однако я бы хотел унаследовать все магические / более сложные методы (например, __add__
, __sub__
, __repr__
) от value
. Очевидное решение состоит в том, чтобы вручную реализовать все методы magi c и указать для операции атрибут value
.
Пример определения:
class ValueContainer:
def __init__(self, value):
self.value = value
def __add__(self, other):
if isinstance(other, ValueContainer):
other = other.value
return self.value.__add__(other)
Пример поведения:
vc1 = ValueContainer(1)
assert vc1 + 2 == 3
vc2 = ValueContainer(2)
assert vc1 + vc2 == 3
Однако здесь есть две проблемы.
- Я хочу Унаследуйте ВСЕ методы magi c от
type(self.value)
, что в конечном итоге может составить более 20 различных функций с одинаковыми функциональными возможностями ядра (вызывая super
magi c -метод value
). Это заставляет меня дрожать каждую унцию моего тела, и крик «DRY! DRY! DRY!» value
может быть любого типа. По крайней мере, VERY мне нужно поддерживать как минимум число типов c (int
, float
) и строк. Набор методов magi c и их поведение для чисел и строк уже достаточно различны, чтобы справиться с этой сложной ситуацией. Теперь, добавляя тот факт, что мне нужна возможность хранить пользовательские типы в value
, становится невообразимо реализовать это вручную.
Учитывая эти две вещи, я трачу долго время пробовал разные подходы, чтобы это заработало. Сложность заключается в том, что более сложные методы являются свойствами класса (?), Но value
присваивается экземпляру .
Попытка 1: после назначения value
мы ищем все методы, которые начинаются с __
в классе type(self.value)
, и назначаем методы класса dunder для ValueContainer
этими функциями. Поначалу это казалось хорошим решением, прежде чем понять, что теперь это будет переназначать методы dunder ValueContainer
для всех экземпляров .
Это означает, что когда мы создаем экземпляр:
valc_int = ValueContainer(1)
он будет применять все более сложные методы от int
до ValueContainer
класса . Отлично!
... но если мы затем создадим экземпляр:
valc_str = ValueContainer('a string')
, то все методы dunder для str
будут установлены в классе ValueContainer
, что означает, что valc_int
теперь будет пытаться использовать более сложные методы из str
, что может вызвать проблему при перекрытии.
Попытка 2: Это решение, которое я использую в настоящее время, которое обеспечивает большинство функций, которые мне нужны.
Добро пожаловать, метаклассы.
import functools
def _magic_function(valc, method_name, *args, **kwargs):
if hasattr(valc.value, method_name):
# Get valc.value's magic method
func = getattr(valc.value, method_name)
# If comparing to another ValueContainer, need to compare to its .value
new_args = [arg.value if isinstance(arg, ValueContainer)
else arg for arg in args]
return func(*new_args, **kwargs)
class ValueContainerMeta(type):
blacklist = [
'__new__',
'__init__',
'__getattribute__',
'__getnewargs__',
'__doc__',
]
# Filter magic methods
methods = {*int.__dict__, *str.__dict__}
methods = filter(lambda m: m.startswith('__'), methods)
methods = filter(lambda m: m not in ValueContainer.blacklist, methods)
def __new__(cls, name, bases, attr):
new = super(ValueContainer, cls).__new__(cls, name, bases, attr)
# Set all specified magic methods to our _magic_function
for method_name in ValueContainerMeta.methods:
setattr(new, method_name, functools.partialmethod(_magic_function, method_name))
return new
class ValueContainer(metaclass=ValueContainerMeta):
def __init__(self, value):
self.value = value
Объяснение:
Используя метакласс ValueContainerMeta
, мы перехватываем создание ValueContainer
и переопределяем методы c magi c, которые мы собираем в ValueContainerMeta.methods
атрибут класса. Волхвы c здесь происходят из комбинации нашей _magic_function
функции и functools.partialmethod . Точно так же, как метод dunder, _magic_function
принимает экземпляр ValueContainer
, который вызывается в качестве первого параметра. Мы вернемся к этому через секунду. Следующий аргумент method_name
- это имя строки метода magi c, который мы хотим вызвать (например, '__add__'
). Остальные *args
и **kwargs
будут аргументами, которые будут переданы исходному методу magi c (обычно без аргументов или просто other
, но иногда больше ).
В метаклассе ValueContainerMeta
мы собираем список магических c методов для переопределения и используем partialmethod
, чтобы ввести имя метода для вызова без фактического вызова самого _magic_function
. Изначально я думал, что просто использование functools.partial
послужит цели, так как более сложные методы - это методы класса , но, очевидно, методы magi c так или иначе также связаны с экземплярами , даже если они являются методами класса ? Я до сих пор не полностью понимаю реализацию, но использование functools.partialmethod
решает эту проблему путем , внедряя экземпляр ValueContainer
, вызываемый в качестве первого аргумента в _magic_fuction
(valc
) .
Выход:
def test_magic_methods():
v1 = ValueContainer(1.0)
eq_(v1 + 4, 5.0)
eq_(4 + v1, 5.0)
eq_(v1 - 3.5, -2.5)
eq_(3.5 - v1, 2.5)
eq_(v1 * 10, 10)
eq_(v1 / 10, 0.1)
v2 = ValueContainer(2.0)
eq_(v1 + v2, 3.0)
eq_(v1 - v2, -1.0)
eq_(v1 * v2, 2.0)
eq_(v1 / v2, 0.5)
v3 = ValueContainer(3.3325)
eq_(round(v3), 3)
eq_(round(v3, 2), 3.33)
v4 = ValueContainer('magic')
v5 = ValueContainer('-works')
eq_(v4 + v4, 'magicmagic')
eq_(v4 * 2, 'magicmagic')
eq_(v4 + v5, 'magic-works')
# Float magic methods still work even though
# we instantiated a str ValueContainer
eq_(v1 + v2, 3.0)
eq_(v1 - v2, -1.0)
eq_(v1 * v2, 2.0)
eq_(v1 / v2, 0.5)
В целом, я доволен этим решением, ЗА ИСКЛЮЧЕНИЕМ за то, что вы должны указать, какие имена методов наследовать явно в ValueContainerMeta
. Как вы видите, сейчас Я взял надмножество методов str
и int
magi c. Если возможно, мне бы хотелось, чтобы способ динамически заполнять список имен методов на основе типа value
, но, поскольку это происходит до его создания, я не верю, что это было бы возможно при таком подходе. Если в настоящее время существуют методы magi c для типа, которые не содержатся в расширенном наборе int
и str
, это решение не будет работать с ними.
Хотя это решение это 95% от того, что я ищу, это была такая интересная проблема, что я хотел знать, может ли кто-нибудь еще придумать лучшее решение, которое обеспечивает динамический c выбор методов magi c из типа value
, или имеет оптимизацию / трюки для улучшения других аспектов, или если бы кто-то мог объяснить больше внутренностей о том, как работают маги c методы.