为何在lapply包装函数中调用subset_base会找不到对象zzz?
subset_base是base R中subset()函数的简化版本,示例出自《Advanced R》第20.6.1章节。单独调用可正常运行:
subset_base <- function(data, rows) { rows <- substitute(rows) rows_val <- eval(rows, data, parent.frame()) data[rows_val, , drop = FALSE] } my_df <- data.frame(x = 1:3) subset_base(my_df, x == 1) #> x #> 1 1
但构建包装函数apply_subset时,在其中定义变量zzz,再调用lapply执行subset_base并传入x == zzz会报错找不到zzz:
apply_subset <- function(){ zzz <- 2 dfs <- list(data.frame(x = 1:3), data.frame(x = 4:6)) lapply(dfs, FUN = subset_base, x == zzz) } apply_subset() #> Error in eval(rows, data, parent.frame()): object 'zzz' not found
而把subset_base包裹在匿名函数中传入lapply就能正常找到zzz:
apply_subset2 <- function(){ zzz <- 2 dfs <- list(data.frame(x = 1:3), data.frame(x = 4:6)) lapply(dfs, FUN = \(df) subset_base(df, x == zzz)) } apply_subset2() #> [[1]] #> x #> 2 2 #> #> [[2]] #> [1] x #> <0 rows> (or 0-length row.names)
1. 调用栈中每个执行环境的父环境是什么
报错场景(apply_subset)的环境链:
subset_base的执行环境:父环境是**lapply的内部执行环境**(lapply调用subset_base时创建的临时环境)lapply的内部执行环境:父环境是全局环境(base函数的执行环境父环境默认绑定全局环境)apply_subset的执行环境:父环境是全局环境- 全局环境:父环境是base环境
正常场景(apply_subset2)的环境链:
subset_base的执行环境:父环境是匿名函数的执行环境- 匿名函数的执行环境:父环境是**
apply_subset2的执行环境**(匿名函数定义时绑定了所在的apply_subset2环境,属于闭包特性) apply_subset2的执行环境:父环境是全局环境
2. 原假设(父环境层级可找到zzz)错误的原因
问题出在lapply的参数传递逻辑和parent.frame()的指向:
当你在lapply中直接写x == zzz作为额外参数时,这个表达式是在apply_subset的环境中被捕获,但subset_base里的parent.frame()返回的是**subset_base的直接调用者环境**——也就是lapply的内部执行环境,而非apply_subset的环境。
eval(rows, data, parent.frame())的第三个参数指定了查找非data内变量的环境,而lapply的内部环境里没有zzz,自然会报错。你原本以为环境链会回溯到apply_subset,但lapply作为中间调用者,直接切断了这个链路。
3. 使用匿名函数后情况改变的原因
匿名函数相当于做了一层“环境中转”:
x == zzz这个表达式是在匿名函数的执行环境中传递给subset_base的subset_base的直接调用者变成了匿名函数,此时parent.frame()返回的是匿名函数的执行环境- 匿名函数是在
apply_subset2内部定义的,它的父环境就是apply_subset2的执行环境(闭包会保留定义时的环境) - 所以顺着环境链,就能找到
apply_subset2里定义的zzz
简单说,匿名函数把apply_subset2的环境“带”到了subset_base的调用上下文里,补上了被lapply打断的环境链。
4. 不使用匿名函数使lapply调用生效的方法
核心是要把表达式和它的定义环境绑定在一起,也就是使用rlang包的quosure(闭包表达式),这也是《Advanced R》推荐的方案:
第一步:修改subset_base为支持quosure的版本
library(rlang) subset_quosure <- function(data, rows) { # enquo捕获表达式和其定义环境,打包成quosure rows <- enquo(rows) # eval_tidy在数据环境+quosure绑定的环境中求值 rows_val <- eval_tidy(rows, data) data[rows_val, , drop = FALSE] }
第二步:修改apply_subset函数
apply_subset_quosure <- function(){ zzz <- 2 dfs <- list(data.frame(x = 1:3), data.frame(x = 4:6)) # 先捕获表达式和环境为quosure rows_expr <- quo(x == zzz) # 用!!解引用quosure传递给函数 lapply(dfs, FUN = subset_quosure, rows = !!rows_expr) } apply_subset_quosure() #> [[1]] #> x #> 2 2 #> #> [[2]] #> [1] x #> <0 rows> (or 0-length row.names)
quosure会把表达式和它的定义环境(也就是apply_subset_quosure的环境)打包在一起,不管中间经过多少层调用,eval_tidy()都能正确找到zzz所在的环境,从而避免作用域问题。
内容的提问来源于stack exchange,提问作者TimTeaFan

