非標準計算

在前面幾節中,咱們學習瞭如何使用 quote( ) 和 substitute( ) 將表達式捕獲爲
語言對象,以及如何使用 eval( ) 在給定列表或環境中計算表達式。這些函數組成
了 R 中元編程的基本功能,這使咱們可以調整標準計算。元編程的主要應用是執行非標
準計算以使某些特定用法更容易。接下來的內容中,咱們將討論幾個例子,以便對它的
工做方式有一個更好的理解。
1.使用非標準計算快速構建子集
咱們常常須要從向量中取出某個子集。子集的範圍多是前幾個元素、後幾個元素,
或是中間的元素。
前兩種狀況用 head(x, n) 和 tail(x, n) 很容易解決。第 3 種狀況須要輸入向量
的長度。
例如,假設有一個整數向量,咱們想從中提取第 3 個到倒數第 5 個,這 3 個元素:
x <- 1:10
x[3:(length(x) -5)]
## [1] 3 4 5
上面提取子集的表達式用了兩次 x,看起來有些繁瑣。咱們能夠定義一個快速取子集
的函數,使用元編程工具提供一個特殊符號來引用輸入向量的長度。下面這個函數 qs( )
是這個想法的簡單實現,它容許咱們使用點( . )來表示輸入向量的長度:
qs <- function(x, range) {
range <- substitute(range)
selector <- eval(range, list(. =length(x)))
x[selector]
}
使用這個函數,咱們能夠用 3:(.-5) 來表示相同的範圍:
qs(x, 3:(. -5))
## [1] 3 4 5
也能夠經過倒數(逆序)的方式來選取元素:
qs(x, . -1)
## [1] 9
基於 qs( ),下面這個函數用於修剪向量 x 兩端的 n 個元素。也就是說,返回剔除
前 n 個和後 n 個元素後的向量 x 的中間部分:
trim_margin <- function(x, n) {
qs(x, (n +1):(. -n -1))
}
這個函數看起來彷佛還不錯,可是當咱們輸入一個值調用它時,卻發生了錯誤:
trim_ _margin(x, 3)
## Error in eval(expr, envir, enclos): 找不到對象'n'
爲何會找不到 n 呢?要理解爲何發生這種狀況,咱們須要分析在調用trim_margin( )
時符號的查找路徑。下一節將詳細說明這一點,而且介紹動態做用域( dynamic scoping )
的概念來解決這個問題。
2.動態做用域
在嘗試解決這個問題以前,咱們先用以前學到的知識來分析哪裏出錯了。當調用
trim_margin(x, 3)時,就是在一個新的執行環境裏調用 qs(x,(n+1):(.-n-1)),參數
爲x 和n。qs( )在這裏比較特殊,由於它使用了非標準計算。更具體地說,它首先捕獲 range
做爲語言對象,而後基於提供的額外符號的列表來對其求值,本例中列表僅包含.=length(x)。
錯誤就發生在 eval(range,list(.=length(x)))上。此處找不到須要修剪的邊
緣元素的數目 n,那必定是封閉環境哪裏有問題。如今,咱們仔細觀察 eval( )函數的
enclos 參數的默認值:
eval
## function (expr, envir = parent.frame(), enclos = if (is.list(envir) ||
## is.pairlist(envir)) parent.frame() else baseenv())
## .Internal(eval(expr, envir, enclos))
## <bytecode: 0x00000000106722c0>
## <environment: namespace:base>
eval( ) 的定義說明,若是咱們給 envir 提供一個列表 — 正如前面所作的,
enclos 會默認取 parent.frame( ),而這是 eval( )的調用環境,也就是調用 qs( )
時的執行環境。而 qs( ) 的執行環境中固然沒有 n。
這裏,咱們發現了在 trim_margin( )中使用 substitute( )的一個缺點,由於
表達式只有在正確的語境下才是徹底有意義的,即 trim_margin( )的執行環境,同時
也是 qs( )的調用環境。不幸的是,substitute( )只捕獲表達式,而不捕獲使表達式
有意義的環境。所以,咱們必須本身完成這一步。
如今,知道了問題所在。解決辦法很簡單,就是始終使用正確的封閉環境,即定義被捕獲
的表達式的環境。在本例中,咱們指定 enclos = parent.frame( ),以便 eval( )在提
供了n 的qs( )的調用環境(即trim_margin( )的執行環境)查找除了 . 之外的全部符號。
下面這行代碼是 qs( )的修改版本:
qs <- function(x, range) {
range <- substitute(range)
selector <- eval(range, list(. =length(x)), parent.frame())
x[selector]
}
使用以前報錯的代碼從新測試該函數:
trim_ _margin(x, 3)
## [1] 4 5 6
如今,該函數可以用正確的方式運行了。事實上,這個機制就是動態做用域。回想一
下上一章學到的知識:每次調用函數時都會建立一個執行環境。若是一個符號在執行環境
中找不到,就會去封閉環境中搜索。
根據在標準計算中用到的詞法做用域機制,函數的封閉環境在函數被定義時就已肯定,
而且定義函數的環境也被肯定。
然而,與之相反的是,根據非標準計算用到的動態做用域機制,封閉環境應是調用環
境,在這個調用環境中定義了被捕獲的表達式,這樣就能夠在自定義的執行環境或封閉環
境及其父環境中找到相關符號。
總之,當一個函數使用非標準計算時,正確實現動態做用域機制是很重要的。
3.使用公式來捕獲表達式和環境
爲了正確實現動態做用域機制,咱們使用 parent.frame( )來追蹤 substitute( )
捕獲的表達式。一個更簡單的辦法是用公式同時捕獲表達式和環境。
在第 7 章中,咱們看到公式常常被用來表示變量之間的關係。大多數模型函數(如lm( ))
接收一個公式來指定響應變量和解釋變量之間的關係。
實際上,公式對象比這簡單得多。它會自動捕獲 ~ 符號兩邊的表達式以及建立它的環
境。例如,咱們能夠直接建立一個公式並存儲在一個變量中:
formula1 <- z ~ x ^2 + y ^2
能夠看到公式本質上是屬於 formula 類的語言對象:
typeof(formula1)
## [1] "language"
class(formula1)
## [1] "formula"
若是咱們將公式轉換爲列表,就能夠仔細查看它的結構:
str(as.list(formula1))
## List of 3
## $ : symbol ~
## $ : symbol z
## $ : language x^2 + y^2
## - attr(*, "class")= chr "formula"
## - attr(*, ".Environment")=< environment: R_GlobalEnv>
能夠看到formula1不只將 ~ 兩側的表達式捕獲爲語言對象,還捕獲了建立它的環境。
實際上,公式就只是一個基於被捕獲的參數和調用環境的函數( ~ )調用。若是指定了 ~ 的
兩側,調用的長度便爲 3 :
is.call(formula1)
## [1] TRUE
length(formula1)
## [1] 3
要訪問被捕獲的語言對象,咱們能夠提取第 2 個和第 3 個元素:
formula1[[2]]
## z
formula1[[3]]
## x^2 + y^2
要訪問建立該調用的環境,可使用 environment( ):
environment(formula1)
## <environment: R_GlobalEnv>
公式也能夠是右側型的,即只指定 ~ 的右邊。示例以下:
formula2 <- ~x +y
str(as.list(formula2))
## List of 2
## $ : symbol ~
## $ : language x + y
## - attr(*, "class")= chr "formula"
## - attr(*, ".Environment")=<environment: R_GlobalEnv>
本例中,咱們只提供並捕獲了 ~ 的一個參數,因此有一個包含兩個語言對象的調用,
能夠經過提取第 2 個元素來訪問被捕獲表達式:
length(formula2)
## [1] 2
formula2[[2]]
## x + y
瞭解了公式如何工做以後,就能夠用公式實現qs( )和trim_margin( )的另外一個版本。
當 range 是一個公式時,下面這個函數 qs2( )與 qs( )的運行方式一致;不然它就
直接用 range 來提取 x 的子集:
qs2 <- function(x, range) {
selector <- if (inherits(range, "formula")) {
eval(range[[2]], list(. =length(x)), environment(range))
} else range
x[selector]
}
注意到,咱們使用 inherits(range, "formula")檢查 range 是否是一個公式,並
且用 environment (range)實現動態做用域。而後,用一個右側型公式來激活非標準計算:
qs2(1:10, ~3:(. -2))
## [1] 3 4 5 6 7 8
或者,也可使用標準計算:
qs2(1:10, 3)
## [1] 3
如今,咱們能夠藉助使用公式的 qs2( )來從新實現 trim_margin( ):
trim_margin2 <- function(x, n) {
qs2(x, ~ (n +1):(. -n -1))
}
能夠驗證,動態做用域機制正常運做,由於 trim_margin2( )中使用的公式自動捕
獲執行環境(也是定義公式和 n 的環境):
trim_ _margin2(x, 3)
## [1] 4 5 6
4.使用元編程構建子集
瞭解了語言對象、求值函數和動態做用域機制後,咱們如今就能夠實現 subset 的另
一種版本。
這個實現的基本想法很簡單:
• 捕獲行構建子集表達式,並在數據框內對其求值,數據框本質上是一個列表;
• 捕獲按列選取的表達式,並在整數索引的命名列表中對其求值;
• 使用行選擇器(邏輯向量)和列選擇器(整數向量)對數據框選取子集。
這裏給出上述邏輯的一種實現:
subset2 <- function(x, subset =TRUE, select =TRUE) {
enclos <- parent.frame()
subset <- substitute(subset)
select <- substitute(select)
row_selector <- eval(subset, x, enclos)
col_envir <- as.list(seq_ _along(x))
names(col_envir) <- colnames(x)
col_selector <- eval(select, col_envir, enclos)
x[row_selector, col_selector]
}
按行構建子集要比按列更容易實現。要執行按行構建子集,咱們只須要捕獲 subset
並在數據框內對其求值便可。
按列構建子集則比較棘手,要給列建立一個整數索引列表,並給它們賦予相應的名稱。
例如,一個具備 3 列(如 x,y,z)的數據框須要這樣一個索引列表:list(a = 1, b = 2,
c = 3),這使咱們可以以 select = c(x, y) 的形式選取列,由於 c(x, y) 是在列表
內被計算的。
如今,subset2( ) 的運行方式就很是接近內置函數 subset( ) 了:
subset2(mtcars, mpg >= quantile(mpg, 0.9), c(mpg, cyl, qsec))
## mpg cyl qsec
## Fiat 128 32.4 4 19.47
## Honda Civic 30.4 4 18.52
## Toyota Corolla 33.9 4 19.90
## Lotus Europa 30.4 4 16.90
兩種實現都容許咱們用 a:b 來選取 a 和 b 之間的全部列,包括 a 和 b:
subset2(mtcars, mpg >= quantile(mpg, 0.9), mpg:drat)
## mpg cyl disp hp drat
## Fiat 128 32.4 4 78.7 66 4.08
## Honda Civic 30.4 4 75.7 52 4.93
## Toyota Corolla 33.9 4 71.1 65 4.22
## Lotus Europa 30.4 4 95.1 113 3.77編程

相關文章
相關標籤/搜索