【发布时间】: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 多个不同的函数,都具有相同的核心功能(调用value的super魔法方法)。这让我的每一寸身体都在颤抖,并大喊“干!干!干!” -
value可以是任何类型。 非常至少我需要至少支持数字类型(int、float)和字符串。数字和字符串的魔法方法集及其行为已经不同,足以使这种情况难以处理。现在,当我希望能够在value中存储自定义类型这一事实时,手动实现它变得有些难以想象。
考虑到这两点,我花了 长时间 时间尝试不同的方法来实现这一点。困难在于 dunder 方法是类属性(?),但 value 被分配给实例。
尝试一:value赋值后,查找type(self.value)类上所有以__开头的方法,将ValueContainer上的类dunder方法赋值为这些函数。起初这似乎是一个很好的解决方案,但在意识到现在这样做会为所有实例重新分配 ValueContainer 的 dunder 方法之前,这似乎是一个很好的解决方案。
这意味着当我们实例化时:
valc_int = ValueContainer(1)
它将应用从int 到ValueContainer 类 的所有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 中明确指定要继承的方法名称。 如您所见,现在我已经采用了str 和int 魔术方法的超集。如果可能的话,我希望有一种方法可以根据value 的类型动态填充方法名称列表,但由于这是在实例化之前发生的,我不相信这种方法是可能的。如果当前类型的魔法方法不包含在 int 和 str 的超集中,则此解决方案不适用于这些方法。
虽然这个解决方案是我正在寻找的 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% 能够从值的类型中动态选择要添加的方法,包括那些可能不在
int和str类中的方法。看看是否有人对此解决方案有很好的解释或改进。
标签: python inheritance metaclass magic-methods