R语言4.1.0版本引入了原生的管道操作符|>
,它的工作原理与magrittr
包中提供的管道操作符%>%
类似。例如,在下面这个例子中,LHS |> RHS
和LHS %>% RHS
这两种写法,均可以将LHS的输出结果作为RHS第一个参数的输入值,从而实现流程化的管道操作效果。
library(magrittr)
library(tidyverse)
## 使用R语言原生的管道操作符 |>
|>
diamonds group_by(cut) |>
summarise(across(.col = c("carat", "price"),
.fns = sum,
.names = "sum_of_{.col}")) |>
mutate(price_per_carat = sum_of_price/sum_of_carat)
# A tibble: 5 × 4
cut sum_of_carat sum_of_price price_per_carat
<ord> <dbl> <int> <dbl>
1 Fair 1684. 7017600 4167.
2 Good 4166. 19275009 4627.
3 Very Good 9743. 48107623 4938.
4 Premium 12301. 63221498 5140.
5 Ideal 15147. 74513487 4919.
## 使用magrittr包提供的管道操作符 %>%
%>%
diamonds group_by(cut) %>%
summarise(across(.col = c("carat", "price"),
.fns = sum,
.names = "sum_of_{.col}")) %>%
mutate(price_per_carat = sum_of_price/sum_of_carat)
# A tibble: 5 × 4
cut sum_of_carat sum_of_price price_per_carat
<ord> <dbl> <int> <dbl>
1 Fair 1684. 7017600 4167.
2 Good 4166. 19275009 4627.
3 Very Good 9743. 48107623 4938.
4 Premium 12301. 63221498 5140.
5 Ideal 15147. 74513487 4919.
虽然从结果上看,二者并没有什么区别,但原生的管道操作符|>
在实际使用时有些需要注意的地方。为了说明这点,需要先回顾一下%>%
的一些特性。
一、%>%
的一些特性
1. %>%
是一个function
R语言中创建function需要用到function()
函数,大多数时候我们都会将这个函数的结果赋值给一个变量,例如my_func <- function(x) { ... }
这行代码创建了一个名为my_func
的function,使用时则需要调用my_func(x)
来复现{ ... }
中的逻辑。
实际上R语言还有另外一种创建function的方式,作为使用者虽然用的不多,但作为开发者则会经常用到,那就是类似+
(加)、-
(减)、*
(乘)、/
(除)这样的function。这种function最大的特点就在于调用时不需要加()
,且参数是写在function的左右两端,例如下面这个例子:
## 创建一个名为%A_and_B%的function
`%A_and_B%` <- function(A, B) { paste(A, B, sep = " and ") }
## 调用%A_and_B%
"矿爹" %A_and_B% "杨姐"
[1] "矿爹 and 杨姐"
可以看到,我们创建了一个名为%A_and_B%
的function,这个function的功能是将参数A
和B
通过and连接起来,但是其调用方式与传统的my_func(x)
不同,我们直接将A
和B
分别写在这个function的左右两边("矿爹" %A_and_ _B% "杨姐"
)即可实现调用。
与这个例子类似的,我们可以看出管道操作符%>%
实际上是magrittr
包提供的一个function,它与一般的function除了在调用方式上不同以外,并没有什么本质区别。
2.
使用%>%
时如果不想传参给右侧第一个输入怎么办?
在开头部分我们提到:
LHS |> RHS
和LHS %>% RHS
这两种写法,均可以将LHS的输出结果作为RHS第一个参数的输入值,从而实现流程化的管道操作效果。
那如果不想把左侧的结果传给右侧的第一个参数,而是传给右侧某个指定的参数,要如何实现呢?
答案是:使用占位符.
。我们来看下面这个例子:
head(iris) # 鸢尾花数据
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 5.1 3.5 1.4 0.2 setosa
2 4.9 3.0 1.4 0.2 setosa
3 4.7 3.2 1.3 0.2 setosa
4 4.6 3.1 1.5 0.2 setosa
5 5.0 3.6 1.4 0.2 setosa
6 5.4 3.9 1.7 0.4 setosa
%>%
iris plot(Sepal.Width ~ Sepal.Length, col = Species) # 这样作图会报错
Error in pairs.default(data.matrix(x), ...): object 'Species' not found
上面作图的代码之所以会报错,是因为plot()
函数中的第一个参数不是data
,而我们希望把iris
数据传给data
,此时便可以使用占位符.
来实现我们想要的效果:
%>%
iris plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
由于占位符.
可以代替左侧的输出结果,那我们自然会想到,如果将上面的代码换一种写法,是不是也能成功呢?
%>%
iris plot(x = .$Sepal.Length, y = .$Sepal.Width, col = .$Species)
Error in plot.xy(xy, type, ...): invalid plot type
很遗憾,这样的写法是行不通的……正确的做法需要用{}
将plot()
函数包起来。
%>%
iris plot(x = .$Sepal.Length, y = .$Sepal.Width, col = .$Species) } {
这样得到的图形结果,与第一种写法相比,除了X轴和Y轴的标题不同,其余部分都是一样的。
当然,使用with()
函数配合占位符.
也是能得到同样的结果的,某种程度上可能更易于理解。
%>%
iris with(., plot(x = .$Sepal.Length, y = .$Sepal.Width, col = .$Species))
总之,占位符.
作为magrittr
包提供的一个功能,在很多场合都非常实用,例如我们希望将diamonds
数据当中重量大于等于1克拉的钻石价格存在一个名为diamonds_price_1
的vector
里,如果用管道符%>%
来进行操作,大致应该是这样:
<- diamonds %>%
diamonds_price_1 filter(carat >= 1) %>%
select(price)
head(diamonds_price_1)
# A tibble: 6 × 1
price
<int>
1 2774
2 2781
3 2788
4 2788
5 2789
6 2789
这样做本身并没有任何问题,但如果我们检查一下diamonds_price_1
的类型会发现,它并不是一个vector
,而是一个只有一列的data.frame
。
class(diamonds_price_1)
[1] "tbl_df" "tbl" "data.frame"
dim(diamonds_price_1)
[1] 19060 1
假如因为某些原因,我们必须将结果存成vector
,此时我们当然可以选择再进行一步操作,将它转为一个vector
并保存:
<- diamonds_price_1$price
diamonds_price_1.vec
class(diamonds_price_1.vec)
[1] "integer"
head(diamonds_price_1.vec)
[1] 2774 2781 2788 2788 2789 2789
但这样的操作打断了管道符操作的连贯性,也失去了使用管道符操作的初衷。
所以我们可以使用占位符.
来保证管道操作的连贯性。
<- diamonds %>%
diamonds_price_1 filter(carat >= 1) %>%
select(price) %>% # 这一步并不是必须的,仅为对比演示需要而保留
$price
.
class(diamonds_price_1)
[1] "integer"
head(diamonds_price_1)
[1] 2774 2781 2788 2788 2789 2789
可以看到结果变成了vector
。这是因为在占位符.
之前输出的结果是一个data.frame
,我们用占位符.
代替了这个data.frame
,再使用$
选取price
这一列,这样得到的结果就是vector
了。
又或者,我们可以直接使用pull()
函数,得到的结果也是一个vector
:
<- diamonds %>%
diamonds_price_1 filter(carat >= 1) %>%
pull(price)
class(diamonds_price_1)
[1] "integer"
head(diamonds_price_1)
[1] 2774 2781 2788 2788 2789 2789
二、|>
与%>%
的区别
本文开头的例子已经向大家展示了,在基本功能上|>
与%>%
并没有任何区别,都是将LHS的输出结果传参给RHS的第一个输入。他们最大的不同可能就在于占位符.
是magrittr
包提供的,因此当使用R原生的管道操作符|>
时,是无法使用占位符.
的。
虽然只有这一点点的区别,但在实际使用习惯了占位符.
的情况下,还是会带来一些不便,例如将之前的作图代码中的%>%
替换为|>
后就会报错。
|>
iris plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
Error in pairs.default(data.matrix(x), ...): object 'Species' not found
或者
|>
iris plot(x = .$Sepal.Length, y = .$Sepal.Width, col = .$Species) } {
Error: function '{' not supported in RHS call of a pipe
又或者
|>
iris with(., plot(x = .$Sepal.Length, y = .$Sepal.Width, col = .$Species))
Error in eval(substitute(expr), data, enclos = parent.frame()): object '.' not found
如何绕开这一点限制呢?这里涉及到一个很重要的知识点,那就是匿名function。
三、匿名function
匿名function,顾名思义就是没有名字的function,与一开始提到的my_func
的例子不同,匿名function不会将function()
函数定义好的逻辑赋值给一个变量(因此匿名)。它随写随用,用完即弃,无法像my_func
那样被复用(在Python中,这种function也被称为lambda
function)。
<- 0:9
x sapply(x, function(x) { x + 1 })
[1] 1 2 3 4 5 6 7 8 9 10
在上面这个例子当中,我们定义了一个匿名function:function(x) { x + 1 }
,它没有名字,其作用是给它一个x,它会给这个x加1。所以当我们给它从0到9这十个数字时,它返回了1到10。
R语言中function还有一种简便写法,例如
function(x) { x + 1 }
可以简化为
\(x) { x + 1 }
也就是用\
替代function
。所以刚才的例子可以简化的写为:
<- 0:9
x sapply(x, \(x) { x + 1 })
[1] 1 2 3 4 5 6 7 8 9 10
注意,这种写法不局限于匿名function,非匿名function也可以这样写,但既然要创建非匿名function,还是尽量避免这种写法,尽量使function的赋值意义更加明确,因此我自己只会在匿名function时使用这种写法。
四、匿名function与管道操作符|>
回到管道操作符上来,如果我们希望能够在使用|>
时,实现占位符.
的效果,要怎么办呢?答案很简单,就是自己用匿名function写一个。
还是刚才的作图代码,我们来定义一个匿名function替代占位符.
的功能好了:
|>
iris function(.) {
plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
}
Error: function 'function' not supported in RHS call of a pipe
WHAT?!居然还是报错了…让我们来仔细看一下匿名function是如何工作的。
之前我们定义了一个非匿名的function叫做my_func
,当调用它时我们需要在my_func
的后面加上括号()
,也就是说my_func
只是这个function的名字,加了括号之后的my_func()
才会实际调用这个function,这与R自带的function,例如mean()
,median()
,summary()
是同理。
仔细观察一下我们定义的匿名function的代码:
function(.) {
plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
}
假如我们将这部分代码赋值给一个变量,例如plot_func
,实际上它就变成了一个非匿名的function:
<- function(.) {
plot_func plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
}
此时如果我们想调用这个plot_func
,就必须在后面加上()
才可以。
换句话说我们写的匿名function的代码其实只是function的名字部分(相当于没有加括号的plot_func
),而调用时则必须在后面加上括号。
那么按照这个思路,刚才报错的代码其实并没有真的调用我们写好的匿名function,我们试试看在后面加上()
对它进行调用,会发生什么:
|>
iris function(.) {
(plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
}# 这里的括号实现function的调用 )()
成功了!需要注意的是,匿名function的前半部分也需要用一组括号括起来,表示它是一个整体。
如果使用简便写法,则上面的代码可以简化如下:
|>
iris plot(Sepal.Width ~ Sepal.Length, col = Species, data = .) })() (\(.) {
五、小结
|>
和%>%
均可以用来实现批量管道操作,提高代码书写和运行的效率。占位符
.
只能配合%>%
使用,不能配合|>
使用。可以使用匿名function来替代占位符
.
实现类似的效果,但需要注意语法结构:data |> (function(.) { ... })()
或者
data |> (\(.) { ... })()