关于继承:Python – 像扩展函数一样扩展属性

Python - extending properties like you'd extend a function

问题

如何扩展python属性?

子类可以通过在重载版本中调用超类的函数,然后对结果进行操作来扩展该类的函数。下面是我说"扩展函数"时的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Extending a function (a tongue-in-cheek example)

class NormalMath(object):
    def __init__(self, number):
        self.number = number

    def add_pi(self):
        n = self.number
        return n + 3.1415


class NewMath(object):
    def add_pi(self):
        # NewMath doesn't know how NormalMath added pi (and shouldn't need to).
        # It just uses the result.
        n = NormalMath.add_pi(self)  

        # In NewMath, fractions are considered too hard for our users.
        # We therefore silently convert them to integers.
        return int(n)

除了使用属性修饰器的函数之外,是否还有类似于扩展函数的操作?

我想在得到一个昂贵的计算属性后立即进行一些额外的计算。我需要保持属性的访问延迟。我不希望用户必须调用一个特殊的例程来进行计算。基本上,我不希望用户知道计算是在第一时间进行的。但是,该属性必须保持为属性,因为我有需要支持的遗留代码。

也许这是装饰师的工作?如果我没有弄错,decorator是一个包装另一个函数的函数,我希望用更多的计算来包装一个属性,然后将它再次呈现为一个属性,这看起来像是一个类似的想法…但我不太明白。

我的具体问题

我有一个基类日志文件,它的构造attribute.dataframe代价很高。我已经将它实现为一个属性(使用属性修饰器),所以在我请求数据帧之前,它不会真正解析日志文件。到目前为止,效果很好。我可以构建一组(100+个)日志文件对象,并使用更便宜的方法来过滤和选择要解析的重要对象。每当我一次又一次地使用相同的日志文件时,我只需要在第一次访问数据帧时解析它。

现在,我需要编写一个日志文件子类sensorlog,它向基类的dataframe属性添加了一些额外的列,但是我还不能很好地找到调用超级类的dataframe构造例程的语法(不知道它们的内部工作),然后对生成的dataframe进行操作,然后缓存/返回它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# Base Class - rules for parsing/interacting with data.
class LogFile(object):
    def __init__(self, file_name):
        # file name to find the log file
        self.file_name = file_name
        # non-public variable to cache results of parse()
        self._dataframe = None

    def parse(self):
        with open(self.file_name) as infile:
            ...
            ...
            # Complex rules to interpret the file
            ...
            ...
        self._dataframe = pandas.DataFrame(stuff)

    @property
    def dataframe(self):
       """
        Returns the dataframe; parses file if necessary. This works great!

       """

        if self._dataframe is None:
            self.parse()
        return self._dataframe

    @dataframe.setter
    def dataframe(self,value):
        self._dataframe = value


# Sub class - adds more information to data, but does't parse
# must preserve established .dataframe interface
class SensorLog(LogFile):
    def __init__(self, file_name):
        # Call the super's constructor
        LogFile.__init__(self, file_name)

        # SensorLog doesn't actually know about (and doesn't rely on) the ._dataframe cache, so it overrides it just in case.
        self._dataframe = None

    # THIS IS THE PART I CAN'T FIGURE OUT
    # Here's my best guess, but it doesn't quite work:
    @property
    def dataframe(self):
        # use parent class's getter, invoking the hidden parse function and any other operations LogFile might do.
        self._dataframe = LogFile.dataframe.getter()    

        # Add additional calculated columns
        self._dataframe['extra_stuff'] = 'hello world!'
        return self._dataframe


    @dataframe.setter
    def dataframe(self, value):
        self._dataframe = value

现在,当这些类在交互会话中使用时,用户应该能够以相同的方式与之交互。

1
2
3
4
5
6
>>> log = LogFile('data.csv')
>>> print log.dataframe
#### DataFrame with 10 columns goes here ####
>>> sensor = SensorLog('data.csv')
>>> print sensor.dataframe
#### DataFrame with 11 columns goes here ####

我有很多现有的代码,它们采用一个日志文件实例,它提供.dataframe属性,并做一些有趣的事情(主要是绘图)。我希望SensorLog实例具有相同的接口,这样它们就可以使用相同的代码。是否可以扩展超级类的数据帧getter以利用现有的例程?怎样?还是我最好换一种方式?

感谢你阅读那面巨大的文本墙。你是一个互联网超级英雄,亲爱的读者。有什么想法吗?


您应该调用超类属性,而不是通过self._dataframe绕过它们。下面是一个通用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class A(object):

    def __init__(self):
        self.__prop = None

    @property
    def prop(self):
        return self.__prop

    @prop.setter
    def prop(self, value):
        self.__prop = value

class B(A):

    def __init__(self):
        super(B, self).__init__()

    @property
    def prop(self):
        value = A.prop.fget(self)
        value['extra'] = 'stuff'
        return value

    @prop.setter
    def prop(self, value):
        A.prop.fset(self, value)

使用它:

1
2
3
b = B()
b.prop = dict((('a', 1), ('b', 2)))
print(b.prop)

输出:

1
{'a': 1, 'b': 2, 'extra': 'stuff'}

我通常建议在setter中放置副作用,而不是getter,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class A(object):

    def __init__(self):
        self.__prop = None

    @property
    def prop(self):
        return self.__prop

    @prop.setter
    def prop(self, value):
        self.__prop = value

class B(A):

    def __init__(self):
        super(B, self).__init__()

    @property
    def prop(self):
        return A.prop.fget(self)

    @prop.setter
    def prop(self, value):
        value['extra'] = 'stuff'
        A.prop.fset(self, value)

在getter中进行昂贵的操作通常也是可以避免的(例如解析方法)。


您可以考虑:

1/从logfile继承并在派生传感器类中重写parse。不管dataframe有多少成员,都可以修改您在dataframe上工作的方法来工作,因为您正在使用熊猫,很多都是为您完成的。

2/使sensor成为logfile的实例,然后提供自己的解析方法。

3/将parse和可能的一些其他方法概括为使用数据描述符列表和方法/规则字典,这些方法/规则可以在类初始化器中设置,也可以由方法设置。

4/如果您和其他人认为熊猫会被接受为有用的扩展,请考虑更多地利用熊猫已有的方法,或者可能扩展熊猫以提供缺失的方法。

我个人认为,你会发现选项3或4的好处是最强大的。


如果我正确理解您要做的是从子实例调用父级的方法。通常的方法是使用super内置的。

我以你的舌头为例,修改为使用super,以便向你展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class NormalMath(object):
    def __init__(self, number):
        self.number = number

    def add_pi(self):
        n = self.number
        return n + 3.1415


class NewMath(NormalMath):
    def add_pi(self):
        # this will call NormalMath's add_pi with
        normal_maths_pi_plus_num = super(NewMath, self).add_pi()
        return int(normal_maths_pi_plus_num)

在日志示例中,不要调用:

1
self._dataframe = LogFile.dataframe.getter()

你应该打电话给:

1
self._dataframe = super(SensorLog, self).dataframe

你可以在这里读到更多关于超级的东西

编辑:即使我给你的例子是关于方法的,对@properties做同样的事情也不成问题。


问题是你缺少进入父类的自我。如果您的父级是单例的,那么@staticmethod应该可以工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class X():
    x=1
    @staticmethod
    def getx():
        return X.x

class Y(X):
    y=2
    def getyx(self):
        return X.getx()+self.y

wx = Y()
wx.getyx()
3