茫然的胡萝卜 · 安全简报 - 正则表达式拒绝服务攻击和防御 ...· 2 周前 · |
八块腹肌的遥控器 · 每日一问15:C++中的.h,cpp以及.h ...· 4 月前 · |
深沉的大白菜 · forEach ...· 1 年前 · |
另类的牙膏 · WPF ComboBox ...· 1 年前 · |
道上混的电影票 · macos - ...· 1 年前 · |
大鼻子的鸡蛋 · sql中where ...· 1 年前 · |
evalCpp()
转换单一计算表达式
cppFunction()
转换简单的C++函数—Fibnacci例子
sourceCpp()
转换C++程序—正负交替迭代例子
sourceCpp()
转换C++源文件中的程序—正负交替迭代例子
sourceCpp()
转换C++源程序文件—卷积例子
wrap()
把C++变量返回到R中
as()
函数把R变量转换为C++类型
as()
和
wrap()
的隐含调用
//[[Rcpp::export]]
sourceCpp()
函数中直接包含C++源程序字符串
cppFunction()
函数中直接包含C++函数源程序字符串
evalCpp()
函数中直接包含C++源程序表达式字符串
depends
指定要链接的库
invisible
要求函数结果不自动显示
clone
函数
is_na
seq_along
seq_len
pmin
和
pmax
ifelse
sapply
和
lapply
sign
diff
kable()
函数制作表格
`-`(5, 2)
。因为
-
不是合法的R变量名(函数名),
所以在写成前缀形式时要用反向单撇号
`
保护。
这样,在
lapply
等泛函中可以使用
`+`
这样的四则运算作为输入的操作。
## [1] 3
## [1] 3
为了给
1:5
每个元素减去2,可以写成
## [1] -1 0 1 2 3
用户也可以自己定义函数名如
%x%
这样的中缀函数,
可以用中缀格式调用。
## [1] "xyz123"
三、 替换形式 。 对属性的修改经常这样写,
x <- 1:2 names(x) <- c("a", "b")
## a b ## 1 2
看起来是在对一个函数的输出结果赋值, 这很不合理, 但实际相当于前缀形式的
x <- 1:2 `*tmp*` <- x x <- `names<-`(x, c("a", "b")) rm(`*tmp*`)
即制作了
x
的副本,调用names<-
函数, 将x
重新绑定到names<-
函数的返回值。四、特殊形式。
x[1]
,x[[1]]
这些取子集或元素以及修改,()
,{}
,if
结构、for
循环等本质上也是函数调用, 只不过用了特殊的语法。 这些函数在R中都是初等函数(primitive functions)。 初等函数仅在基本R中定义, 是由C代码实现的, 用户无法访问其三个部分。取子集的特殊函数例如:
## [1] 1
## [1] 1
x[1] <- 999
## [1] 999 2 3 4 5
x <- 1:5 x <- `[<-`(x, 1, 999)
## [1] 999 2 3 4 5
注意上面的
x[1] <- 999
的替代写法中, 调用`[<-`(x, 1, 999)
是将其返回值(一个向量对象)重新绑定给变量x
, 才达到了修改x
的目的。
for
循环也是函数调用, 如for(i in 1:3) print(i)
可以等价地写成`for`(i, 1:3, print(i))
。19.2 嵌套定义与句法作用域(lexical scoping)
R语言允许在函数体内定义函数。
x <- -1 f0 <- function(x){ f1 <- function(){ x + 100 f1()
其中内嵌的函数
f1()
称为一个closure(闭包)。内嵌的函数体内在读取某个变量值时, 如果此变量在函数体内还没有被赋值, 它就不是局部的, 会向定义的外面一层查找, 外层一层找不到,就继续向外查找。 上面例子
f1()
定义中的变量x
不是局部变量, 就向外一层查找, 找到的会是f0
的自变量x
,而不是全局空间中x
。## [1] 101
最后
x+100
中x
取的是f0
的实参值x=1
, 而不是全局变量x=-1
。这样的变量查找规则叫做句法作用域(lexical scoping), 即函数运行中需要使用某个变量时, 从其定义时的环境向外层逐层查找, 而不是在调用时的环境中查找。
## [1] 99
其中
f2()
运行时, 用到的x
是f1()
函数体内的局部变量x=-1
, 而不是被调用时f0()
函数体内的局部变量x=1000
, 所以结果是-1 + 100 = 99
。“句法作用域”指的是函数调用时查找变量是查找其定义时的变量对应的存储空间, 而不是定义时变量所取的历史值。 函数运行时在找到某个变量对应的存储空间后, 会使用该变量的当前值,而不是函数定义的时候该变量的历史值。 这种规则称为动态查找(dynamic lookup)。
## [1] 1100
结果为什么不是
-1 + 100 = 99
而是1000 + 100 = 1100
? 这是因为,f1()
在调用时, 使用的x
是f0
函数体内局部变量x
的值, 但是要注意的是程序运行时会访问该变量的当前值,即1000, 而不是函数定义的时候x
的历史值-1。 句法作用域与动态查找一个说的是如何查找某个变量对应的存储空间, 一个说的是使用该存储空间何时的存储值, 程序运行时两个规则需要联合使用。句法作用域不仅适用于查找变量, 也适用于函数体内调用别的函数时查找函数。 查找函数的规则与查找变量规则相同。
19.3 辅助嵌套函数
有时内嵌函数仅仅是函数内用来实现模块化的一种工具, 和正常的函数作用相同,没有任何特殊作用。 例如,如下的程序在自变量
x
中输入一元二次方程\(a x^2 + b x + c = 0\)的三个系数,solve.sqe <- function(x){ fd <- function(a, b, c) b^2 - 4*a*c d <- fd(x[1], x[2], x[3]) if(d >= 0){ return( (-x[2] + c(1,-1)*sqrt(d))/(2*x[1]) ) } else { return( complex(real=-x[2], imag=c(1,-1)*sqrt(-d))/(2*x[1]) )
在这个函数中内嵌的函数
fd
仅起到一个计算二次判别式的作用, 没有用到任何的闭包特性, 其中的形参变量a, b, c
都是局部变量。## [1] 1 1
## [1] 2 0
## [1] 1+1i 1-1i
这样的内嵌函数与直接在全局空间中定义的函数区别不大, 只有一条区别: 只能在定义它的函数内运行, 不能被直接调用, 可以看成是函数内的私有函数, 可以避免名字冲突。
19.4 懒惰求值
R函数在调用执行时, 除非用到某个形式变量的值才求出其对应实参的值。 这一点在实参是常数时无所谓, 但是如果实参是表达式就不一样了。 形参缺省值也是只有在函数运行时用到该形参的值时才求值。
## [1] -1110
可以看出,虽然形参
x
输入的实参值为5, 但是这时形参y
并没按x=5
被赋值为TRUE
, 而是到函数体中第二个语句才被求值, 这时x
的值已经变成了-111, 故y
的值是FALSE
。另外要注意的是, 懒惰求值使得缺省值在初次访问时, 是在函数内的环境(局部变量作用域)内求值的, 不是在其调用处的环境内求值。
在函数内部, 用
missing(x)
对形参x
判断用户是否没有提供对应的实参, 对位置形参和有缺省值的形参都适用。19.5 程序调试
19.5.1 基本调试策略
自己编写的某些涉及到复杂的算法的程序可能一开始并不能给出期望的结果。 这包括如下的情况:
程序报错, 需要找到出错的地方加以纠正; 程序正常运行, 输出了结果, 但是结果明显有错; 最糟糕的是, 程序结果也看起来很正常, 但实际结果是错误的。 以上这三种情况是依次越来越糟糕的。 直接运行出错的情况一般是比较容易处理的。
为了尽可能保证程序结果正确, 在自己编写新算法时, 要运用模块化思想, 将问题分解为若干个小问题, 使得每一个小问题都比较容易验证结果是否正确, 将每一个小问题编写成一个单独的函数, 这样也可以避免一段很长的程序中许多变量混杂在一起。
在划分模块并编写好程序后, 应该编写一系列的测试函数, 对每个函数进行测试, 保证其在各种情况下的结果是正确的。 最好采纳R的规则化的测试策略进行自动化测试, 在编写R扩展包时就推荐同时提供测试代码。
如果程序还是有错误, 首先可以求助于搜索引擎、用户社区等。 如果这个问题是常见问题, 往往这样就可以解决问题。
如果问题没有解决, 需要将问题最小化: 减少引起错误的程序的复杂程度, 将不必要的代码尽可能用固定的输入数据代替, 使得出错程序很短, 而且错误可重复。 有时会有不可重复的错误, 这样的错误往往很难解决, 超出了一般R用户的能力。
在将问题程序简化并且错误可重复以后, 就要试图定位错误。 一般编程语言都有如下的一些一般性查错(debugging)方法:
在程序中适当位置加上输出命令(语句), 输出怀疑有错的变量值。 某些变成语言提供了跟踪以及条件跟踪命令, 可以在程序运行到某个语句或者触发了某个条件时程序中止, 但允许用户控制逐行运行程序并随时查看变量值, 称为跟踪调试(tracing)。 跟踪调试可以是命令行工具, 也可以是集成在RStudio这样的集成编程环境中的图形界面工具。 在查错时, 科学研究思维照样起作用: 根据错误表现提出可能的原因假设, 制作测试输入数据验证假设, 记录相应输出并分析是否与假设的错误原因吻合。 如此反复直到找到出错原因并加以纠正。
查错并纠正就可能会破坏了成熟的代码, 造成新的错误, 所以最好能有自动化的测试策略, 在每次修改程序后都重新测试程序是否保持输出正确。
19.5.2 找到出错的函数
在较复杂的程序出错时, 需要首先将错误定位到某一函数调用。如:
为了在多层次函数调用中找到出错的函数,可以用如下命令:
结果是所谓的反向追踪(traceback), 一般编程语言中称为调用栈(calling stack)。 这个输出是从下向上堆叠显示, 下层是调用方, 上层是被调用方。
在RStudio中运行时, 出错程序的右端可以显示“Show Traceback”以及“Rerun with Debug”快捷图标, 点击“Show Traceback”图标也可以显示反向追踪结果。 如果是一个源文件用source命名或图标运行时出错, 在显示反向追踪结果时还可以显示调用的各命令所在的程序行号。 点击“Rerun with Debug”可以进入跟踪调试状态, 显示出错时的运行环境中的变量值。
19.5.3 跟踪调试
R和RStudio提供了很好的跟踪运行程序的能力。 R的
browser()
命令可以用在程序中, 命令进入跟踪调试; RStudio的源文件显示界面可以用鼠标点击定义跟踪调试位置。函数定义一般都包含多行,所以一般不在命令行定义函数, 而是把函数定义以及较长的程序写在源程序文件中, 用
source
命令运行。 用source
命令调入运行的程序与在命令行运行的效果基本相同, 这样定义的变量也是全局变量。考虑如下函数定义:
f <- function(x){ for(i in 1:n){ s <- s + x[i]
这个函数定义有许多问题。 用一个测试输入调用
f
,发现有错误:简单的函数可以直接仔细检查发现错误, 用
cat
,browser()
函数, 在程序中插入对browser()
函数的调用, 可以进入跟踪调试状态, 可以实时地查看甚至修改运行时变量的值。在RStudio的编辑窗口中打开.R源程序文件, 在某一程序行行号左端的空白处用鼠标单击, 就可以设定某一行为断点, 在用
source
命令运行到该处时就可以进入跟踪调试状态。加入
browser()
命令后的程序如:f <- function(x){ browser() for(i in 1:n){ s <- s + x[i]
程序运行遇到
browser()
函数或设定的断点时程序进入跟踪调试状态, 命令行的提示符变成“Browse[1]>”。 这个命令行的环境一般不再是全局环境, 而是断点所在的函数的运行环境, 可以看到函数的局部变量。 可以在调试环境中用命令去查看当前定义的变量值、逐个命令地运行, 但是用RStudio则可以更方便地执行这些操作。在调试命令行,可以使用如下命令:
输入变量名查看变量值; 用 n
命令或者换行键逐句运行;用 s
命令跟踪进调用的函数内部逐句运行;用 f
命令快速执行到循环末尾或者函数末尾;用 c
命令恢复正常运行,不再跟踪;用 Q
命令强行终止程序运行。进入调试状态后, RStudio界面提供了相应的支持。 这时RStudio的命令行窗格(Console)将会显示用于控制运行的图标, 包括执行下一语句(Next)、跟踪进入要调用的函数运行(Step into)、执行到函数末尾或者循环末尾(Finish)、不再跟踪继续正常运行(Continue)、终止运行(Stop)。 在RStudio的Environment窗格中会显示当前执行的命令所在的运行环境的内容, 包括函数内的局部变量; 如果点击其中的下拉菜单还可以显示函数的各层父环境。 在同一窗格中还会显示向后追踪(Traceback), 即函数是如何被调用的。
为调试如上函数
f
的程序, 在定义中插入对browser()
的调用如:f <- function(x){ browser() for(i in 1:n){ s <- s + x[i]
当在RStudio中调试运行时, 程序编辑窗口将显示当前要运行的程序行, 用命令行窗口(Console)的Next快捷图标可以运行到下一行。 命令行的显示如:
> print(f(1:5)) Called from: eval(expr, p) Browse[1]> n debug在D:/disk/projects/Rbookweb/tmp2.R#2: for (i in 1:n) { s <- s + x[i] Browse[2]>
继续用“Next”图标运行,命令行结果如:
Browse[2]> n Error in 1:n : NA/NaN参数
发现是在
for(i in 1:n)
行遇到未定义的变量n
。在源文件中把出错行改为
for(i in 1:length(x))
, 再次运行, 发现在运行s <- s + x[i]
行时, 遇到“错误: 找不到对象’s’”。 这是忘记初始化引起的。 在for
语句前添加s <- 0
语句,函数定义变成:f <- function(x){ browser() s <- 0 for(i in 1:length(x)){ s <- s + x[i]
再次运行, 在跟踪到循环时, 为了避免繁琐的跟踪过程, 可以用“执行到函数末尾或者循环末尾”快捷图标或命令行的
f
命令, 或者“Continue”快捷图标或命令行的c
命令。 程序不显示错误但是也没有显示结果为NULL
而不是我们期望得输入元素之和。 检查可以看出错误是忘记把函数返回值写在函数定义最后。在函数定义最后添加
s
一行, 再次运行,程序结果与手工验算结果一致。f <- function(x){ browser() n <- length(x) s <- 0 for(i in 1:n){ s <- s + x[i]
自定义函数应该用各种不同输入测试其正确性和稳定性。 比如,上面的函数当自变量x为零长度向量时应该返回0才合适, 但是上面的写法会返回一个
numeric(0)
结果, 这个结果表示长度为零的向量:程序输入了零长度自变量, 我们期望其输出为零而不是
numeric(0)
。 在自变量x为零长度时, 函数中for(i in 1:length(x)
应该一次都不进入循环, 跟踪运行可以发现实际对i=1和i=0共运行了两轮循环。 把这里的1:length(x)
改成seq_along(x)
解决了问题,seq_along(x)
生成x的下标序列, 如果x是零长度的则下标序列为零长度向量。函数不需要修改后, 可以把对
browser()
的调用删除或注释掉, 在RStudio中关闭断点。 函数最终修改为:f <- function(x){ s <- 0 for(i in seq_along(x)){ s <- s + x[i]
这里只是用这个简单函数演示如何调试程序, 求向量元素和这个问题本身是不需要我们去定义新函数的,
sum
函数本来就是完成这样的功能。 实际上,许多我们认为需要自己编写程序做的事情, 在R网站都能找到别人已经完成的扩展包。19.5.4 条件断点
用
browser()
函数与R的if
结构配合可以制作条件断点。 在调试带有循环的程序时, 发现错误发生在循环内, 如果从循环开始就跟踪运行, 会浪费许多时间。 设已知错误发生在循环变量i
等于501的时候, 就可以在循环内插入:这样就可以在更接近出错的位置进入跟踪运行。
19.5.5 开启对一个函数的调试
可以用
debug(f)
命令对函数f
开启跟踪运行, 这时每次调用f()
时都会自动进入跟踪运行状态。 用undebug(f)
取消对f
的这种操作。19.5.6 出错调试选项
比较长的程序在调试时如果从开头就跟踪, 比较耗时。可以设置成出错后自动进入跟踪模式, 检查出错时的变量值。只要进行如下设置:
则在出错后可以选择进入出错的某一层函数内部, 在browser环境中跟踪运行。
在RStudio中调试某一源程序文件时, 可以选择“Debug–On Error”菜单, 并选择“Break in Code”, 就可以在出错时自动在出错处进入跟踪状态。
例如,用
options()
函数进行设置后, 前面那个求向量元素和的例子程序按最初的定义, 运行时出现如下的选择:## Error in f(1:5) : object 'n' not found ## Enter a frame number, or 0 to exit ## 1: f(1:5) ## Selection: f(1:5) ## Selection: 1 ## Called from: top level ## Browse[1]>
在
Selection
后面输入了1,就进入了函数内部跟踪。 用Q终止运行并退出整个browser跟踪。 当函数调用函数时可以选择进入哪一个函数进行跟踪。 如果在RStudio中设置了“Break in Code”, 会自动在出错处进入跟踪运行状态。19.5.7
stop()
、warning()
、message()
编写程序时应尽可能提前发现不合法的输入和错误的状态。 发现错误时, 可以用
stop(s)
使程序运行出错停止, 其中s
是一个字符型对象, 用来作为显示的出错信息。发现某些问题后如果不严重, 可以不停止程序运行, 但用
warning(s)
提交一个警告信息, 其中s
是字符型的警告信息。 警告信息的显示可能比实际运行要延迟一些。有些警告信息实际是错误, 用
options()
的warn
参数可以设置警告级别, 如设置warn=2
则所有警告当作错误处理。其中
warn=0
是默认做法,warn=1
表示不延迟显示。函数
message()
与stop()
、warning()
类似, 不算是错误或者警告, 是提示性的信息输出。message()
不会像warning()
那样延迟显示。 长时间的等待之前给出一个提示, 正在读写文件或者网络时给出提示, 与cat()
等相比较,cat()
是用户要求的输出, 而message()
是程序员对用户的提示。19.5.8 预防性设计
在编写自定义函数时, 可以检查自变量输入以确保输入符合要求。 函数
stopifnot
可以指定自变量的若干个条件, 当自变量不符合条件时自动出错停止。例如,函数
f()
需要输入两个数值型向量x, y, 需要长度相等, 可以用如下的程序f <- function(x, y){ stopifnot(is.numeric(x), is.numeric(y), length(x)==length(y)) ## 函数体程序语句...
19.5.9 出错处理机制
R运行可以用
stop()
产生错误状态, 停止运行; 用warning()
产生警告, 用message()
产生提示。 基本R提供了tryCatch()
函数, 用来保护可能出错的代码, 并可以指定出错时用的恢复或诊断代码。try()
函数也可以保护可能出错的代码, 使得出错时错误信息照样显示但不中断运行。扩展包rlang提供了一些对出错处理机制的增强功能。 详见(Hadley Wickham 2019)第8章:Conditions。
19.6 单元测试
现代程序设计的最重要的思想是模块化设计。 将整个任务分解为功能明确的多个小模块, 在R中用函数表示, 确保每个函数的功能正确就可以使得整个程序正确。
在程序的反复修改调试过程中, 程序员会使用许多调试代码测试函数是否正确运行, 这些测试代码也是大的编程项目的重要内容, 所以应该将这些测试代码收集起来放到一个或若干个程序文件中, 每次程序修改后都重新运行这些测试代码, 看结果是否改变了。
人工地去检查每个测试的结果是否正确过于繁琐了, 没有很好地发挥计算机的自动执行繁琐任务的能力。 应该将测试时调用每个函数的正确结果与测试代码放在一起, 直接判断函数的输出是否符合预期。 R的testthat扩展包提供了这样的自动化测试的功能。 这个功能主要用于R扩展包的开发测试, 但是较复杂的一般R程序也可以使用。
testthat包用
test_that()
执行测试, 用expect_equal()
等函数规定所测试的函数(代码)应有的行为。例如,自定义了如下的计算简单收益率的函数:
simple.return <- function(x){ x <- as.vector(x) c(NA, diff(x) / x[1:(length(x)-1)])
单独写一个包含测试的R文件,如
test-return.R
,library(testthat) test_that("simple.return calculates simple return", { expect_equal(simple.return(c(1, 2, 3)), c(NA, 1, 0.5)) expect_equal(simple.return(ts(c(1, 2, 3))), c(NA, 1, 0.5)) expect_equal(simple.return(ts(c(1, 2, 3), start=c(2000,1), frequency=12)), c(NA, 1, 0.5))
结果显示
Test passed
。 有多个测试时会显示有多少个通过,多少个失败, 多少个被跳过, 失败的测试会给出出错程序位置。 在RStudio中运行时还用不同颜色区分通过、失败和跳过。 一个测试文件中可以多个test_that()
调用, 每个调用测试一个特定的功能, 每个test_that()
调用中有若干个exepect_xxx()
调用进行测试。 除了expect_equal()
以外, 还有expect_match()
,expect_true()
,expect_output()
等测试。 详见(Hadley Wickham and Bryan 2022)第12章:Testing。19.7 环境
环境是R语言比较困难的概念, 一般用户也不需要了解环境就能很好地使用R, 也不影响自定义函数。 环境是支持变量作用域、命名空间、R6类型等功能的数据结构, 了解环境有助于更好地理解作用域等概念。
这部分内容主要来自(Hadley Wickham 2019)相应章节。
19.7.1 基本概念
19.7.1.1 基本认识
环境作为一个数据结构与有名的列表相似, 但是其中的名字必须都互不相同, 且没有次序(类似集合), 环境都有一个父环境, 修改环境内容时都不制作副本。
rlang扩展包可以比较方便地操作R的语法内容。 可以用
rlang::env()
生成新的环境, 这类似于list()
函数的用法,