Динамически наследовать все Python маги c методы от атрибута экземпляра - PullRequest
4 голосов
/ 04 февраля 2020

Я столкнулся с интересной ситуацией, работая над проектом:

Я создаю класс, который мы можем назвать 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 

Однако здесь есть две проблемы.

  1. Я хочу Унаследуйте ВСЕ методы magi c от type(self.value), что в конечном итоге может составить более 20 различных функций с одинаковыми функциональными возможностями ядра (вызывая super magi c -метод value). Это заставляет меня дрожать каждую унцию моего тела, и крик «DRY! DRY! DRY!»
  2. 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 методы.

Ответы [ 2 ]

4 голосов
/ 04 февраля 2020

Как вы правильно определили, методы

  1. magi c обнаружены в классе, а не в экземпляре, и
  2. у вас нет доступа к завернутым value до создания класса.

Имея это в виду, я думаю, что невозможно заставить экземпляры одного и того же класса перегружать операторы по-разному в зависимости от типа упакованного значения.

One Обходной путь должен динамически создавать и кэшировать ValueContainer подклассы. Например,

import inspect

blacklist = frozenset([
    '__new__',
    '__init__',
    '__getattribute__',
    '__getnewargs__',
    '__doc__',
    '__setattr__',
    '__str__',
    '__repr__',
])

# container type superclass
class ValueContainer:
    def __init__(self, value):
        self.value = value

    def __repr__(self):
        return '{}({!r})'.format(self.__class__.__name__, self.value)

# produce method wrappers
def method_factory(method_name):
    def method(self, other):
        if isinstance(other, ValueContainer):
            other = other.value
        return getattr(self.value, method_name)(other)
    return method

# create and cache container types (instances of ValueContainer)
type_container_cache = {}
def type_container(type_, blacklist=blacklist):
    try:
        return type_container_cache[type_]
    except KeyError:
        pass

    # e.g. IntContainer, StrContainer
    name = f'{type_.__name__.title()}Container'
    bases = ValueContainer,
    method_names = {
        method_name for method_name, _ in inspect.getmembers(type_, inspect.ismethoddescriptor) if
        method_name.startswith('__') and method_name not in blacklist
    }

    result = type_container_cache[type_] = type(name, bases, {
        n: method_factory(n) for n in method_names})
    return result

# create or lookup an appropriate ValueContainer
def value_container(value):
    cls = type_container(type(value))
    return cls(value)

Затем можно использовать фабрику value_container.

i2 = value_container(2)
i3 = value_container(3)
assert 2 + i2 == 4 == i2 + 2
assert repr(i2) == 'IntContainer(2)'
assert type(i2) is type(i3)

s = value_container('a')
assert s + 'b' == 'ab'
assert repr(s) == "StrContainer('a')"
3 голосов
/ 04 февраля 2020

Игорь предоставил очень хороший кусок кода. Возможно, вы захотите усовершенствовать фабрику методов для поддержки недвоичных операций, но, на мой взгляд, использование черного списка не является идеальным с точки зрения обслуживания. Вы должны внимательно просмотреть все возможные специальные методы и проверять их снова на возможные новые с каждым новым выпуском python.

Построение поверх кода Игоря , я предлагаю другой способ использования множественного наследования. Наследование как от упакованного типа, так и от контейнера значений позволяет почти полностью совместить контейнеры с упакованными типами, включая общие службы из контейнера generi c. В качестве бонуса этот подход делает код еще проще (и даже лучше с подсказкой Игоря о lru_cache).

import functools

# container type superclass
class ValueDecorator:
    def wrapped_type(self):
        return type(self).__bases__[1]

    def custom_operation(self):
        print('hey! i am a', self.wrapped_type(), 'and a', type(self))

    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, super().__repr__())

# create and cache container types (e.g. IntContainer, StrContainer)
@functools.lru_cache(maxsize=16)
def type_container(type_):
    name = f'{type_.__name__.title()}Container'
    bases = (ValueDecorator, type_)
    return type(name, bases, {})

# create or lookup an appropriate container
def value_container(value):
    cls = type_container(type(value))
    return cls(value)

Обратите внимание, что в отличие от методов Сэма и Игоря, которые ссылаются на объект ввода в контейнере этот метод создает новый подклассный объект, инициализированный с входным объектом. Это хорошо для базовых значений c, но может вызвать нежелательные эффекты для других типов, в зависимости от того, как их конструктор работает с копией.

...