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

Posted

技术标签:

【中文标题】从实例属性动态继承所有 Python 魔术方法【英文标题】:Dynamically inherit all Python magic methods from an instance attribute 【发布时间】:2020-05-19 18:04:58 【问题描述】:

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

我正在构建一个类,我们可以称之为ValueContainer,它总是会在value 属性下存储一个值。 ValueContainer 拥有自定义功能,保留其他元数据等,但是我想从 value 继承所有的魔法/dunder 方法(例如 __add____sub____repr__)。显而易见的解决方案是手动实现所有魔术方法并将操作指向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 

但是,这里有两个问题。

    我想从type(self.value) 继承所有魔法方法,这最终可能是 20 多个不同的函数,都具有相同的核心功能(调用 valuesuper 魔法方法)。这让我的每一寸身体都在颤抖,并大喊“干!干!干!” value 可以是任何类型。 非常至少我需要至少支持数字类型(intfloat)和字符串。数字和字符串的魔法方法集及其行为已经不同,足以使这种情况难以处理。现在,当我希望能够在 value 中存储自定义类型这一事实时,手动实现它变得有些难以想象。

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

尝试一:value赋值后,查找type(self.value)类上所有以__开头的方法,将ValueContainer上的类dunder方法赋值为这些函数。起初这似乎是一个很好的解决方案,但在意识到现在这样做会为所有实例重新分配 ValueContainer 的 dunder 方法之前,这似乎是一个很好的解决方案。

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

valc_int = ValueContainer(1)

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

...但是如果我们再实例化:

valc_str = ValueContainer('a string')

str 的所有 dunder 方法都将设置在 class ValueContainer,这意味着 valc_int 现在会尝试使用来自 str 的 dunder 方法,当有重叠。

尝试 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 的创建,并覆盖我们在ValueContainerMeta.methods 类属性上收集的特定魔术方法。这里的魔力来自我们的_magic_function 函数和functools.partialmethod 的组合。就像 dunder 方法一样,_magic_function 将调用它的 ValueContainer 实例作为第一个参数。我们稍后再讨论这个问题。下一个参数method_name 是我们要调用的魔术方法的字符串名称(例如'__add__')。剩余的*args**kwargs 将是传递给原始魔术方法的参数(通常没有参数或只有other,但sometimes more)。

ValueContainerMeta 元类中,我们收集要覆盖的魔术方法列表,并使用partialmethod 注入要调用的方法名称,而无需实际调用_magic_function 本身。最初我虽然只使用functools.partial 就可以达到目的,因为dunder 方法是类方法,但显然魔术方法以某种方式也绑定到实例,即使它们是类方法?我仍然不完全理解实现,但使用functools.partialmethod 通过注入ValueContainer 实例被称为_magic_fuction (valc) 中的第一个参数来解决此问题@ (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 的类型,或者具有改进其他方面的优化/技巧,或者是否有人可以解释更多魔术方法的内部原理。

【问题讨论】:

魔术方法不是“以某种方式绑定到实例”。每当您调用实例方法时,都会将此实例作为第一个参数self 传递给被调用的方法。但是您将int.__add__ 暴露为Container.__add__ - int.__add__ 的实现需要int 或其子类为self。它得到Container 实例,因为Container(5) + 4 被解析为Container(5).__add__(4),这相当于Container.__add__(Container(5), 4)...这就是为什么你必须实现_magic_function() @ElmoVanKielmo 奇怪的是 __add__ 不存在于实例字典中,它只存在于类字典中。 您的具体问题是什么——解释这些怪癖或实施那些“5%”?那些“5%”是什么?该解决方案似乎已经完成了您想要的一切。 @ivan_pozdeev 我提到的 5% 能够从值的类型中动态选择要添加的方法,包括那些可能不在 intstr 类中的方法。看看是否有人对此解决方案有很好的解释或改进。 【参考方案1】:

正如您正确识别的那样,

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

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

一种解决方法是动态创建和缓存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')"

【讨论】:

【参考方案2】:

Igor 提供了一段非常好的代码。您可能希望增强方法工厂以支持非二进制操作,但除此之外,在我看来,使用黑名单在维护方面并不理想。您现在必须仔细检查所有可能的特殊方法,并在每个新版本的 python 中再次检查它们是否可能有新的方法。

在Igor's code 的基础上,我建议使用多继承的另一种方法。从包装类型和值容器中继承使得容器与包装类型几乎完全兼容,同时包括来自通用容器的公共服务。作为奖励,这种方法使代码更加简单(使用 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 的方法不同,它们引用容器中的输入对象,此方法创建一个使用输入对象初始化的新子类对象。对于基本值来说没问题,但对其他类型可能会造成不良影响,具体取决于它们的构造函数如何处理副本。

【讨论】:

更新:颠倒基类的顺序以使repr() 正常工作 好主意!我完全同意,黑名单太脆弱了,虽然 OP 的 _magic_function 实现处理一元运算符,但我认为这种方法要好得多。 我也推荐@functools.lru_cache。我在写答案时忘记了它,它比我的手动缓存更好,并使您对 type_container 的实现成为单行。 我不知道 lru 缓存功能,这是一个巧妙的增强,感谢这个很酷的提示!答案已更新,感谢团队合作:)

以上是关于从实例属性动态继承所有 Python 魔术方法的主要内容,如果未能解决你的问题,请参考以下文章

PHP高级特性魔术方法/魔术常量

Python魔术方法

python 魔术方法

python常用魔术方法概览

Python中类的特殊属性和魔术方法

python魔术方法