从实例属性动态继承所有 Python 魔术方法

Dynamically inherit all Python magic methods from an instance attribute

我在做一个项目时 运行 遇到了一个有趣的情况:

我正在构造一个 class,我们可以将其称为 ValueContainer,它始终将单个值存储在 value 属性下。 ValueContainer 具有自定义功能、保留其他元数据等,但是我想继承所有 magic/dunder 方法(例如 __add____sub____repr__ ) 来自 value。显而易见的解决方案是手动实现所有魔术方法并将操作指向 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. 我想从 type(self.value) 继承所有魔法方法,最终可能有 20 多个不同的函数,所有函数都具有相同的核心功能(调用 super 魔法方法 value).这让我浑身颤抖,大喊"DRY! DRY! DRY!"
  2. value 可以是任何类型。在 VERY 至少我需要至少支持数字类型(intfloat)和字符串。魔术方法集及其对数字和字符串的行为已经不同到足以使这种情况难以处理。现在,当添加我希望能够在 value 中存储自定义类型的事实时,手动实现变得有些难以想象。

考虑到这两点,我花了 很长 的时间尝试不同的方法来实现它。困难的部分来自于 dunder 方法是 class 属性 (?),但是 value 被分配给一个实例 .

尝试一:value赋值后,我们在classtype(self.value)上查找所有以__开头的方法,赋值给[=150] =] ValueContainer 上的 dunder 方法是这些功能。起初这似乎是一个很好的解决方案,但在意识到这样做现在 为所有实例 .

重新分配 ValueContainer 的 dunder 方法之前

这意味着当我们实例化时:

valc_int = ValueContainer(1)

它将应用从 intValueContainer class 的所有 dunder 方法。太棒了!

...但是如果我们接着实例化:

valc_str = ValueContainer('a string')

str 的所有 dunder 方法都将在 class ValueContainer 上设置,这意味着 valc_int 现在会尝试使用 str 中的 dunder 方法,在重叠时可能会导致问题。

尝试 2:这是我目前使用的解决方案,它实现了我所追求的大部分功能。

欢迎,Metaclasses.

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元class,我们拦截ValueContainer的创建,并覆盖我们在ValueContainerMeta.methods[=150]上收集的特定魔法方法=] 属性。这里的魔法来自我们的 _magic_function 函数和 functools.partialmethod. Just like a dunder method, _magic_function takes the ValueContainer instance it is being called on as the first parameter. We'll come back to this in a second. The next argument, method_name, is the string name of the magic method we want to call ('__add__' for example). The remaining *args and **kwargs will be the arguments that would be passed to the original magic method (generally no arguments or just other, but sometimes more) 的组合。

ValueContainerMeta metaclass中,我们收集了一个要覆盖的魔法方法列表,并使用partialmethod注入要调用的方法名,而不实际调用_magic_function 本身。最初我虽然只使用 functools.partial 就可以达到目的,因为 dunder 方法是 class 方法 ,但显然魔法方法不知何故 也绑定到实例 尽管它们是 class 方法?我仍然不完全理解实现,但是使用 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中明确指定要继承的方法名称。 如您所见,现在我采用了 strint 魔法方法的超集。如果可能的话,我希望有一种方法可以根据 value 的类型动态填充方法名称列表,但是由于这是在实例化之前发生的,所以我认为这种方法不可能实现。如果现在 intstr 的超集中不包含某个类型的魔术方法,则此解决方案不适用于这些方法。

虽然这个解决方案是我正在寻找的 95%,但这是一个非常有趣的问题,我想知道是否还有其他人可以提出更好的解决方案,实现动态选择来自 value 类型的魔术方法,或具有 optimizations/tricks 用于改进其他方面,或者如果有人可以解释更多魔术方法如何工作的内部结构。

如您所见,

  1. 魔术方法是在 class 上发现的,而不是在实例上发现的,并且
  2. 在 class 创建之前,您无权访问包装的 value

考虑到这一点,我认为不可能强制相同 class 的实例根据包装值类型不同地重载运算符。

一种解决方法是动态创建和缓存 ValueContainer 子classes。例如,

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')"

Igor 提供了一段非常好的代码。您可能希望增强方法工厂以支持非二进制操作,但除此之外,我认为使用黑名单在维护方面并不理想。您现在必须仔细检查所有可能的特殊方法,并在 python.

的每个新版本中再次检查它们是否有可能是新的。

的基础上,我建议另一种利用多重继承的方法。从包装类型和值容器继承使得容器与包装类型几乎完全兼容,同时包括来自通用容器的公共服务。作为奖励,这种方法使代码更加简单(并且根据 Igor 关于 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)

请注意,与引用容器中的输入对象的 Sam 和 Igor 的方法不同,此方法创建一个用输入对象初始化的新子类对象。这对基本值没问题,但可能会对其他类型造成不良影响,具体取决于它们的构造函数如何处理副本。