Компромисс между дублированием кода и производительностью

Python, являясь динамическим языком, который он представляет, предлагает несколько способов реализации одной и той же функции. Эти параметры могут отличаться в читаемости, ремонтопригодности и производительности. Несмотря на то, что обычные сценарии, которые я пишу на Python, имеют одноразовый характер, теперь у меня есть определенный проект, над которым я работаю (академический), который должен быть читаемым, поддерживаемым и хорошо работать. Поскольку я раньше не делал серьезного кодирования на Python, включая какой-либо профилирование, мне нужна помощь в решении баланса между тремя факторами, упомянутыми выше.

Вот фрагмент кода из одного из модулей в научном пакете, над которым я работаю. Это n-арный класс дерева с очень простой структурой скелета. Это было написано с учетом наследования и подкласса.

Примечание. В приведенном ниже коде дерево имеет то же самое, что и node. Каждое дерево является экземпляром того же дерева классов.

class Tree(object):

    def __init__(self, parent=None, value=None):
        self.parent = parent
        self.value = value
        self.children = set()

Две функции ниже относятся к этому классу (наряду со многими другими)

    def isexternal(self):
        """Return True if this is an external tree."""
        return not bool(self.children)

    def isleaf(self):
        """Return True if this is a leaf tree."""
        return not bool(self.children)

Обе эти функции выполняют одну и ту же вещь - это всего лишь два разных имени. Итак, почему бы не изменить его на что-то вроде:

    def isleaf(self):
        """Return True of this is a leaf tree."""
        return self.isexternal()

Мое сомнение таково:

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

Вот еще один пример сценария дублирования кода в том же классе:

def isancestor(self, tree):
    """Return True if this tree is an ancestor of the specified tree."""
    return tree.parent is self or (not tree.isroot() 
        and self.isancestor(tree.parent))

def isdescendant(self, tree):
    """Return True if this tree is a descendant of the specified tree."""
    return self.parent is tree or (not self.isroot() 
        and self.parent.isdescendant(tree))

Вместо этого я мог бы пойти:

def isdescendant(self, tree):
    """Return True if this tree is a descendant of the specified tree."""
    return tree.isancestor(self)

Ответ 1

В широком смысле существует два типа оптимизации: оптимизация макросов и микрооптимизация. Оптимизация макросов включает в себя такие вещи, как выбор алгоритмов, выбор между различными структурами данных и т.п. Вещи, которые могут иметь большое влияние на производительность, и часто имеют большие волновые эффекты на вашей базе кода, если вы передумаете. Переход от структуры данных с линейным O (n) к одному с постоянными вставками O (1) может стать огромной победой и стоит затрат на ее выполнение. Добавление кеширования может изменить медленный алгоритм собаки на молниеносный.

Микрооптимизация - это такие вещи, как возврат или вложение вызовов функций, устранение или добавление переменных, кэширование результатов расчета для очень короткого окна, развертка циклов и т.д. Как правило, вы должны забыть об этих типах оптимизации и focus на удобочитаемость и ремонтопригодность вашего кода. Эффекты микрооптимизации просто слишком малы, чтобы стоить того.

Вы должны учитывать эти типы изменений после профилирования кода. Если вы можете определить критический цикл, который выиграет от такой оптимизации, и ваше профилирование подтвердит это, и вы внесете изменения и убедитесь, что улучшение работает с другим циклом профилирования, - тогда вы должны микро оптимизировать.

Но до тех пор не потейте мелкие вещи.

def isdescendant(self, tree):
    """Return True if this tree is a descendant of the specified tree."""
    return tree.isancestor(self)

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

def isleaf(self):
    """Return True of this is a leaf tree."""
    return self.isexternal()

Здесь я бы спросил, если isleaf и isexternal концептуально одинаковы. Игнорируя, что они реализованы одинаково, они логически идентичны? Если это так, я бы позвонил другому. Если просто случается, что они имеют одну и ту же реализацию, я могу дублировать код. Можете ли вы представить себе сценарий, в котором вы хотите изменить одну функцию, но не другую? Это указывает на дублирование.

Ответ 2

Этот подход хорошо работает, не вводя дополнительные кадры стека.

def isexternal(self):
    """Return True of this is an external tree."""
    return not bool(self.children)

isleaf = isexternal

В вашем втором случае алгоритмы принципиально отличаются между этими двумя методами. Я думаю, что решение, которое вы предлагаете, прекрасное.

Ответ 3

Просто небольшой тест:

>>> timeit('a()', setup="def a(): pass")
0.08267402648925781
>>> timeit('1+1')
0.03854799270629883

Таким образом, простой вызов функции имеет менее 2,5-кратное время работы по сравнению с простым арифметическим выражением. Я не думаю, что это считается "довольно дорогим".

Ответ 4

Дэвид Хесс отвечает хорошо...

Кроме того, ни оптимальный, ни канонический Python не говорят not bool(x).

not x дает точно такие же результаты и один глобальный поиск и один вызов функции дешевле.

Кроме того, если вы используете self.parent дважды в одном вызове, вы можете подумать о том, хотите ли вы поместить его в локальный - parent = self.parent, потому что локальный поиск намного дешевле, чем поиск экземпляров. Конечно, вам нужно запустить timeit, чтобы убедиться, что вы получаете выгоду.