如果在实例中,我有self.foo = 1
,这些(或其他更复杂的例子)有什么区别:
# 1
for i in range(10):
print(self.foo)
# 2
foo = self.foo
for i in range(10):
print(foo)
我目前正在查看一个代码库,其中所有self
变量都被重新分配给其他东西。只是想知道是否有任何理由这样做,并希望从效率角度和代码清晰度的角度听取意见。
考虑以下可能性:
- 局部变量
self
在循环中间反弹。(这在您给出的特定代码中是不可能的,但可以想象不同的循环可以做到这一点。在这种情况下,#1 将看到新self
的foo
属性,而 #2 则不会。当然,尽管您可以像局部变量一样轻松地重新绑定局部变量foo
self
... self
是可变的,self.foo
在循环中间反弹到不同的值。(例如,在同一对象上运行另一个线程时,这可能更容易发生。同样,#1 将看到foo
属性的新值,但 #2 不会。self.foo
本身是可变的,它的值在循环的中间发生了变化(例如,它是一个列表,其他一些线程调用append(2)
它)。现在 #1 和 #2 都将看到新值。- 一切都是不可变的,或者只是没有代码(包括在其他线程上)来改变任何东西。现在 #1 和 #2 都将看到原始值,因为没有其他值可查看。
如果这些语义差异中的任何一个是相关的,那么你当然想使用任何一个给你正确的答案。
同时,每次访问self.foo
时,都需要进行属性查找。在最常见的情况下,这意味着在self.__dict__
中查找'foo'
,这非常快,但不是免费的。您可以轻松创建病理案例,其中它按 MRO 顺序通过 23 个基类,然后调用动态创建值并返回描述符的__getattr__
,该描述符__get__
方法执行一些非平凡的转换。
另一方面,访问foo
将被编译为使用编译的索引从帧上的数组中加载一个值。所以它几乎总是会更快,在某些情况下它可以快得多。
在大多数现实生活中,这根本不重要。但偶尔,它确实如此。在这种情况下,将值复制到循环外部的本地是一个值得的微优化。这在绑定方法中比在普通值中更常见一些(因为它们总是有一个描述符调用);有关示例,请参阅itertools
文档中的unique_everseen
配方。
当然,你可以设计一个这样的情况,这种优化实际上使事情变慢——例如,使该循环非常小,但将整个循环放在一个外部循环中。现在,每次通过外部循环复制的额外self.foo
(以及循环中涉及的字节码更长并且可能溢出到另一个缓存行的事实)的成本可能比节省的成本高得多。
如果没有重要的语义差异,并且性能差异无关紧要,那么这只是一个澄清的问题。
如果表达式比self.foo
复杂得多,那么提取值并给它一个名字可能会更清楚。
但是对于像这样的琐碎情况,仅使用self.foo
可能更清楚。通过采取额外的步骤将其复制到局部变量,您表明您有理由这样做。因此,读者会想知道self.foo
是否可以在不同的线程中反弹,或者这个循环是代码中的主要瓶颈,self.foo
访问是性能问题等,并且浪费时间处理所有这些不相关问题,而不仅仅是按预期阅读代码。