Pythonの内包表記はRにないのか?

RにないPythonの文法・テクニックとして,これまで2つの記事(1つめ2つめ)を書きました。

今回は内包表記について書きます。Pythonユーザーにはお馴染みの,for文を1行で書ける便利なやつです。

結論から言うと,Rに内包表記はありません。処理の目的次第で内包表記よりRの方が簡単に書けたり,複雑に書かざるを得なかったりします。その辺を説明していきます。

Pythonの内包表記とは

簡単に言うと「複数行に渡るfor文を1行に凝縮して,リスト・タプル・辞書・セットを作成する方法」が内包表記です。

リスト内包表記

内包表記でリストを作る方法がこちらです。

[変数を使った式/操作 for 変数 in イテラブルなオブジェクト]

例として「0から5までの数字をstr型として持つリスト」をfor文と内包表記でそれぞれ書くとこうなります。

# for文
>>> num_str = []
>>> for i in range(5):
...    num_str.append(str(i))
>>> print(num_str)
['0', '1', '2', '3', '4']

#内包表記
>>> [str(i) for i in range(5)]
['0', '1', '2', '3', '4']

このように内包表記を使えばfor文なしで簡単にリストを作成できます。

if文を使った内包表記

次の書き方でif文の条件式に合う値だけリストに格納することができます。

[変数を使った式/操作 for 変数 in イテラブルなオブジェクト if 変数を使った条件]

例として「0から49までの整数のうち3で割り切れるもの」は以下のように書けます。

>>> [x for x in range(50) if x % 3== 0]
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48]

ネストした内包表記

2重のfor文も1行の内包表記で表現できます。「colorsとthingsの要素の文字列を総当りしてfstringで結合しリストに格納する」という例が以下です。

>>> colors = ["red", "green"]
>>> things = ["house", "car", "tree"]

# for文
>>> color_thing = []
...for x in colors:
...    for y in things:
...        color_thing.append(f'{x} {y}')
>>> print(color_thing)
['red house', 'red car', 'red tree', 'green house', 'green car', 'green tree']

# 内包表記
>>> [f'{x} {y}' for x in colors for y in things]
['red house', 'red car', 'red tree', 'green house', 'green car', 'green tree']

辞書内包表記

内包表記はリストだけではなく辞書を作るのにも重宝されます。「文字列をキー,その文字数を値とする辞書」を内包表記を作成するなら次のようになります。

>>> words = ['Python', 'R', 'C++', 'Ruby']
>>> {word: len(word) for word in words}
{'Python': 6, 'R': 1, 'C++': 3, 'Ruby': 4}

Rにリスト内包表記はないのか?なければどう表現するか?

私の知る限り,Rに内包表記はありません*1

特定の処理/操作を行う際,Pythonでは内包表記がベストプラクティスだったとしても,Rでは異なる方法を用いる必要があります。処理の目的によってRでのベストプラクティスが異なるので,ここからは思いついた例をいくつか挙げていきます。

1. そもそも内包表記(for文)が必要ない場合が多い

「0から4までの整数を2乗してstr型に変換しリストに格納」という処理を考えます。内包表記で書くとこうなります。

>>> [str(i * i) for i in range(5)]
['0', '1', '4', '9', '16']

Rユーザーは「Pythonではこんな簡単な処理にfor文/内包表記が必要なのか?」と思うことでしょう。Rだったらfor文なしでこう書けます。

> as.character((0:4)^2)
 [1] "0"  "1"  "4"  "9"  "16"

Rではこのように0:4というベクトルに対し累乗の演算子やas.characterという関数を適用できます。Rと同じノリでPythonで書こうと思ってもエラーになります。

str([0,1,2,3,4]**2) # TypeError: unsupported operand

この例のように,Rには「ベクトルを引数として受け取って,要素ごとに処理を行う」という関数/演算子が多くあります。一方でPythonの組み込み関数や演算子はそのような仕様になっていないため,for文や内包表記を用いてコンテナオブジェクト(リストや辞書)の要素ごとに関数/演算子を適用し,処理する必要があります。

別の例として「リストの要素である文字列の文字数をカウントしてリストを作成」という操作を考えます。Pythonでは内包表記が必要になりますが,Rではnchar関数(またはstringr::str_length関数)で簡単に書けます。

>>> words = ['Python', 'R', 'C++', 'Ruby']
>>> [len(word) for word in words]
[6, 1, 3, 4]
> words <- c('Python', 'R', 'C++', 'Ruby')
> nchar(words)
[1] 6 1 3 4

先に挙げたif文を使った内包表記もRではベクトル操作と演算子だけで簡単に表現できます。「0から49までの整数のうち3で割り切れるもの」は以下のように書けます。

>>> [x for x in range(50) if x % 3== 0]
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48]
> num <- 0:49
> num[num %% 3 == 0]
 [1]  0  3  6  9 12 15 18 21 24 27 30 33 36 39 42 45 48

以上のように,Pythonにおいて内包表記が伴う操作の多くはRでfor文の手を借りずに表現することができます。

2. sapplyやpurrr:mapで対応

先に見た「0から5までの数字をstr型に変換してリストに作成」のように「繰り返し変数に関数や演算子を適用し,入力(変数)と出力(操作後の要素)が1対1対応したリストを作成する」という単純な内包表記であれば,Rのbase::sapply関数やpurrr:map関数で表現できます。どちらの関数も「list/ベクトル(第1引数)の各要素に対して関数(第2引数)を適用して,その関数の返り値を出力する」という機能です。

map関数の派生系が特に便利で,map_dblは出力をdouble型に,map_chrは出力を文字列として出力できたりします。

> sapply(0:5, function(x) x*2)
[1]  0  2  4  6  8 10

> map_dbl(0:5, function(x) x*2)
[1]  0  2  4  6  8 10

> map_dbl(0:5, ~ .x*2) # map系はfunctionを省いてチルダと.xで表現可能
[1]  0  2  4  6  8 10

> map_chr(0:5, ~ .x*2) # map系はfunctionを省いてチルダと.xで表現可能
[1] "0.000000"  "2.000000"  "4.000000"  "6.000000"  "8.000000"  "10.000000"

Pythonの内包表記と比較するため「円周率を小数点i桁で丸めて,文字列にする。iを1から6まで処理してリストに格納」という操作を考えます。PythonとRでそれぞれ以下のように書けます*2

>>> from math import pi
>>> [str(round(pi, i)) for i in range(1, 6)]
['3.1', '3.14', '3.142', '3.1416', '3.14159']
> sapply(1:5, function(i) as.character(round(pi, i)))
[1] "3.1"     "3.14"    "3.142"   "3.1416"  "3.14159"

> map_chr(1:5, ~as.character(round(pi, .x)))
[1] "3.1"     "3.14"    "3.142"   "3.1416"  "3.14159"

3. 妥協してfor文で書く または tidyverse系でゴリ押し

内包表記の処理によっては,Rで上手いこと表現できないものもあります。例えば,上で挙げたネストした内包表記の例はRのsapplyやmapで表現しきれません*3「colorsとthingsの要素の文字列をfstringで総当りして結合しリストに格納する」という処理でしたが,妥協してfor文で書くとアホみたいに冗長になります。

> colors = c("red", "green")
> things = c("house", "car", "tree")
> 
> color_thing <- c()
> for(color in colors){
+   for(thing in things){
+     color_thing <- c(color_thing, paste(color, thing))
+   }
+ }

> print(color_thing)
[1] "red house"   "red car"     "red tree"    "green house" "green car"   "green tree" 

総当りなので,tidyr::expand_gridを使ってゴリ押しすることもできます。

> colors = c("red", "green")
> things = c("house", "car", "tree")

> library(tidyr)
> expand_grid(colors, things) %>%
+   mutate(color_thing = paste(colors, things)) %>%
+   pull(color_thing)
[1] "red house"   "red car"     "red tree"    "green house" "green car"   "green tree" 

この例くらい複雑な処理になると内包表記の便利さを感じることができますね。

終わりに

今回はPythonの内包表記をRでどう表現するかについて書いてみました。 Pythonでは内包表記が超便利なので,RユーザーでPythonをマスターしたい人は早く身につけましょう。

追記 (2020年6月11日)

@dd_am11さんから「0から4までの整数を2乗してstr型に変換しリストに格納」の処理に関して「numpyだったらRっぽく書けるよね」というコメントを頂きましたので,追記しておきます。

>>> import numpy as np
>>> list((np.arange(5)**2).astype(str))
['0', '1', '4', '9', '16']

またR界隈で有名な@Atsushi776さんから,最後のcolorsとthingsの処理に関してRでシンプルに書く方法を2つ教えて頂きました。 1つめはlapplyしてunlist,2つのexpand.gridしてdo.callです。

> unlist(lapply(colors, paste, things))
[1] "red house"   "red car"     "red tree"    "green house" "green car"   "green tree" 

> do.call(paste, rev(expand.grid(things, colors)))
[1] "red house"   "red car"     "red tree"    "green house" "green car"   "green tree" 

何ともオシャレですね。 ベクトルの処理はRの最も得意とする所なので,書き方が色々あって面白いですね。

*1:厳密にはcomnprehrというPythonの内包表記を模したRのライブラリはありますが,おそらく誰も使っていません。

*2:実際こんな例だとsapplyやmapは使わず,as.character(round(pi, 0:5) ) と書いてしまいます。

*3:簡単に書く方法を教えて頂いたので追記を御覧ください!