在Python中创建包装类时,如何使父类的方法返回我的包装类实例

1 投票
2 回答
2244 浏览
提问于 2025-04-17 23:02

比如说,如果我在一个 pandas 数据框(dataframe)上创建一个简单的包装类:

    from pandas import DataFrame

    class MyDataFrame(DataFrame):

        def __init__(self,data):

            DataFrame.__init__(self,data)

        @staticmethod
        def add(a, b):
            return a + b

然后我实例化它,也就是创建一个这个包装类的具体对象……

    x = MyDataFrame([1,2,3])

            x.add(1,2)
            #    => 3

            type(x)
            #    => __main__.MyDataFrame

这样是可以正常工作的。但是,如果我在这个对象上调用一个返回数据框的方法,它就不再是我这个包装类的实例了。

    y = x.reindex([3,4,5])
    type(y)
    #    => pandas.core.frame.DataFrame

我该怎么做才能让所有数据框的方法都返回 MyDataFrame 的实例呢?这是一个常见的问题吗?我这样做是不是有问题?

2 个回答

0

在Pandas这个库里,有几个地方的类设计得不太好,导致后续的类无法很好地继承。有些问题已经解决了,比如在这两个链接里可以看到的内容:https://github.com/pydata/pandas/pull/4271https://github.com/pydata/pandas/issues/60

我们可以实现一个父类的 reindex 方法,这样返回的结果就是一个子类的实例:

from pandas import DataFrame

class DF():
    def __init__(self, data):
        print('DF __init__')
        self.data = data
    def reindex(self, index):
        print('DF reindex')
        return self.__class__(self.data)
        # return DF(self.data)  # not like this!

class MyDF(DF):
    def __init__(self, data):
        DF.__init__(self, data)
    @staticmethod
    def add(a, b):
        return a + b


x = MyDF([1,2,3])

x.add(1,2)
#    => 3
type(x)

y = x.reindex([3,4,5])
type(y)

z = DF([1,2,3])
type(z.reindex([1, 2]))

在Pandas的新版本中,内部设置了一个叫 `_constructor` 的东西,用来控制返回的类型。设置这个类属性似乎可以解决问题:

class MyDataFrame(DataFrame):
    def __init__(self, *args, **kwargs):
        DataFrame.__init__(self, *args, **kwargs)
    @staticmethod
    def add(a, b):
        return a + b

MyDataFrame._constructor = MyDataFrame

>>> type(y)
<class '__main__.MyDataFrame'>
2

你展示的例子其实不是一个包装器,而是Python中的一个子类。关于Python的子类和方法解析,在你的情况下,它们遵循一些简单的规则。

  1. 首先查看调用方法的对象的type
  2. 检查这个类的层级结构,找到第一个定义该方法的地方。然后查看这个方法的签名,并按照这个签名来执行。在你的例子中,类的层级结构很简单,就是子类和父类。

所以,在你的例子中,

  1. x被定义为MyDataFrame类的一个对象——这很简单。显然,type(x)就是MyDataFrame,这是定义好的。
  2. 在调用add方法时,它查看接收对象,也就是x,它属于MyDataFrame类。而这个类实际上定义了add方法。所以,它直接返回这个方法的结果。有趣的是,试试调用DataFrame([1, 2, 3]).add(1, 2)。结果会不同,因为它查看的是在pandas.DataFrame类中定义的add方法。
  3. 接下来是第三部分——我们继续用同样的逻辑。reindexMyDataFrame中没有定义。那我们该往哪里找呢?看类的层级结构,也就是pandas.DataFrame。确实,这个类定义了reindex,而且它返回的是一个pandas.DataFrame对象!(查看这个链接:http://pandas.pydata.org/pandas-docs/dev/generated/pandas.DataFrame.reindex.html#pandas.DataFrame.reindex)所以,y是一个pandas DataFrame也就不奇怪了。

现在我不太明白你为什么要在一开始就扩展pandas DataFrame。这样扩展并不是一种常见的做法。如果你能提供更多你想做的事情的细节,也许我们可以给出解决方案。

编辑:你最初的问题是关于扩展方法或扩展对象(C#有这些,正如你所说,JS的原型也提供了相同的功能。Python并没有将扩展方法/对象作为第一类成员。对此有过讨论。例如:python扩展方法

撰写回答