如何在 python 中设置魔术方法的默认行为?

How to set default behaviors of magic methods in python?

假设我想要一个用于 numpy 数组的包装器 class Image。我的目标是让它表现得像一个二维数组,但有一些额外的功能(这在这里并不重要)。我这样做是因为继承numpy数组比较麻烦

import numpy as np


class Image(object):
    def __init__(self, data: np.ndarray):
        self._data = np.array(data)

    def __getitem__(self, item):
        return self._data.__getitem__(item)

    def __setitem__(self, key, value):
        self._data.__setitem__(key, value)

    def __getattr__(self, item):
        # delegates array's attributes and methods, except dunders.
        try:
            return getattr(self._data, item)
        except AttributeError:
            raise AttributeError()

    # binary operations
    def __add__(self, other):
        return Image(self._data.__add__(other))

    def __sub__(self, other):
        return Image(self._data.__sub__(other))

    # many more follow ... How to avoid this redundancy?

如您所见,我想拥有所有用于数字运算的魔术方法,就像普通的 numpy 数组一样,但 return 值作为 Image 类型。所以这些魔术方法的实现,即 __add____sub____truediv__ 等等,几乎是一样的,有点傻。我的问题是是否有办法避免这种冗余?

除了我在这里具体做的事情之外,有没有一种方法可以通过某种元编程技术在一个地方编写魔术方法,或者这根本不可能?我搜索了一些关于 python metaclass,但我还是不清楚。

注意__getattr__ 不会处理魔术方法的委托。参见

编辑

澄清一下,我知道继承是解决此类问题的通用方法,尽管我的经验非常有限。但我觉得继承 numpy array 确实不是一个好主意。因为 numpy 数组需要处理视图投射和 ufunc(参见 this). And when you use your subclass in other py-libs, you also need to think how your array subclass gets along with other array subclasses. See my stupid gh-issue。这就是我寻找替代方案的原因。

您所追求的是称为继承的概念,它是面向对象编程的关键部分(参见维基百科here

当您用 class Image(object): 定义 class 时,这意味着 Imagesubclass object,这是一个内置类型,做的很少。您的功能被添加到该或多或少的空白概念上。但是,如果您用 class Image(np.array): 定义 class,那么 Image 将是 array 的子 class,这意味着它将继承 array 的所有默认功能数组 class。本质上,任何您想保留原样的 class 方法都不应该重新定义。如果您不编写 __getitem__ 函数,它会使用 array 中定义的函数。

如果您需要在任何这些函数中添加额外的功能,您仍然可以重新定义它们(称为 overriding),然后使用 super().__getitem__(或其他)来访问继承 class 中定义的函数。例如,这经常发生在 __init__ 中。

要获得更详尽的解释,请查看 中的 the chapter on inheritance 思考 Python

魔术方法总是在 class 中查找并完全绕过 get 属性,因此您必须在 class 中定义它们。 https://docs.python.org/3/reference/datamodel.html#special-lookup

但是,您可以节省一些输入时间:

import operator
def make_bin_op(oper):
    def op(self, other):
        if isinstance(other, Image): 
            return Image(oper(self._data, other._data))
        else:
            return Image(oper(self._data, other))
    return op

class Image:
    ...
    __add__ = make_bin_op(operator.add)
    __sub__ = make_bin_op(operator.sub)

如果需要,您可以制作一个 dict 运算符双下注名称和相应的运算符,并使用装饰器添加它们。例如

OPER_DICT = {'__add__' : operator.add, '__sub__' : operator.sub, ...}
def add_operators(cls):
    for k,v in OPER_DICT.items():
        setattr(cls, k, make_bin_op(v))

@add_operators
class Image:
    ...

您可以使用元class 来做同样的事情。但是,您可能不想使用 metaclass 除非您真的了解发生了什么。