什么时候建议使用ret_val变量?

时间:2009-09-28 21:06:45

标签: python coding-style return-value

关于以下代码是否更好,我看到了相互矛盾的建议

def function():
    ret_val = 0
    if some_condition():
        ret_val = 2
    else:
        ret_val = 3
    return ret_val

或者这是否更好:

def function():
    if some_condition():
        return 2
    else:
        return 3

这是一个简单的例子,我用python风格编写它,但我正在寻找的是关于何时使用一些“累加器”变量来跟踪返回值的一般原则,或者是否使用多个退出点。我知道不同的语言可能有不同的原因使用一种风格而不是另一种风格,所以我很欣赏不同的观点,为什么特定的语言可能会坚持特定的风格。 (特别是在过去,我听说C中的结构化编程避免了函数的多个退出点。)

11 个答案:

答案 0 :(得分:8)

我们是否忘记了为什么“多个出口点”被认为是有害的?回到当天(在广泛访问良好的异常处理和最终构造之前,或管理像auto_ptr这样的对象,当它们离开作用域时进行清理),这是困扰许多多退出函数的问题:

int function blah(arg1, arg2)
    allocate resource

    if early failure detection
        return failure_status

    ... much later...

    release resource // oh rats! resource didn't release
    return success_status

如果资源是内存,则会产生内存泄漏。如果它是数据库事务,我们正在走向错误的数据库争用或死锁。就此而言,随着更多异常支持的出现,我们隐式地从方法中添加许多潜在退出(由于未处理的异常)。在我的C ++时代,我养成了从不调用delete的习惯,而是使用auto_ptr,以便在auto_ptr退出其范围时清除已分配的内存,即使一些意外的异常抬头了。

在我们的垃圾收集Python世界中,我们仍然可以解决这个问题,即使我们的许多对象(如文件或锁)具有改进的自清洁行为。但是在CPython以外的实现中(jython和IronPython命名为2),不能保证析构函数何时被调用,因此需要在方法中构建更主动的东西。为此目的的第一个机制是try / finally:

int function blah(arg1, arg2)
    allocate resource
    try:

        if early failure detection
            return failure_status

        ... much later...
        return success_status

    finally:
        release resource // always releases no matter what

但是现在Python有了上下文管理器,结合了新的'with'语法:

int function blah(arg1, arg2)
    allocate resource
    with context_manager(resource): // releases on exit from 'with'

        if early failure detection
            return failure_status

        ... much later...
        return success_status

所以,让我们确定我们讲述整个故事,我们可以抛弃这个旧板栗的原因是新的编码实践使它变得没必要。

答案 1 :(得分:5)

在Python中,在函数中间有一个return语句是很常见的 - 特别是如果它是早期退出。您的示例通常被重写为

def function():
    if some_condition():
        return 2
    return 3

即。如果if以return结尾,则删除else case。

答案 2 :(得分:5)

除非绝对不可避免,否则不要使用累加器。它会在您的过程中引入不必要的有状态和分支,然后您必须手动跟踪它们。通过提前返回,您可以减少代码的状态和分支数。

  

特别是在过去,我听说C中的结构化编程避免了函数的多个退出点。

恰恰相反,结构化的编程阻碍了条目的多个点,但多个退出点是可以接受的,甚至是鼓励的(例如“保护条款”)。

答案 3 :(得分:2)

除了文体学之外,我们来看看这两种方法的反汇编:

>>> def foo():
...     r = 0
...     if bar():
...             r = 2
...     else:
...             r = 3
...     return r
... 
>>> dis.dis(foo)
  2           0 LOAD_CONST               1 (0)
              3 STORE_FAST               0 (r)

  3           6 LOAD_GLOBAL              0 (bar)
              9 CALL_FUNCTION            0
             12 JUMP_IF_FALSE           10 (to 25)
             15 POP_TOP             

  4          16 LOAD_CONST               2 (2)
             19 STORE_FAST               0 (r)
             22 JUMP_FORWARD             7 (to 32)
        >>   25 POP_TOP             

  6          26 LOAD_CONST               3 (3)
             29 STORE_FAST               0 (r)

  7     >>   32 LOAD_FAST                0 (r)
             35 RETURN_VALUE        

第一种方法中的14字节码指令......

>>> def quux():
...     if bar():
...             return 2
...     else:
...             return 3
... 
>>> dis.dis(quux)
  2           0 LOAD_GLOBAL              0 (bar)
              3 CALL_FUNCTION            0
              6 JUMP_IF_FALSE            5 (to 14)
              9 POP_TOP             

  3          10 LOAD_CONST               1 (2)
             13 RETURN_VALUE        
        >>   14 POP_TOP             

  5          15 LOAD_CONST               2 (3)
             18 RETURN_VALUE        
             19 LOAD_CONST               0 (None)
             22 RETURN_VALUE        

第二种方法中的11 ......

第三种方法,略短于第二种方法:

>>> def baz():
...     if bar():
...             return 2
...     return 3
... 
>>> dis.dis(baz)
  2           0 LOAD_GLOBAL              0 (bar)
              3 CALL_FUNCTION            0
              6 JUMP_IF_FALSE            5 (to 14)
              9 POP_TOP             

  3          10 LOAD_CONST               1 (2)
             13 RETURN_VALUE        
        >>   14 POP_TOP             

  4          15 LOAD_CONST               2 (3)
             18 RETURN_VALUE        

只有九条指令。这些差异可能看起来不多,但实际上与timeit的运行相比有一点差异,bar被定义为返回交替的零和1:

$ sudo nice -n -19 python b.py
('foo', 1.3846859931945801)
('quux', 1.282526969909668)
('baz', 1.2973799705505371)
$ sudo nice -n -19 python b.py
('foo', 1.354640007019043)
('quux', 1.2609632015228271)
('baz', 1.2767179012298584)

$ sudo nice -n -19 python3 b.py
foo 1.72521305084
quux 1.62322306633
baz 1.62547206879
$ sudo nice -n -19 python3 b.py
foo 1.73264288902
quux 1.67029309273
baz 1.62204194069

quuxbaz往往接近同一时间,两者都始终快于foo

如果你仍然关注哪一个更好,那么希望这说明到目前为止没有其他人提到的无累加器方法的另一个优势。

答案 4 :(得分:1)

它在很大程度上取决于语言,但是我会使用第二种方法直接返回值而不是强加另一个多余的变量。第二种方法更清洁,更精确,因此我认为更易于维护。

答案 5 :(得分:1)

我想这更像是一种风格和编码惯例的问题。一般来说,理论告诉我们多个出口点都很糟糕。在实践中,可以更容易地遵循以简单地在每个条件内返回。代码很可能被编译成非常相似的(如果不是相同的)指令,因此几乎没有功能影响。

我的经验法则是:如果函数长于一页(25行),请避免多个退出点。如果你能一次看到它,那么在你写它的时候做一些最好的事情。

答案 6 :(得分:1)

Python的最新版本中的另一个替代方案(从2.6开始?)是这样的三元运算符语句:

def function():
    return (2 if some_condition() else 3)

以防万一你更喜欢。

答案 7 :(得分:1)

对于原语,没关系。在像C ++这样的语言中(并且可能在C语言中使用结构体,编译器会做类似的事情),如果确保所有代码路径都返回相同的变量,编译器就能够优化copy constructor。例如:

Foo someFunction()
{
    Foo result(5);
    if (someConditionA())  return result;
    else if (someConditionB()) result.doSomething();
    result.doSomethingElse();
    return result;
}

变得更有效率(除非你的编译器非常好):

Foo someFunction()
{
   if (someConditionA()) return Foo(5);
   if (someConditionB()) { Foo result(5); result.doSomething(); result.doSomethingElse(); return result; }
   Foo result(5);
   result.doSomethingElse();
   return result;
}

在所有其他情况下,它更多的风格偏好&可读性。最后,选择对于特定情况更具可读性的格式。

答案 8 :(得分:1)

尽管人们提倡单一退出策略,但我发现提前退货很有用。这样,您在以后添加代码时就不必跟踪。

答案 9 :(得分:0)

在具有C ++或Java等函数原型的语言中,编译器会强制您返回正确类型的某些,即使执行会从函数末尾开始执行。在Python中,由于没有函数原型,因此从函数末尾开始将返回特殊值None。因此,在Python编码时,您可能希望在末尾使用累加器变量和显式return ret_val。或者使用另一种样式,确保执行无法在不返回值的情况下结束。

答案 10 :(得分:0)

对于像您的示例这样的小函数,直接返回值并不可怕。但是,如果您具有大型或复杂的功能,则多个返回点可能更难以调试。如果您有编码标准,我会参考它(根据我们公司的编码标准,这里的变量是首选)。