显式调用函数返回或不调用

时间:2012-07-31 11:12:56

标签: r

来自R核心团队的Simon Urbanek(我相信)推荐用户在功能结束时显式调用return(他的评论已被删除)的一段时间I got rebuked:< / p>

foo = function() {
  return(value)
}

相反,他建议:

foo = function() {
  value
}

可能在这种情况下需要:

foo = function() {
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

他的评论阐明了为什么不调用return,除非严格要求是好事,但这被删除了。

我的问题是:为什么不更快或更好地致电return,因此更可取?

9 个答案:

答案 0 :(得分:118)

问题是:为什么不(明确地)调用返回更快或更好,因此更可取?

R文档中没有任何声明做出这样的假设 Tha man page?'function'说:

function( arglist ) expr
return(value)

不调用退货会更快吗?

function()return()都是原始函数,function()本身即使不包含return()函数也会返回最后评估的值。

使用最后一个值作为参数调用return()作为.Primitive('return')将执行相同的工作,但需要多一个调用。这样(通常)不必要的.Primitive('return')调用可以吸引额外的资源。 然而,简单的测量表明产生的差异非常小,因此不能成为不使用显式返回的原因。以下以这种方式选择的数据创建以下图:

bench_nor2 <- function(x,repeats) { system.time(rep(
# without explicit return
(function(x) vector(length=x,mode="numeric"))(x)
,repeats)) }

bench_ret2 <- function(x,repeats) { system.time(rep(
# with explicit return
(function(x) return(vector(length=x,mode="numeric")))(x)
,repeats)) }

maxlen <- 1000
reps <- 10000
along <- seq(from=1,to=maxlen,by=5)
ret <- sapply(along,FUN=bench_ret2,repeats=reps)
nor <- sapply(along,FUN=bench_nor2,repeats=reps)
res <- data.frame(N=along,ELAPSED_RET=ret["elapsed",],ELAPSED_NOR=nor["elapsed",])

# res object is then visualized
# R version 2.15

Function elapsed time comparison

上图可能会在您的平台上略有不同。 根据测量数据,返回对象的大小不会造成任何差异,重复次数(即使按比例放大)只会产生非常小的差异,实际数据和真实算法的实际单词无法计算或使您的脚本运行得更快。

如果不致电返回会更好吗?

Return是清楚地设计代码“叶子”的好工具,其中例程应该结束,跳出函数并返回值。

# here without calling .Primitive('return')
> (function() {10;20;30;40})()
[1] 40
# here with .Primitive('return')
> (function() {10;20;30;40;return(40)})()
[1] 40
# here return terminates flow
> (function() {10;20;return();30;40})()
NULL
> (function() {10;20;return(25);30;40})()
[1] 25
> 

这取决于程序员的策略和编程风格,他使用的是什么样的风格,他可以不使用return(),因为它不是必需的。

R核心程序员使用两种方法即。有和没有显式return(),因为它可以在'base'函数的源代码中找到。

很多时候只使用return()(无参数)在条件下停止函数返回NULL。

目前尚不清楚它是否更好,因为使用R的标准用户或分析师无法看到真正的差异。

我的意见是问题应该是:使用来自R实现的显式回报是否有任何危险?

或者,或许更好,用户编写函数代码应始终询问:使用显式返回(或将对象作为代码分支的最后一个叶子返回放置)中的影响是什么功能代码?

答案 1 :(得分:85)

如果每个人都同意

    在函数体的末尾
  1. return不是必需的
  2. 不使用return的速度略快(根据@Alan的测试,4.3微秒与5.1相比)
  3. 我们是否应该在功能结束时停止使用return?我当然不会,我想解释原因。我希望听到其他人是否赞同我的意见。如果它不是OP的直接答案,我会道歉,但更像是一个长期的主观评论。

    我不使用return的主要问题是,正如Paul所指出的那样,函数体中还有其他地方可能需要它。如果你被迫在函数中间的某个地方使用return,为什么不将所有return语句都显式化?我讨厌不一致。我认为代码读得更好;可以扫描功能并轻松查看所有出口点和值。

    保罗使用了这个例子:

    foo = function() {
     if(a) {
       return(a)
     } else {
       return(b)
     }
    }
    

    不幸的是,有人可能会指出它很容易被重写为:

    foo = function() {
     if(a) {
       output <- a
     } else {
       output <- b
     }
    output
    }
    

    后一版本甚至符合一些编程编码标准,这些标准主张每个函数使用一个return语句。我认为一个更好的例子可能是:

    bar <- function() {
       while (a) {
          do_stuff
          for (b) {
             do_stuff
             if (c) return(1)
             for (d) {
                do_stuff
                if (e) return(2)
             }
          }
       }
       return(3)
    }
    

    使用单个return语句重写会更加困难:它需要多个break s和一个复杂的布尔变量系统来传播它们。所有这些都说单一返回规则与R不匹配。所以如果你需要在函数体的某些地方使用return,为什么不一致并在任何地方使用它?

    我不认为速度参数是有效参数。当您开始查看实际执行某些操作的函数时,0.8微秒的差异就不算什么了。我能看到的最后一件事是打字少了,但是嘿,我并不懒惰。

答案 2 :(得分:22)

似乎没有return()它会更快......

library(rbenchmark)
x <- 1
foo <- function(value) {
  return(value)
}
fuu <- function(value) {
  value
}
benchmark(foo(x),fuu(x),replications=1e7)
    test replications elapsed relative user.self sys.self user.child sys.child
1 foo(x)     10000000   51.36 1.185322     51.11     0.11          0         0
2 fuu(x)     10000000   43.33 1.000000     42.97     0.05          0         0

____ 编辑 __ _ __ _ __ _ __ _ __ _ ___

我继续其他基准测试(benchmark(fuu(x),foo(x),replications=1e7)),结果反过来......我会尝试服务器。

答案 3 :(得分:22)

这是一个有趣的讨论。我认为@ flodel的例子很棒。但是,我认为这说明了我的观点(@koshke在评论中提到这一点) return在您使用命令式而不是功能时有意义编码风格

不要强调这一点,但我会像这样重写foo

foo = function() ifelse(a,a,b)

功能样式可以避免状态更改,例如存储output的值。在这种风格中,return不合适; foo看起来更像是一种数学函数。

我同意@flodel:在bar中使用错综复杂的布尔变量系统会不那么明确,而在return时则毫无意义。使bar如此适合return语句的原因在于它是以命令式的方式编写的。实际上,布尔变量表示功能样式中避免的“状态”变化。

在功能样式中重写bar真的很困难,因为它只是伪代码,但这个想法是这样的:

e_func <- function() do_stuff
d_func <- function() ifelse(any(sapply(seq(d),e_func)),2,3)
b_func <- function() {
  do_stuff
  ifelse(c,1,sapply(seq(b),d_func))
}

bar <- function () {
   do_stuff
   sapply(seq(a),b_func) # Not exactly correct, but illustrates the idea.
}

while循环最难重写,因为它由a的状态更改控制。

调用return引起的速度损失可以忽略不计,但通过避免return和以函数式重写而获得的效率通常是巨大的。告诉新用户停止使用return可能无济于事,但引导他们使用功能样式将会有所收获。


@Paul return在命令式样式中是必需的,因为您经常希望在循环中的不同点退出函数。功能样式不使用循环,因此不需要return。在纯函数风格中,最终调用几乎总是所需的返回值。

在Python中,函数需要return语句。但是,如果您以函数式编写函数,则可能只有一个return语句:在函数结束时。

使用另一个StackOverflow帖子中的示例,假设我们想要一个函数,如果给定TRUE中的所有值都有奇数长度,则该函数返回x。我们可以使用两种风格:

# Procedural / Imperative
allOdd = function(x) {
  for (i in x) if (length(i) %% 2 == 0) return (FALSE)
  return (TRUE)
}

# Functional
allOdd = function(x) 
  all(length(x) %% 2 == 1)

在函数式中,要返回的值自然落在函数的末尾。再次,它看起来更像是一个数学函数。

@GSee ?ifelse中列出的警告肯定很有意思,但我认为他们并没有试图劝阻使用该功能。事实上,ifelse具有自动向量化功能的优势。例如,考虑foo的略微修改版本:

foo = function(a) { # Note that it now has an argument
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

length(a)为1时,此函数正常工作。但如果您使用foo重写了ifelse

foo = function (a) ifelse(a,a,b)

现在foo适用于a的任意长度。事实上,当a是一个矩阵时它甚至会起作用。返回与test形状相同的值是一个有助于矢量化而不是问题的特征。

答案 4 :(得分:13)

最后没有明确地放置'return'的问题是如果在方法结束时添加了额外的语句,突然返回值是错误的:

foo <- function() {
    dosomething()
}

返回dosomething()的值。

现在我们来到第二天并添加一个新行:

foo <- function() {
    dosomething()
    dosomething2()
}

我们希望我们的代码返回dosomething()的值,但不再是它。

通过明确的回报,这变得非常明显:

foo <- function() {
    return( dosomething() )
    dosomething2()
}

我们可以看到这段代码有些奇怪,并修复它:

foo <- function() {
    dosomething2()
    return( dosomething() )
}

答案 5 :(得分:5)

我认为return是一个技巧。作为一般规则,函数中计算的最后一个表达式的值将成为函数的值 - 并且在许多地方都可以找到此常规模式。以下所有评估为3:

local({
1
2
3
})

eval(expression({
1
2
3
}))

(function() {
1
2
3
})()

return所做的并不是返回一个值(无论是否完成),但以不规则的方式“突破”该函数。从这个意义上说,它是R中最接近的GOTO语句(也有中断和下一个)。我很少使用return,而且从不在函数的末尾。

 if(a) {
   return(a)
 } else {
   return(b)
 }

...这可以改写为if(a) a else b,它更易读,更少卷曲。这里根本不需要return。我使用“返回”的典型案例就像是......

ugly <- function(species, x, y){
   if(length(species)>1) stop("First argument is too long.")
   if(species=="Mickey Mouse") return("You're kidding!")
   ### do some calculations 
   if(grepl("mouse", species)) {
      ## do some more calculations
      if(species=="Dormouse") return(paste0("You're sleeping until", x+y))
      ## do some more calculations
      return(paste0("You're a mouse and will be eating for ", x^y, " more minutes."))
      }
   ## some more ugly conditions
   # ...
   ### finally
   return("The end")
   }

一般来说,需要多次回报表明问题要么是丑陋的,要么是结构不合理。

&LT;&GT;

return并不需要一个函数来运行:你可以使用它来分解一组要评估的表达式。

getout <- TRUE 
# if getout==TRUE then the value of EXP, LOC, and FUN will be "OUTTA HERE"
# .... if getout==FALSE then it will be `3` for all these variables    

EXP <- eval(expression({
   1
   2
   if(getout) return("OUTTA HERE")
   3
   }))

LOC <- local({
   1
   2
   if(getout) return("OUTTA HERE")
   3
   })

FUN <- (function(){
   1
   2
   if(getout) return("OUTTA HERE")
   3
   })()

identical(EXP,LOC)
identical(EXP,FUN)

答案 6 :(得分:5)

  

我的问题是:为什么不更快地拨打return

它更快,因为return是R中的(原始)函数,这意味着在代码中使用它会产生函数调用的开销。将此与大多数其他编程语言进行比较,在其他编程语言中,return是关键字,而不是函数调用:它不会转换为任何运行时代码执行。

也就是说,在R中以这种方式调用原始函数非常快,而调用return则导致开销很小。这不是省略return的理由。

  

还是更好,因此更可取?

因为没有理由 使用它。

因为它是多余的,并且没有添加有用的冗余。

要明确:redundancy can sometimes be useful。但是大多数冗余不是这种类型的。相反,它是在不添加信息的情况下添加visual clutter的类型:相当于filler wordchartjunk)的编程方式。

考虑以下解释性注释示例,该注释通常被认为是不好的冗余,因为该注释只是解释代码已经表达的内容:

# Add one to the result
result = x + 1

在R中使用return属于同一类别,因为R是functional programming language,并且在R中每个函数调用都有一个值。这是R的基本属性。一旦您从每个表达式(包括每个函数调用)都有一个值的角度看到R代码,问题就会变成:“为什么应该我使用return吗?”肯定有一个积极的原因,因为默认设置是不使用它。

一个积极的原因是发出信号提前退出功能,例如在guard clause中表示:

f = function (a, b) {
    if (! precondition(a)) return() # same as `return(NULL)`!
    calculation(b)
}

这是return的有效,非冗余使用。但是,与其他语言相比,此类保护子句在R中很少见,并且由于每个表达式都有值,因此常规if不需要return

sign = function (num) {
    if (num > 0) {
        1
    } else if (num < 0) {
        -1
    } else {
        0
    }
}

我们甚至可以像这样重写f

f = function (a, b) {
    if (precondition(a)) calculation(b)
}

…,其中if (cond) exprif (cond) expr else NULL相同。

最后,我想阻止三个常见的反对意见:

  1. 有人认为使用return可以增加清晰度,因为它表示“此函数返回一个值”。但是如上所述,每个函数都会在R中返回某些内容。将return视为返回值的标记不仅是多余的,而且还会误导

  2. Zen of Python的相关指导非常出色,应始终遵循:

      

    显式优于隐式。

    如何丢弃冗余return不会违反此规定?因为函数在函数式语言中的返回值始终是明确的:这是它的最后一个表达式。关于显式 vs 冗余,这再次是同一论点。

    实际上,如果您想要明确性,可以使用它来突出显示规则的异常:标记返回有意义的值的函数,这些函数仅出于它们的副作用而被调用(例如为cat)。在这种情况下,除了R具有比return更好的标记:invisible。例如,我会写

    save_results = function (results, file) {
        # … code that writes the results to a file …
        invisible()
    }
    
  3. 但是长函数呢?难道不容易追回所退回的物品吗?

    两个答案:首先,不是真的。规则很明确:函数的最后一个表达式是其值。没有什么可追踪的。

    但更重要的是,长函数的问题不是缺少明确的return标记。这是函数的长度。长函数几乎(?)始终会违反single responsibility principle,即使不这样做,它们也会因可读性的分解而受益。

答案 7 :(得分:2)

return可以提高代码的可读性:

foo <- function() {
    if (a) return(a)       
    b     
}

答案 8 :(得分:1)

在这里,关于冗余的争论很多。我认为这还不足以省略return()。 冗余并不是一件坏事。如果策略性地使用冗余,则可使代码更清晰,更可维护。

请考虑以下示例:函数参数通常具有默认值。因此,指定与默认值相同的值是多余的。除非它使我预期的行为显而易见。无需调出功能手册页即可提醒自己默认值是什么。不用担心该功能的未来版本会更改其默认值。

调用return()的性能损失可忽略不计(根据其他人在此处发布的基准),它归结为风格,而不是对与错。对于某些“错误”,必须有明显的缺点,并且这里没有人令人满意地证明包含或省略return()始终是缺点。似乎是特定于案例和特定于用户的。

这就是我坚持的立场。

function(){
  #do stuff
  ...
  abcd
}

我不喜欢上面的例子中的“孤立”变量。 abcd是否会成为我未完成撰写的声明的一部分?它是我的代码中拼接/编辑的残余,需要删除吗?我是否从其他地方不小心粘贴/移动了东西?

function(){
  #do stuff
  ...
  return(abdc)
}

相比之下,第二个示例对我来说显然是一个预期的返回值,而不是某些意外或不完整的代码。对我来说,这种冗余绝对不是没有用的。

当然,一旦函数完成并正常工作,我就可以删除返回值。但是删除它本身是多余的多余步骤,而且我认为比起首先包含return()来说,它没有更多用处。

所有这些,我不会在简短的未命名单线函数中使用return()。在那里,它占了函数代码的很大一部分,因此,大多数情况下会导致视觉混乱,使代码难以辨认。但是对于较大的正式定义和命名的函数,我会使用它,并且可能会继续这样做。