【问题标题】:Python - extending properties like you'd extend a functionPython - 像扩展函数一样扩展属性
【发布时间】:2014-03-17 15:27:12
【问题描述】:

问题

如何扩展 python 属性?

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

# 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)

是否有类似于扩展函数的操作,但对于使用属性装饰器的函数?

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

也许这是装饰师的工作?如果我没记错的话,装饰器是一个包装另一个函数的函数,我希望用更多的计算来包装一个属性,然后再次将其呈现为一个属性,这似乎是一个类似的想法......但我不太清楚。

我的具体问题

我有一个基类 LogFile,它具有构造成本高的属性 .dataframe。我已经将它实现为一个属性(使用属性装饰器),所以在我请求数据帧之前它实际上不会解析日志文件。到目前为止,它工作得很好。我可以构造一堆(100 多个)LogFile 对象,并使用更便宜的方法来过滤并仅选择要解析的重要对象。每当我一遍又一遍地使用同一个 LogFile 时,我只需要在第一次访问数据帧时解析它。

现在我需要编写一个 LogFile 子类 SensorLog,它在基类的数据帧属性中添加了一些额外的列,但我不太清楚调用超类的数据帧构造的语法例程(对它们的内部工作一无所知),然后对生成的数据帧进行操作,然后然后缓存/返回它。

# 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

现在,当在交互式会话中使用这些类时,用户应该能够以相同的方式与其中任何一个进行交互。

>>> 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 ####

我有很多现有代码,它们采用 LogFile 实例,该实例提供 .dataframe 属性并执行一些有趣的操作(主要是绘图)。我希望 SensorLog 实例呈现相同的界面,以便它们可以使用相同的代码。是否可以扩展超类的数据帧获取器以利用现有例程?如何?还是我最好换一种方式?

感谢您阅读那堵巨大的文字墙。亲爱的读者,您是互联网超级英雄。有什么想法吗?

【问题讨论】:

  • 为什么不从父级复制到子级并根据需要进行更改?
  • 您是否看到过关于类似问题herehere 的先前问题?
  • @BrenBarn -- 感谢您的链接。我看了看,但找不到他们。第一个会很好地回答我的问题。

标签: python inheritance properties extends


【解决方案1】:

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

我取了你的半开玩笑的例子,并将其修改为使用super,以便向你展示:

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)

在您的日志示例中,而不是调用:

self._dataframe = LogFile.dataframe.getter() 

你应该打电话:

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

你可以阅读更多关于超级here

编辑:即使我给您的示例处理的是方法,对@properties 做同样的事情也不应该是个问题。

【讨论】:

  • 有趣!我还没有使用 super() 调用。我得读书了。
【解决方案2】:

问题是你错过了进入父类的自我。如果您的父母是单身人士,那么 @staticmethod 应该可以工作。

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

【讨论】:

    【解决方案3】:

    您可以考虑一些可能性:

    1/ 从logfile 继承并在派生的传感器类中覆盖parse。无论dataframe 拥有多少成员,都应该可以修改您在dataframe 上工作的方法以使其工作 - 因为您正在使用pandas,其中很多都是为您完成的。

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

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

    4/ 考虑更多地使用 pandas 中已有的方法,或者如果您和其他人认为 pandas 会接受它们作为有用的扩展,则可能扩展 pandas 以提供缺少的方法。

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

    【讨论】:

    • 熊猫很棒。 Wes McKinney 为我完成了 99% 的艰苦工作。我所要做的就是弄清楚如何将我的数据放入 DataFrame 对象中,然后以正确的方式调用原生的 .join() 和 .align() 方法。在 Sensor 对象中覆盖 parse() 可能会起作用——我会研究一下。然后就变成了一个问题,什么时候应该解析单个数据帧,什么时候应该对齐/加入它们,什么时候应该对它们进行额外的计算。
    【解决方案4】:

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

    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)
    

    并使用它:

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

    输出:

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

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

    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 中进行昂贵的操作(例如您的 parse 方法)。

    【讨论】:

    • 这正是我想要的。具体来说,就是 sn -p "A.prop.fget(self)"。我承认在属性 getter 中放置像 parse() 这样的昂贵操作感觉有点脏,但我不知道还能把它放在哪里。我不希望用户必须显式调用 parse(),而且我绝对不会在 __init__() 中添加任何昂贵的东西。 LogFile.dataframe 很少由用户明确设置(尽管可以)。有什么更好的方法?
    • 由于属性的语法隐藏了正在执行附加代码的事实,因此使用您的 API 的人可能会在没有意识到的情况下意外地导致性能大幅下降。虽然有人可能不会认为在循环中重复调用b.prop 会产生很多开销,但如果b.prop 需要很长时间才能调用,那么将b.prop 缓存在循环外的临时变量中可能会大大提高性能。跨度>
    • 在你的情况下,你的 parse 方法应该只运行一次,所以它不是那么糟糕。如果你真的需要延迟加载文件,那么你应该这样做。但是,通过将负载放在以文件名作为参数的构造函数中,它会在您的 API 中表现得更加明显。这将使大型应用程序更容易智能地管理性能。
    • 我认为理想的情况是__init__ 调用parseparse 调用设置器,设置器添加额外的字段。按照您当前的设置方式,您的 getter 将重复设置已设置的额外字段。如果您需要延迟文件加载,则可以从 getter 调用 parse,但 parse 仍应调用 setter,在其中进行字段修改。
    • 如果A 是一个抽象类,而属性prop 只是一个抽象定义呢?
    猜你喜欢
    • 1970-01-01
    • 2013-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多