Python — расширение свойств, как если бы вы расширяли функцию

Вопрос

Как расширить свойство 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, 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 представляли один и тот же интерфейс, чтобы они могли использовать один и тот же код. Можно ли расширить получатель фреймов данных суперкласса, чтобы воспользоваться существующими подпрограммами? Как? Или мне лучше сделать это по-другому?

Спасибо, что прочитали эту огромную стену текста. Ты интернет-супергерой, дорогой читатель. Есть идеи?


person Matt Merrifield    schedule 18.02.2014    source источник
comment
Почему бы вам не скопировать из родителя в дочерний и не изменить его в соответствии с вашими потребностями?   -  person thefourtheye    schedule 18.02.2014
comment
Видели ли вы предыдущие вопросы о подобных проблемах здесь и здесь?   -  person BrenBarn    schedule 18.02.2014
comment
@BrenBarn - спасибо за ссылки. Я искал, но не мог их найти. Первый хорошо ответил бы на мой вопрос.   -  person Matt Merrifield    schedule 18.02.2014


Ответы (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'}

Обычно я бы рекомендовал помещать побочные эффекты в сеттеры вместо геттеров, например:

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)

Как правило, следует избегать дорогостоящих операций внутри геттера (например, вашего метода синтаксического анализа).

person Dane White    schedule 18.02.2014
comment
Это именно то, что я искал. В частности, фрагмент A.prop.fget(self). Я признаюсь, что чувствую себя немного грязным, добавляя дорогостоящие операции, такие как parse(), в метод получения свойств, но я не уверен, куда еще их прилепить. Я не хочу, чтобы пользователям приходилось явно вызывать parse(), и я определенно не добавляю ничего дорогого в __init__(). LogFile.dataframe редко устанавливается явно пользователем (хотя это может быть). Какой способ лучше? - person Matt Merrifield; 18.02.2014
comment
Поскольку синтаксис свойств скрывает тот факт, что выполняется дополнительный код, кто-то, использующий ваш API, может случайно получить существенное снижение производительности, не осознавая этого. Хотя кто-то может не подумать, что повторный вызов b.prop в цикле приведет к большим накладным расходам, если b.prop вызывает много времени, то кэширование b.prop во временной переменной вне цикла может значительно повысить производительность. - person Dane White; 19.02.2014
comment
В вашем случае ваш метод parse должен запускаться только один раз, так что это не так уж плохо. И если вам действительно нужно лениво загрузить файл, то вы должны сделать это именно здесь. Однако, поместив нагрузку в свой конструктор, который принимает имя файла в качестве аргумента, это будет гораздо более очевидно в вашем API, где будут удары по производительности. Это облегчит более крупному приложению разумное управление производительностью. - person Dane White; 19.02.2014
comment
Я думаю, что идеальным сценарием было бы, чтобы __init__ вызывал parse, parse вызывал установщик, а установщик добавлял дополнительные поля. Как вы сейчас настроили, ваш геттер будет повторно устанавливать дополнительные поля, которые уже были установлены. Если вам нужна отложенная загрузка файла, вы можете вызвать parse из геттера, но parse все равно должен вызывать сеттер, где вы выполняете модификацию поля. - person Dane White; 19.02.2014
comment
А если A — абстрактный класс, а свойство prop — только абстрактное определение? - person IAbstract; 21.02.2016

Если я правильно понимаю, что вы хотите сделать, это вызвать родительский метод из дочернего экземпляра. Обычный способ сделать это — использовать встроенный 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

Подробнее о super можно прочитать здесь

Редактировать: даже несмотря на то, что пример, который я вам привел, касается методов, сделать то же самое с @properties не должно быть проблемой.

person kirbuchi    schedule 18.02.2014
comment
Интересный! Я еще не использовал вызов super(). Мне нужно будет немного почитать. - person Matt Merrifield; 18.02.2014

У вас есть несколько возможностей для рассмотрения:

1/Наследовать от logfile и переопределить parse в производном классе датчика. Должна быть возможность изменить ваши методы, которые работают на dataframe, чтобы они работали независимо от количества членов, которые есть у dataframe — поскольку вы используете pandas, многое делается для вас.

2/Сделайте sensor экземпляром logfile, затем предоставьте собственный метод синтаксического анализа.

3/ Обобщите parse и, возможно, некоторые другие ваши методы, чтобы использовать список дескрипторов данных и, возможно, словарь методов/правил, установленных в инициализаторе вашего класса или установленных методами.

4/Посмотрите либо на более широкое использование методов, уже имеющихся в пандах, либо, возможно, на расширение панд, чтобы предоставить отсутствующие методы, если вы и другие считаете, что они будут приняты в пандах как полезные расширения.

Лично я думаю, что вы найдете преимущества вариантов 3 или 4 наиболее эффективными.

person Steve Barnes    schedule 18.02.2014
comment
Панды классные. Уэс МакКинни проделал за меня 99% тяжелой работы. Все, что мне нужно сделать, это выяснить, как получить мои данные в объекты DataFrame, а затем правильно вызвать собственные методы .join() и .align(). Переопределение parse() в объекте Sensor может сработать — я посмотрю на это. Затем возникает вопрос о том, когда мне следует анализировать отдельные кадры данных, когда я должен их выравнивать/объединять и когда я должен выполнять над ними дополнительные вычисления. - person Matt Merrifield; 18.02.2014

Проблема в том, что вам не хватает self, входящего в родительский класс. Если ваш родитель является синглтоном, тогда должен работать @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
person user2197172    schedule 18.02.2014