Как преобразовать конвейер данных на основе функций в ООП?

#python #oop

#python #ооп

Вопрос:

Я выполняю некоторую обработку данных и построил несколько конвейеров, каждый из которых состоит из нескольких функций, которые в целом изменяют словари на каждом шаге. Поскольку разные конвейеры работают с одними и теми же данными и имеют схожие функции, я пытался преобразовать его в более ориентированную на ООП структуру. Однако, прежде чем я начну, я немного завязал себя в узлы.

Возьмем следующий упрощенный пример:

 for f in foos:
    y = extract_y_info(f)
    z = extract_z_info(f)
    *some code that does something with y and z*
    
def extract_y_info(f):
    return *some code that extracts y info from f*

def extract_z_info(f):
    return *some code that extracts z info from f*
  

Мне кажется, есть несколько способов, которыми я мог бы приблизиться к переносу этого в структуру ООП. Первый подход очень похож на подход «функция за функцией».

 class foo():
    def __init__(self, x):
        self.x = x

    def extract_y_info(self):
        return *some code that extracts y info from self.x*

    def extract_z_info(self):
        return *some code that extracts z info from self.x*

for f in foo_instances:
    y = b.extract_y_info()
    z = b.extract_z_info()
    *some code that does something with y and z*
  

Другим вариантом является изменение экземпляров класса:

 class foo():
    def __init__(self, x):
        self.x = x

    def extract_y_info(self):
        self.y = *some code that extracts y info from self.x*

    def extract_z_info(self):
        self.z = *some code that extracts z info from self.x*

for f in foo_instances:
    f.extract_y_info()
    f.extract_z_info()
    *some code that does something with f.y and f.z*
  

Является ли любой из этих вариантов лучшей практикой, чем другой? Есть ли лучший третий способ?

Комментарии:

1. Честно говоря, я думаю, что вы могли бы лучше провести время только со свободными функциями, если только вам не нужно наследование (и ничто не мешает вам смешивать и сопоставлять классы и функции в вашем конвейере)…

Ответ №1:

Это действительно зависит от того, каков ваш общий дизайн и в каком состоянии вы ожидаете, что ваши экземпляры будут находиться в любой момент времени, и что вы с ним делаете (другими словами, означает ли существование y самого атрибута, но … первое кажется мне в целом более безопасным. Вы вызываете и получаете значение, вам не нужно отслеживать, вызвал ли я метод и в каком состоянии находится тот или иной атрибут? Обратите внимание, однако, что вы действительно должны определять атрибуты экземпляра в конструкторе, иначе доступ может быть не просто неожиданным, но и фатальным ( AttributeError ) .

Теперь аккуратным решением, учитывающим некоторые из приведенных выше пунктов и, возможно, соответствующим тому, что вы, похоже, делаете здесь для доступа к значениям, может быть свойство, которое, по сути, позволяет вам получать доступ к значению, возвращаемому методом, как если бы это был атрибут экземпляра:

 class foo():
    def __init__(self, x):
        self.x = x

    def extract_y_info(self):
        return #some code that extracts y info from self.x

    y = property(extract_y_info)     

for f in foo_instances:
    print(f"value of f.y = {f.y}")
  

Или вы можете сделать то же самое, используя property декоратор метода as:

     @property
    def y(self):
        return #some code that extracts y info from self.x
  

Если получение y было дорогостоящим и его значение не менялось в течение срока службы экземпляра, вы также можете использовать запуск Python 3.8 cached_property .

Комментарии:

1. Спасибо за это! Один быстрый вопрос… вы написали «вы действительно должны определять атрибуты экземпляра в конструкторе» — означает ли это, что я всегда должен определять self.y и self.z в __init__() , даже если для них установлено значение None для начала?

2. ДА. Веская причина: ваш интерфейс экземпляра (его атрибуты) легко понять, посмотрев на конструктор и атрибуты, установленные в constructor . Разница в том, что вы тогда спросили, также заключается в том, что, если вы не назначены в конструкторе и получили доступ к методу, который бы это сделал, f.z возникло AttributeError бы, если бы вы назначили None ему в constructor ( __init__ ) , вы бы получили None , даже если бы он не имел никакого особого значения (но и для этого я бы все равно обычнопредпочитаю: return над комбинацией манипулирования / доступа, если я хочу получить значение.