代码复制和性能之间的权衡

12

Python是一种动态语言,提供多种实现相同功能的方法。这些选项可能在可读性、可维护性和性能方面有所不同。虽然我通常在Python中编写的脚本是一次性的,但我现在正在做一项(学术)项目,需要可读性、可维护性和合理的性能。由于我以前没有在Python中进行过任何严肃的编码工作,包括任何类型的分析,因此我需要帮助决定上述三个因素之间的平衡。

下面是我正在使用的一个科学软件包中一个模块的代码片段。它是一个n元树类,具有非常基本的骨架结构,并考虑到了继承和子类化。

注意:在下面的代码中,树与节点是同一物。每个树都是 Tree 类的实例。

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)
4个回答

21

非常笼统地说,优化可以分为两类:宏观优化和微观优化。宏观优化包括您选择的算法、在不同数据结构之间进行决策等。这些可能会对性能产生巨大影响,如果您改变主意,往往会对代码库产生广泛的连锁反应。从具有线性 O(n) 的数据结构切换到具有常数 O(1) 插入的数据结构可能是一个巨大的胜利并且完全值得成本。添加缓存可能会将一个慢如蜗牛的算法变成闪电般的快。

微观优化则是一些如省略或内联函数调用、消除或添加变量、为短时间窗口缓存计算结果、展开循环等。通常来说,您应该忘记这些类型的优化,专注于代码的可读性和可维护性。微观优化的效果太小,不值得去做。

只有在对代码进行了性能分析之后,您才应该考虑这些类型的更改。如果您可以确定受益于此类优化的关键循环,并且您的性能分析确认它确实受益,并且您进行了更改并验证了改进是否有效后进行另一轮性能分析--那么您应该进行微观优化。

但在此之前,请不要过于关注细节。

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

我绝对会推荐这种代码复用方式。它非常清晰地表明isdescendantisancestor的反义词。这确保了两个函数的工作方式相同,因此您不能意外在一个函数中引入错误而在另一个函数中没有。

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

在这里,我会问自己isleafisexternal在概念上是否相同。如果忽略它们实现方式相同,它们在逻辑上是否相同?如果是这样,我会让其中一个调用另一个。如果它们只是具有相同实现的偶然事件,那么我可能会复制代码。您能想象一种情况,您想更改一个函数但不更改另一个函数吗?这将指向重复。


这是一个很好的答案。我有时会过于关注(着迷于)微观优化代码。 - Renae Lider
2
在Donald Knuth的论文《使用GoTo语句进行结构化编程》中,他写道:“程序员浪费了大量时间思考或担心程序中非关键部分的速度,而这些对效率的追求实际上在调试和维护时会产生强烈的负面影响。我们应该忘记小的效率问题,例如97%的时间:过早地优化是万恶之源。然而,在关键的3%机会面前,我们不应该放弃。” - David K. Hess
5
我开始使用C语言和汇编语言编程。不夸张地说,我花了几年的时间才停止担心高级语言中所有小的低效率问题。没有必要循环三次来处理这个10字符的字符串,我可以手动循环一遍完成!我的天啊,这个循环很短,但是它将整个2KB的配置文件读入内存。那不可扩展!我将使用4KB最大缓冲区重新编写它,并分块处理文件,这样我就能处理100MB的配置文件... 理论计算机科学/嵌入式系统背景真正扭曲了一个人的优先事项。 - John Kugelman
1
不要对大局性的事情提出异议,但很多微观优化并不会特别影响代码的可读性或打字时间,如果这是你通常编写代码的方式,那么当你进行优化时,你的代码不会有太大的变化。 - Patrick Maupin
1
@PatrickMaupin 如果它既更快又不会降低可读性,那当然要这么做! - John Kugelman
1
有一个建议我想加上:当已经存在好的实现时,不要自己重复造轮子。假设它们得到了一定程度的使用并具有可接受的性能,这将极大地提高可读性、可维护性和通常的性能。这可能不适用于此处呈现的OP特定的用例,但对程序的其他部分可能适用。尤其是如果OP主要关注短暂的一次性脚本,他们可能不知道那里有丰富的Python库资源。 - jpmc26

5

这种方法在不引入额外的堆栈帧的情况下效果很好。

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

isleaf = isexternal

在你的第二种情况下,这两种方法之间的算法基本上是不同的。我认为你提出的解决方案是可以的。

这是一个非常好的解决方案。您能否建议如何处理第二个示例代码?它与此不同。 - Renae Lider
我认为你在第二个案例上已经做得足够好了。我在我的答案中添加了一条注释来表示这一点。 - David K. Hess

2

只是一个小测试:


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

因此,与简单算术表达式相比,简单的函数调用运行时间少于2.5倍。我认为这并不算是“相当昂贵”的。


我的代码中这些函数将根据Tree的深度被多次调用。因此,时间会逐渐增加。 - Renae Lider
1
"timeit" 运行代码 1,000,000 次以测量运行时间。无论如何,与算术运算相比,因子不会改变。 - Klaus D.
我以为 timeit 会将总时间除以 1,000,000 - 我错了。如果是这样的话,那么时间差根本不是问题。 - Renae Lider

1

David Hess的回答很好...

但是,使用not bool(x)既不优化也不符合Python语法规范。

not x得到的结果完全相同,而且比前者少一次全局查找和一次函数调用,性能更好。

此外,如果在同一次调用中两次使用self.parent,您可能要考虑是否想将其放入本地变量中--parent = self.parent--因为本地查找比实例查找便宜得多。当然,您应该使用timeit来确保您获得了收益。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接