3. 표현식의 평가
3. 표현식의 평가 (Evaluation of Expressions)
R에서 무언가를 실행하는 순간, 내부적으로는 "파싱 → 평가"라는 두 단계가 벌어져요. 사용자가 프롬프트에 명령을 입력하면(또는 파일에서 표현식을 읽으면) 그 명령은 먼저 **파서(parser)**에 의해 R의 내부 표현 형태로 변환되고, 그 다음 **평가기(evaluator)**가 그 파싱된 R 표현식을 실행해 값을 돌려줍니다. 이번 장에서는 그 평가 메커니즘의 기본 원리를 함께 살펴볼게요.
출처: R 공식 매뉴얼
본문
모든 표현식은 값을 가집니다. 이것이 바로 R이라는 언어의 핵심이에요. 이 장은 평가기의 기본 메커니즘을 다루되, 특정 함수나 함수군을 깊이 설명하지는 않아요. 그런 내용들은 뒤의 별도 챕터에서 다루거나, 도움말 페이지(help page)만으로 충분히 설명되는 경우가 많거든요. 사용자는 직접 표현식을 구성해서 그 위에서 평가기를 호출할 수도 있습니다.
이 장에서 다루는 내용의 큰 흐름을 먼저 보면:
- 단순 평가 (Simple evaluation)
- 제어 구조 (Control structures)
- 기본 산술 연산 (Elementary arithmetic operations)
- 인덱싱 (Indexing)
- 변수의 유효 범위 (Scope of variables)
3.1 단순 평가 (Simple evaluation)
단순 평가이지만 여기서 기본기가 완성됩니다. 상수(constant), 기호(symbol)의 조회, 함수 호출(function call), 연산자(operator)라는 네 가지 요소를 차례로 볼게요.
3.1.1 상수 (Constants)
프롬프트에 숫자를 직접 입력하면 그 숫자는 상수로 취급되어 평가됩니다.
> 1
[1] 1
표현식 1을 평가해서 돌아온 값이 numeric(실수형)이라는 점은 다소 의외일 수 있어요. 대부분의 경우 정수(integer)와 실수(numeric) 사이의 차이는 크게 신경 쓸 필요가 없습니다. R이 알아서 적절하게 처리해 주거든요. 그런데 가끔은 상수를 명시적으로 정수 값으로 만들고 싶을 때가 있어요. 그럴 땐 as.integer 함수를 호출하거나 여러 다른 방법을 쓸 수 있지만, 가장 간단한 방법은 상수 뒤에 접미사 문자 **L**을 붙이는 것입니다.
예를 들어 정수 값 1을 만들고 싶다면 이렇게 써요.
> 1L
[1] 1
L 접미사는 명시적으로 정수로 만들고 싶은 어떤 숫자에도 붙일 수 있어요. 그래서 0x10L은 16진수 표현에서 정수 값 16을 만들어 줍니다. 상수 1e3L은 실수 대신 정수로 1000을 주며, 1000L과 동등합니다. (여기서 L은 1e3이라는 항(term) 전체를 한정하고, 3만을 한정하는 것이 아니라는 점을 주목하세요.) 만약 정수가 아닌 값에 L을 붙이면 — 예컨대 1e-3L처럼 — 경고가 나오면서 실수 값이 만들어집니다. 불필요한 소수점이 들어 있는 숫자, 예를 들어 1.L 같은 경우에도 경고가 발생해요. 그리고 복소수에 L을 쓰면, 예를 들어 12iL은 문법 오류(syntax error)가 납니다.
상수만으로는 할 수 있는 일이 제법 제한적이에요. 더 흥미로운 일을 하려면 이제 **기호(symbol)**가 필요합니다.
3.1.2 기호 조회 (Symbol lookup)
새로운 변수를 만들 때 그 변수는 참조할 수 있도록 이름을 가져야 하고, 보통은 값을 가집니다. 이 이름 자체가 바로 **기호(symbol)**예요. 기호를 평가하면 그 값이 반환됩니다. 기호에 연결된 값을 어떻게 결정하는지는 뒤에서 자세히 설명할게요.
아주 작은 예를 보면, 아래에서 y가 기호이고 그 값은 4입니다. 기호도 사실 R 객체(object)에요. 하지만 "언어 위에서의 프로그래밍(Computing on the language)" 같은 특별한 작업을 할 때가 아니면 기호를 직접 다룰 일은 거의 없습니다.
> y <- 4
> y
[1] 4
3.1.3 함수 호출 (Function calls)
R에서 수행되는 대부분의 계산은 함수의 평가를 통해 이루어집니다. 이를 **함수 호출(function invocation)**이라고도 불러요. 함수는 이름과 함께, 쉼표로 구분된 인자 목록을 붙여서 호출합니다.
> mean(1:10)
[1] 5.5
이 예에서는 mean 함수가 인자 하나 — 1부터 10까지의 정수 벡터 — 를 받아 호출되었습니다.
R에는 목적이 각기 다른 엄청나게 많은 함수가 들어 있어요. 대부분은 결과로 R 객체를 만들어 내는 데 쓰이지만, 결과보다는 **부수 효과(side effect)**를 위해 쓰이는 함수도 있습니다. 예를 들어 출력(print)이나 플로팅(plotting) 함수가 그런 경우죠.
함수 호출은 태그(tag, 이름)가 붙은 인자를 가질 수 있어요. plot(x, y, pch = 3)처럼요. 태그가 없는 인자는 **위치 인자(positional)**라고 하는데, 함수가 그 의미를 인자들이 나열된 순서에서 구분해야 하기 때문입니다. 가령 여기서 x가 가로축 변수, y가 세로축 변수라는 걸 순서로 구분하는 식이에요. 선택적 인자가 많은 함수라면 태그/이름을 쓰는 게 확실히 편리합니다.
특별한 종류의 함수 호출은 할당 연산자의 왼쪽에 나타날 수 있어요. 다음과 같이요.
> class(x) <- "foo"
이 구조가 실제로 하는 일은, 원래 객체와 오른쪽 값을 인자로 하여 class<- 라는 함수를 호출하는 것입니다. 이 함수가 객체를 수정하고 그 결과를 반환하면, 그 값이 다시 원래 변수에 저장돼요. (적어도 개념적으로는 그렇습니다. 실제로는 불필요한 데이터 복제를 피하기 위한 추가적인 노력이 들어갑니다.)
3.1.4 연산자 (Operators)
R은 C 프로그래밍 언어와 비슷한 연산자를 사용한 산술 표현식을 허용합니다. 예를 들어:
> 1 + 2
[1] 3
표현식은 괄호로 묶어 그룹화할 수 있고, 함수 호출과 섞을 수 있으며, 변수에 간단히 할당할 수도 있습니다.
> y <- 2 * (a + log(x))
R에는 여러 연산자가 있습니다. 아래 표에 정리했어요.
| 연산자 | 의미 |
|---|---|
- |
빼기, 단항 또는 이항으로 쓸 수 있음 |
+ |
더하기, 단항 또는 이항으로 쓸 수 있음 |
! |
단항 논리 부정 (not) |
~ |
물결표, 모델 수식(formula)에 사용, 단항 또는 이항으로 쓸 수 있음 |
? |
도움말 |
: |
시퀀스, 이항 (모델 수식에서는 상호작용) |
* |
곱하기, 이항 |
/ |
나누기, 이항 |
^ |
거듭제곱, 이항 |
% x % |
특수 이항 연산자, x는 임의의 유효한 이름으로 대체 가능 |
%% |
나머지(modulus), 이항 |
%/% |
정수 나눗셈, 이항 |
%*% |
행렬 곱, 이항 |
%o% |
외적(outer product), 이항 |
%x% |
크로네커 곱(Kronecker product), 이항 |
%in% |
포함 여부 매칭 연산자, 이항 (모델 수식에서는 중첩) |
| `% | |
< |
작다, 이항 |
> |
크다, 이항 |
== |
같다, 이항 |
>= |
크거나 같다, 이항 |
<= |
작거나 같다, 이항 |
& |
논리곱(and), 이항, 벡터화됨 |
&& |
논리곱(and), 이항, 벡터화되지 않음 |
| |
논리합(or), 이항, 벡터화됨 |
|| |
논리합(or), 이항, 벡터화되지 않음 |
<- |
왼쪽 할당, 이항 |
-> |
오른쪽 할당, 이항 |
$ |
리스트 부분 선택, 이항 |
문법만 다를 뿐, 연산자를 적용하는 것과 함수를 호출하는 것 사이에는 차이가 없습니다. 실제로 x + y는 +(x, y)와 동등하게 쓸 수 있어요. 여기서 +`는 비표준 함수 이름이므로 따옴표로 감싸 줘야 한다는 점을 주의하세요.
R은 한 번에 벡터 전체를 다룹니다. 대부분의 기본 연산자와 log 같은 기초 수학 함수는 벡터화되어 있어요(위 표에 그 표시가 있습니다). 이 말은, 예를 들어 길이가 같은 두 벡터를 더하면 벡터 인덱스를 따라 내부적으로 반복(loop)하면서 요소별 합을 담은 벡터가 만들어진다는 뜻입니다. 이 규칙은 -, *, / 같은 다른 연산자나 더 높은 차원의 구조에도 동일하게 적용됩니다. 특히 주의할 점은 행렬 두 개를 곱한다고 해서 보통의 행렬 곱이 나오지는 않는다는 거예요(그 목적을 위한 연산자가 따로 %*%입니다). 벡터화 연산의 세부 사항은 기본 산술 연산(Elementary arithmetic operations) 절에서 더 다룰게요.
원자 벡터(atomic vector)의 개별 요소에 접근할 때는 보통 x[i] 구조를 사용합니다.
> x <- rnorm(5)
> x
[1] -0.12526937 -0.27961154 -1.03718717 -0.08156527 1.37167090
> x[2]
[1] -0.2796115
리스트의 구성 요소는 더 흔히 x$a 또는 x[[i]]로 접근해요.
> x <- options()
> x$prompt
[1] "> "
인덱싱 구조는 할당의 오른쪽에도 나타날 수 있어요. 다른 연산자와 마찬가지로 인덱싱도 사실은 함수에 의해 이루어지므로, x[2] 대신 `[`(x, 2)를 쓸 수도 있습니다. R의 인덱싱 연산에는 고급 기능이 아주 많은데, 이는 인덱싱(Indexing) 절에서 더 자세히 설명할게요.
3.2 제어 구조 (Control structures)
R에서의 계산은 **문장(statement)**들을 순차적으로 평가하는 것으로 구성됩니다. x<-1:10이나 mean(y) 같은 문장은 세미콜론이나 새 줄(new line)로 구분할 수 있어요.
평가기가 문법적으로 완전한(syntactically complete) 문장을 만나면 그 문장을 평가하고 값을 반환합니다. 문장을 평가한 결과를 "문장의 값"이라고 부를 수 있어요. 그 값은 언제나 어떤 기호에 할당될 수 있습니다.
문장을 구분하는 데는 세미콜론과 새 줄이 모두 사용 가능합니다. 세미콜론은 항상 문장의 끝을 의미하지만, 새 줄은 문장의 끝을 의미할 수도 있고 아닐 수도 있어요. 현재 문장이 문법적으로 완전하지 않으면 새 줄은 그냥 무시됩니다. 대화형(interactive) 세션이라면 프롬프트가 >에서 +로 바뀝니다.
> x <- 0; x + 5
[1] 5
> y <- 1:10
> 1; 2
[1] 1
[1] 2
문장들은 중괄호 {와 }로 묶어 하나로 그룹화할 수 있어요. 이렇게 묶은 문장의 집합을 **블록(block)**이라고 부릅니다. 단일 문장은 문법적으로 완전해지면서 새 줄이 입력되면 평가되고, 블록은 닫는 중괄호 다음에 새 줄이 입력될 때까지는 평가되지 않습니다. 이 절의 나머지에서 "문장"은 단일 문장 또는 블록을 가리킵니다.
> { x <- 0
+ x + 5
+ }
[1] 5
이 절에서 다루는 제어 구조는 다음과 같아요.
if- 반복(Looping)
repeatwhileforswitch
3.2.1 if
if / else 문은 조건에 따라 두 문장 중 하나를 평가합니다. 조건이 먼저 평가되어 그 값이 TRUE이면 첫 번째 문장이, 그렇지 않으면 두 번째 문장이 평가돼요. if / else 문은 그 값으로, 선택된 문장의 값을 반환합니다. 형식적인 문법은 다음과 같습니다.
if ( statement1 )
statement2
else
statement3
먼저 statement1이 평가되어 value1을 만듭니다. value1이 논리(logical) 벡터이고 **첫 번째 요소가 TRUE**이면 statement2가 평가되고, 첫 번째 요소가 FALSE이면 statement3이 평가됩니다. value1이 숫자(numeric) 벡터면 첫 번째 요소가 0일 때 statement3, 그렇지 않으면 statement2가 평가됩니다. 오직 value1의 첫 번째 요소만 사용되고 나머지 요소는 모두 무시돼요. value1이 논리 벡터나 숫자 벡터 외의 다른 타입이면 오류(error)가 발생합니다.
if / else 문은 로그를 취하는데 음수가 들어가는 문제처럼 숫자 계산에서 겪는 문제를 피하는 데 쓰이기도 합니다. if / else 문도 다른 문장과 같으므로 그 값을 변수에 할당할 수 있어요. 아래 두 예는 동등합니다.
> if( any(x <= 0) ) y <- log(1+x) else y <- log(x)
> y <- if( any(x <= 0) ) log(1+x) else log(x)
else 절은 선택 사항입니다. if(any(x <= 0)) x <- x[x <= 0] 같은 문장도 유효해요. if 문이 블록 안에 있을 때, else가 있다면 반드시 statement2의 끝과 같은 줄에 있어야 합니다. 그렇지 않으면 statement2 끝의 새 줄이 if를 완성해서, 문법적으로 완전해진 문장이 평가되어 버리거든요. 간단한 해결책은 중괄호로 감싼 복합 문장을 쓰고, 문장의 끝을 알리는 닫는 중괄호와 같은 줄에 else를 두는 것입니다.
if / else 문은 중첩될 수 있어요.
if ( statement1 ) {
statement2
} else if ( statement3 ) {
statement4
} else if ( statement5 ) {
statement6
} else
statement8
짝수 번호 문장(statement2, statement4, ...) 중 하나가 평가되고 그 결과 값이 반환됩니다. 선택적인 else 절을 생략했는데 모든 홀수 번호 문장이 FALSE로 평가되면, 어떤 문장도 평가되지 않고 NULL이 반환됩니다.
홀수 번호 문장들은 TRUE로 평가되는 문장이 나올 때까지 순서대로 평가되고, 그에 연관된 짝수 번호 문장이 평가됩니다. 이 예에서 statement6은 statement1이 FALSE이고 statement3이 FALSE이며 statement5가 TRUE인 경우에만 평가됩니다. else if 절의 개수에는 제한이 없습니다.
3.2.2 반복 (Looping)
R에는 명시적으로 반복을 수행하는 문장이 세 가지 있습니다. 바로 for, while, repeat예요. 그리고 내장 구성 요소인 next와 break가 평가를 추가로 제어해 줍니다.
R에는 암묵적 반복을 위한 함수들(tapply, apply, lapply 같은)도 있습니다. 게다가 많은 연산, 특히 산술 연산은 벡터화되어 있기 때문에 꼭 반복문을 쓸 필요가 없을 수도 있어요.
반복을 명시적으로 제어하는 데 쓰이는 두 문장이 있습니다. 바로 break와 next입니다. break 문장은 현재 실행 중인 가장 안쪽 반복문에서 빠져나오게 합니다. next 문장은 즉시 제어를 반복문의 시작으로 되돌려서, 다음 반복(있다면)을 실행하게 합니다. 현재 반복에서 next 아래에 있는 문장은 아무것도 평가되지 않아요.
반복문이 반환하는 값은 **항상 눈에 보이지 않게(invisibly) 반환되는 NULL**입니다.
3.2.3 repeat
repeat 문장은 break가 명시적으로 요청될 때까지 본문(body)을 반복해서 평가합니다. 즉 사용할 때 조심해야 한다는 뜻이에요. 무한 루프에 빠질 위험이 있으니까요. repeat 반복의 문법은 다음과 같습니다.
repeat statement
repeat을 쓸 때 statement는 반드시 블록 문장이어야 합니다. 계산을 수행하는 동시에 반복을 빠져나갈지 여부를 검사해야 하는데, 보통 이 두 가지가 두 문장이 필요하거든요.
3.2.4 while
while 문장은 repeat 문장과 매우 비슷합니다. while 반복의 문법은 이렇게 생겼어요.
while ( statement1 ) statement2
statement1이 평가되어 그 값이 TRUE이면 statement2가 평가됩니다. 이 과정은 statement1이 FALSE로 평가될 때까지 계속됩니다.
3.2.5 for
for 반복의 문법은 다음과 같습니다.
for ( name in vector )
statement1
여기서 vector는 벡터일 수도, 리스트일 수도 있어요. vector의 각 요소마다 변수 name이 그 요소의 값으로 설정되고 statement1이 평가됩니다. 부수 효과로, 반복이 끝난 뒤에도 변수 name은 계속 존재하며, 반복이 마지막으로 평가했던 vector의 마지막 요소 값을 갖게 됩니다.
3.2.6 switch
엄밀히 말하면 switch는 그냥 또 하나의 함수입니다. 다만 그 의미(semantics)가 다른 프로그래밍 언어들의 제어 구조와 가깝죠.
문법은 다음과 같아요.
switch (statement, list)
여기서 list의 요소들은 이름을 가질 수 있습니다. 먼저 statement가 평가되어 결과 value를 얻습니다. value가 1부터 list의 길이 사이의 숫자라면 list의 해당 요소가 평가되고 그 결과가 반환됩니다. value가 너무 크거나 너무 작으면 NULL이 반환됩니다.
> x <- 3
> switch(x, 2+2, mean(1:10), rnorm(5))
[1] 2.2903605 2.3271663 -0.7060073 1.3622045 -0.2892720
> switch(2, 2+2, mean(1:10), rnorm(5))
[1] 5.5
> switch(6, 2+2, mean(1:10), rnorm(5))
NULL
value가 문자(character) 벡터라면, 이름이 value와 정확히 일치하는 ...의 요소가 평가됩니다. 일치하는 것이 없으면, 이름이 없는 단일 인자 하나가 기본값(default)으로 사용됩니다. 기본값이 지정되지 않았다면 NULL이 반환됩니다.
> y <- "fruit"
> switch(y, fruit = "banana", vegetable = "broccoli", "Neither")
[1] "banana"
> y <- "meat"
> switch(y, fruit = "banana", vegetable = "broccoli", "Neither")
[1] "Neither"
switch의 흔한 용도 중 하나는 함수 인자 중 하나의 문자 값에 따라 분기하는 것입니다.
> centre <- function(x, type) {
+ switch(type,
+ mean = mean(x),
+ median = median(x),
+ trimmed = mean(x, trim = .1))
+ }
> x <- rcauchy(10)
> centre(x, "mean")
[1] 0.8760325
> centre(x, "median")
[1] 0.5360891
> centre(x, "trimmed")
[1] 0.6086504
switch는 평가된 문장의 값을 반환하며, 평가된 문장이 없다면 NULL을 반환합니다.
이미 존재하는 대안 목록에서 하나를 골라 평가하고 싶다면 switch가 항상 최선은 아닐 수 있어요. 그럴 때는 eval과 부분선택 연산자 [[를 조합해 eval(x[[condition]])처럼 직접 쓰는 편이 나을 때가 많습니다.
3.3 기본 산술 연산 (Elementary arithmetic operations)
이 절에서는 두 벡터나 두 행렬의 덧셈·곱셈 같은 기본 연산에 적용되는 세부 규칙을 다룹니다.
- 재활용 규칙 (Recycling rules)
- 이름의 전파 (Propagation of names)
- 차원 속성 (Dimensional attributes)
- NA 처리 (NA handling)
3.3.1 재활용 규칙 (Recycling rules)
요소 수가 서로 다른 두 구조를 더하려고 하면, **더 짧은 쪽이 더 긴 쪽의 길이에 맞춰 재활용(recycle)**됩니다. 예를 들어 c(1, 2, 3)을 여섯 요소짜리 벡터와 더하면 실제로는 c(1, 2, 3, 1, 2, 3)을 더하게 되는 거예요. 더 긴 벡터의 길이가 더 짧은 벡터 길이의 배수가 아니라면 경고가 발생합니다.
R 1.4.0부터는, 길이가 0인 벡터가 포함된 산술 연산은 길이가 0인 결과를 냅니다.
3.3.2 이름의 전파 (Propagation of names)
원문 주: 이 소절은 원문에서 미완성(stub) 상태이며, "이름의 전파(첫 번째 것이 우선, 재활용되면 더 짧은 쪽은 이름을 잃는다)" 정도의 메모만 남아 있습니다. 정확한 규칙은 원문이 보완되길 기대하며, 여기서는 문장이 그대로입니다.
원문: "propagation of names (first one wins, I think - also if it has no names?? —- first one with names wins, recycling causes shortest to lose names)"
3.3.3 차원 속성 (Dimensional attributes)
원문 주: 이 소절 역시 원문에서 미완성(stub) 상태입니다. (행렬+행렬은 차원이 일치해야 하고, 벡터+행렬은 먼저 재활용한 뒤 차원이 맞는지 검사하며, 맞지 않으면 오류) 정도만 명시되어 있습니다.
원문: "(matrix+matrix, dimensions must match. vector+matrix: first recycle, then check if dims fit, error if not)"
3.3.4 NA 처리 (NA handling)
통계학적 의미에서의 결측값(missing value), 즉 값이 알려지지 않은 변수는 값 NA를 가집니다. 이것은 함수 인자가 제공되지 않았을 때의 "누락(missing)" 속성과 혼동해서는 안 됩니다(Arguments 관련 도움말 참고).
원자 벡터의 요소들은 같은 타입이어야 하므로 여러 타입의 NA 값이 존재합니다. 사용자에게 특히 중요한 경우가 하나 있어요. NA의 기본 타입은 **logical(논리형)**입니다. 다른 타입으로 강제 변환되지 않는 한 그렇죠. 그래서 결측값이 나타나면 숫자 인덱싱 대신 논리 인덱싱을 유발할 수 있습니다(자세한 내용은 Indexing 절 참고).
NA가 포함된 숫자·논리 계산은 일반적으로 NA를 반환합니다. 다만 연산의 결과가 NA가 취할 수 있는 모든 값에 대해 동일한 경우에는 그 값을 반환할 수도 있어요. 특히 FALSE & NA는 FALSE, TRUE | NA는 TRUE입니다. NA는 다른 어떤 값과도, 심지어 자기 자신과도 같지 않아요. NA인지 검사하는 데는 is.na를 사용합니다. 하지만 match에서는 NA 값이 다른 NA 값과 매칭됩니다.
결과가 정의되지 않는 숫자 계산, 예를 들어 0/0 같은 것은 값 NaN을 만들어 냅니다. NaN은 double 타입과 complex(복소수) 타입의 실수부·허수부에만 존재해요. NaN을 특별히 검사하는 함수로 is.nan이 제공되며, is.na도 NaN에 대해 TRUE를 반환합니다. NaN을 logical이나 integer로 강제 변환하면 해당 타입의 NA가 되지만, character로 강제 변환하면 문자열 "NaN"이 됩니다. NaN 값은 **비교 불가(incomparable)**하므로 NaN을 포함한 등호나 정렬(collation) 검사는 NA가 됩니다. match에서는 NaN 값이 어떤 NaN 값(그리고 다른 값 — NA조차 — 과는 매칭되지 않음)과 매칭되는 것으로 취급됩니다.
character 타입의 NA는 문자열 "NA"와 구별됩니다. 명시적인 문자열 NA를 지정해야 하는 프로그래머는 "NA" 대신 NA_character_를 쓰거나, is.na<-를 사용해 요소를 NA로 설정해야 합니다.
상수 NA_integer_, NA_real_, NA_complex_, NA_character_는 (파서에서) 적절한 타입의 NA 값을 생성하며, NA의 타입을 달리 식별할 수 없을 때(그리고 제어 옵션이 그렇게 요구할 때) 역파싱(deparsing)에 사용됩니다.
raw 벡터에는 NA 값이 없습니다.
3.4 인덱싱 (Indexing)
R에는 인덱싱 연산을 통해 개별 요소나 부분 집합에 접근하게 해 주는 여러 구조가 있습니다. 기본 벡터 타입의 경우 x[i]로 i번째 요소에 접근할 수 있고, 리스트·행렬·다차원 배열의 인덱싱도 있습니다. 단일 정수로 인덱싱하는 것 외에도 여러 형태의 인덱싱이 있어요. 인덱싱은 객체의 일부를 추출할 때도, 객체의 일부를 교체(또는 추가)할 때도 사용할 수 있습니다.
R에는 기본 인덱싱 연산자가 세 가지 있는데, 다음 예들로 문법을 보여드릴게요.
x[i]
x[i, j]
x[[i]]
x[[i, j]]
x$a
x$"a"
벡터와 행렬에서는 [[ 형태가 드물게 쓰이지만, [ 형태와는 약간의 의미 차이가 있습니다(예를 들어 names나 dimnames 속성을 버리고, 문자 인덱스에 부분 매칭을 사용한다는 점). 다차원 구조를 단일 인덱스로 인덱싱하면, x[[i]]나 x[i]는 x의 i번째 순차 요소를 반환합니다.
리스트에서는 보통 [[로 단일 요소 하나를 선택하는 반면, [는 선택된 요소들의 리스트를 반환합니다.
[[ 형태는 정수나 문자 인덱스로 단 하나의 요소만 선택할 수 있고, [는 벡터로 인덱싱하는 것을 허용합니다. 다만 리스트나 다른 재귀(recursive) 객체의 경우 인덱스가 벡터일 수 있고, 벡터의 각 요소가 리스트, 선택된 구성 요소, 그 구성 요소의 선택된 구성 요소…… 식으로 차례로 적용됩니다. 결과는 여전히 단일 요소예요.
$ 형태는 리스트나 pairlist 같은 재귀 객체에 적용됩니다. $는 인덱스로 리터럴 문자열이나 기호만 허용합니다. 즉 인덱스가 계산 가능하지 않아요. 인덱스를 찾기 위해 표현식을 평가해야 하는 경우에는 x[[expr]]을 사용하세요. $를 비재귀 객체에 적용하는 것은 오류입니다.
- 벡터로 인덱싱 (Indexing by vectors)
- 행렬과 배열 인덱싱 (Indexing matrices and arrays)
- 다른 구조의 인덱싱 (Indexing other structures)
- 부분 집합 할당 (Subset assignment)
3.4.1 벡터로 인덱싱 (Indexing by vectors)
R은 벡터를 인덱스로 사용하는 강력한 구성을 허용합니다. 먼저 단순 벡터의 인덱싱을 살펴볼게요. 간단히 표현식이 x[i]라고 가정하면, i의 타입에 따라 다음과 같은 가능성이 존재합니다.
- 정수(Integer).
i의 모든 요소는 같은 부호여야 합니다. 양수라면 해당 인덱스 번호를 가진x의 요소들이 선택됩니다.i에 음수 요소가 있으면, 표시된 요소들을 제외한 모든 요소가 선택됩니다.i가 양수인데length(x)를 초과하면 해당 선택 결과는NA입니다. 인덱스가 범위를 벗어난 음수 값은 R 버전 2.6.0부터 S와의 호환을 위해 조용히 무시됩니다. 존재하지 않는 요소를 버리는 것은 빈 연산("no-op")이기 때문이죠. 특별한 경우로 0 인덱스는 아무 효과가 없습니다.x[0]은 빈 벡터이고, 양수·음수 인덱스 사이에 0을 섞는 것도 0이 생략된 것과 같은 효과를 냅니다. - 다른 숫자(Other numeric). 정수가 아닌 값은 사용 전에 (0 방향으로 잘라내어) 정수로 변환됩니다.
- 논리(Logical). 인덱싱
i는 일반적으로x와 같은 길이여야 합니다. 더 짧다면 기본 산술 연산에서 다룬 것처럼 그 요소들이 재활용됩니다. 더 길다면x가 개념적으로NA로 확장됩니다. 선택되는x의 값들은i가TRUE인 위치의 값들입니다. - 문자(Character).
i의 문자열들이x의 names 속성과 매칭되고, 그 결과 정수들이 사용됩니다.[[와$에서는 정확한 매칭이 실패하면 **부분 매칭(partial matching)**이 사용됩니다. 그래서x에 이름이 정확히"aa"인 구성 요소가 없고"aabb"가"aa"를 접두사로 가진 유일한 이름이라면,x$aa는x$aabb와 매칭됩니다.[[의 경우 부분 매칭은exact인자로 제어할 수 있는데, 기본값은NA로 "부분 매칭은 허용하되 발생하면 경고를 내라"는 의미입니다.exact를TRUE로 설정하면 부분 매칭이 일어나지 않고,FALSE로 하면 허용되며 경고도 내지 않습니다. 참고로[는 항상 정확한 매칭을 요구합니다. 빈 문자열""는 특별 취급됩니다.""는 '이름 없음'을 나타내며 어떤 요소와도 매칭되지 않습니다(이름이 없는 요소조차 매칭하지 않아요). 참고로 부분 매칭은 추출할 때만 사용되고 교체할 때는 사용되지 않습니다. - 요인(Factor). 결과는
x[as.integer(i)]와 동일합니다. 요인 레벨(factor levels)은 절대 사용되지 않아요. 원한다면x[as.character(i)]같은 구성을 쓰면 됩니다. - 빈(Empty). 표현식
x[]는x를 반환하되, 결과에서 "무관한" 속성들을 버립니다. names와 다차원 배열의 dim·dimnames 속성만 유지됩니다. - NULL.
integer(0)처럼 취급됩니다.
결측값(NA)으로 인덱싱하면 NA 결과가 나옵니다. 이 규칙은 논리 인덱싱에도 적용됩니다. 즉 i에서 NA 선택자를 가진 x의 요소들은 결과에 포함되지만, 그 값은 NA가 됩니다. 단 NA에는 여러 모드(mode)가 있다는 점을 주의하세요. 리터럴 상수는 mode가 "logical"이지만, 다른 타입으로 자동 강제 변환되는 경우가 잦습니다. 그 효과 중 하나가 x[NA]는 x와 길이가 같은데 x[c(1, NA)]는 길이가 2라는 것입니다. 전자에는 논리 인덱스 규칙이, 후자에는 정수 인덱스 규칙이 적용되기 때문이에요.
[로 인덱싱하면 어떤 names 속성에도 그에 맞는 부분 선택(subsetting)이 수행됩니다.
3.4.2 행렬과 배열 인덱싱 (Indexing matrices and arrays)
다차원 구조의 부분 선택은 일반적으로 각 인덱스 변수에 대해 1차원 인덱싱과 같은 규칙을 따르되, names 자리에 dimnames의 해당 구성 요소가 들어갑니다. 다만 몇 가지 특별한 규칙이 적용돼요.
보통 구조는 그 차원에 해당하는 개수의 인덱스로 접근합니다. 하지만 단일 인덱스를 사용하는 것도 가능한데, 그 경우 dim과 dimnames 속성은 무시되고 결과는 실질적으로 c(m)[i]와 같습니다. m[1]은 m[1, ]나 m[, 1]과 보통 아주 다르다는 점을 주의하세요.
정수 행렬을 인덱스로 사용하는 것도 가능합니다. 이 경우 행렬의 열 수는 구조의 차원 수와 일치해야 하고, 결과는 행렬의 행 수를 길이로 하는 벡터가 됩니다. 다음 예는 m[1, 1]과 m[2, 2] 요소를 한 번의 연산으로 추출하는 방법을 보여줍니다.
> m <- matrix(1:4, 2)
> m
[,1] [,2]
[1,] 1 3
[2,] 2 4
> i <- matrix(c(1, 1, 2, 2), 2, byrow = TRUE)
> i
[,1] [,2]
[1,] 1 1
[2,] 2 2
> m[i]
[1] 1 4
인덱싱 행렬은 음수 인덱스를 포함할 수 없습니다. NA와 0 값은 허용됩니다. 인덱스 행렬에서 0을 포함하는 행은 무시되는 반면, NA를 포함하는 행은 결과에 NA를 만들어 냅니다.
단일 인덱스 사용이든 행렬 인덱싱이든, 구조가 1차원이었다면 사용되는 names 속성이 있으면 그 속성이 사용됩니다.
인덱싱 연산으로 결과의 어떤 차원이 길이 1이 되면 — 예를 들어 3차원 행렬에서 (가령) m[2, , ]처럼 단일 슬라이스를 선택할 때 — 그 차원은 일반적으로 결과에서 **제거(drop)**됩니다. 1차원 구조가 되면 벡터를 얻습니다. 이것은 때때로 바람직하지 않은데, 인덱싱 연산에 drop = FALSE를 추가하면 끌 수 있어요. 이것은 [ 함수에 대한 추가 인자이지 인덱스 개수에 더해지는 것이 아님을 주의하세요. 그래서 행렬의 첫 행을 1×n 행렬로 선택하는 올바른 방법은 m[1, , drop = FALSE]입니다. 인덱스가 대부분 길이 1이지만 가끔 그런 일반 서브루틴에서 drop 기능을 끄는 것을 잊는 것은 흔한 실패 원인입니다. 이 규칙은 1차원 배열에도 적용되는데, drop = FALSE를 쓰지 않으면 어떤 부분 선택이든 벡터 결과를 줍니다.
참고로 벡터는 1차원 배열과 구별됩니다. 후자는 dim과 dimnames 속성(둘 다 길이 1)을 가지기 때문이에요. 1차원 배열은 부분 선택 연산으로 쉽게 얻을 수는 없지만 명시적으로 구성할 수 있고, table이 반환합니다. 이것이 때로 유용한 이유는 dimnames 리스트의 요소들이 그 자체로 이름을 가질 수 있기 때문인데, names 속성은 그렇지 않습니다.
m[FALSE, ] 같은 일부 연산은 어떤 차원이 0의 크기를 가지는 구조를 만듭니다. R은 일반적으로 이런 구조를 합리적으로 처리하려고 합니다.
3.4.3 다른 구조의 인덱싱 (Indexing other structures)
연산자 [는 **제네릭 함수(generic function)**여서 클래스 메서드를 추가할 수 있고, $와 [[ 연산자도 마찬가지입니다. 따라서 어떤 구조에 대해서도 사용자 정의 인덱싱 연산을 만들 수 있어요. 가령 [.foo 같은 함수는 첫 인자가 인덱싱되는 구조이고 나머지가 인덱스들인 인자 집합을 받아 호출됩니다. $의 경우에는 x$"abc" 형태를 써도 인덱스 인자의 mode가 "symbol"입니다. 클래스 메서드가 기본 메서드와 항상 똑같이 동작하지는 않는다는 점(예를 들어 부분 매칭 측면에서)을 알아 두는 것이 중요해요.
[의 클래스 메서드 중 가장 중요한 예는 **데이터 프레임(data frame)**용입니다. 여기서 자세히 설명하지는 않지만([.data.frame의 도움말 참고), 대략적으로 말하면 두 인덱스가 제공되면(하나가 비어 있어도) 기본적으로 같은 길이의 벡터들의 리스트인 구조에 대해 행렬과 같은 인덱싱을 만들어 냅니다. 단일 인덱스가 제공되면 열의 리스트를 인덱싱하는 것으로 해석됩니다 — 그 경우 drop 인자는 무시되며 경고가 발생합니다.
기본 연산자 $와 [[는 환경(environment)에도 적용될 수 있습니다. 이때는 문자 인덱스만 허용되고 부분 매칭은 수행되지 않습니다.
3.4.4 부분 집합 할당 (Subset assignment)
구조의 부분 집합에 할당하는 것은 **복합 할당(complex assignment)**이라는 일반 메커니즘의 특별한 경우입니다.
x[3:5] <- 13:15
이 명령의 결과는 다음을 실행한 것과 같습니다.
`*tmp*` <- x
x <- "[<-"(`*tmp*`, 3:5, value=13:15)
rm(`*tmp*`)
인덱스는 먼저 숫자 인덱스로 변환된 다음, for 반복을 쓴 것처럼 숫자 인덱스를 따라 요소들이 순차적으로 교체된다는 점을 주목하세요. 이미 존재하는 *tmp*라는 이름의 변수는 덮어써졌다가 삭제되므로, 이 변수 이름은 코드에서 사용하면 안 됩니다.
같은 메커니즘은 [ 이외의 함수에도 적용할 수 있습니다. **치환 함수(replacement function)**는 같은 이름에 <-가 덧붙은 형태입니다. 그 마지막 인자는 반드시 value라고 불러야 하며, 새로 할당할 값입니다. 예를 들어:
names(x) <- c("a","b")
는 다음과 동등합니다.
`*tmp*` <- x
x <- "names<-"(`*tmp*`, value=c("a","b"))
rm(`*tmp*`)
복합 할당의 중첩은 재귀적으로 평가됩니다.
names(x)[3] <- "Three"
는 다음과 동등합니다.
`*tmp*` <- x
x <- "names<-"(`*tmp*`, value="[<-"(names(`*tmp*`), 3, value="Three"))
rm(`*tmp*`)
둘러싸는 환경(enclosing environment)에서의 복합 할당(<<- 사용)도 허용됩니다.
names(x)[3] <<- "Three"
는 다음과 동등합니다.
`*tmp*` <- get("x", envir=parent.env(), inherits=TRUE)
names(`*tmp*`)[3] <- "Three"
x <<- `*tmp*`
rm(`*tmp*`)
또한 다음과도 동등합니다.
`*tmp*` <- get("x", envir=parent.env(), inherits=TRUE)
x <<- "names<-"(`*tmp*`, value="[<-"(names(`*tmp*`), 3, value="Three"))
rm(`*tmp*`)
오직 대상 변수만 둘러싸는 환경에서 평가됩니다. 그래서
e<-c(a=1,b=2)
i<-1
local({
e <- c(A=10,B=11)
i <-2
e[i] <<- e[i]+1
})
는 슈퍼할당(superassignment) 문장의 왼쪽(LHS)과 오른쪽(RHS) 양쪽에서 i의 지역(local) 값을, RHS에서는 e의 지역 값을 사용합니다. 그리고 바깥 환경의 e를 다음과 같이 설정합니다.
a b
1 12
즉, 슈퍼할당은 다음 네 줄과 동등합니다.
`*tmp*` <- get("e", envir=parent.env(), inherits=TRUE)
`*tmp*`[i] <- e[i]+1
e <<- `*tmp*`
rm(`*tmp*`)
마찬가지로
x[is.na(x)] <<- 0
는 다음과 동등합니다.
`*tmp*` <- get("x", envir=parent.env(), inherits=TRUE)
`*tmp*`[is.na(x)] <- 0
x <<- `*tmp*`
rm(`*tmp*`)
그리고 다음은 아닙니다.
`*tmp*` <- get("x", envir=parent.env(), inherits=TRUE)
`*tmp*`[is.na(`*tmp*`)] <- 0
x <<- `*tmp*`
rm(`*tmp*`)
이 두 후보 해석은 지역 변수 x가 추가로 존재할 때만 달라집니다. 슈퍼할당의 대상 변수와 같은 이름을 가진 지역 변수를 두지 않는 것이 좋아요. 이 경우가 버전 1.9.1 이하에서는 잘못 처리되었으므로, 그런 코드가 꼭 필요할 정도로 심각한 일은 없을 거예요.
3.5 변수의 유효 범위 (Scope of variables)
거의 모든 프로그래밍 언어에는 스코핑(scoping) 규칙이 있어서, 같은 이름이 서로 다른 객체에 쓰이는 것이 허용됩니다. 이 덕분에 예컨대 함수 안의 지역 변수가 전역 객체와 같은 이름을 가질 수 있어요.
R은 Pascal 같은 언어와 유사한 어휘적 스코핑(lexical scoping) 모델을 사용합니다. 그런데 R은 함수형 프로그래밍 언어라서 함수와 언어 객체의 동적 생성·조작을 허용하며, 이런 사실을 반영한 추가 기능이 있습니다.
- 전역 환경 (Global environment)
- 어휘 환경 (Lexical environment)
- 호출 스택 (The call stack)
- 검색 경로 (Search path)
3.5.1 전역 환경 (Global environment)
전역 환경(global environment)은 사용자 워크스페이스의 뿌리입니다. 명령줄에서의 할당 연산은 해당 객체가 전역 환경에 속하게 합니다. 전역 환경의 둘러싸는 환경(enclosing environment)은 검색 경로의 다음 환경이고, 그렇게 계속 거슬러 올라가 base 환경을 둘러싸는 **빈 환경(empty environment)**에 이르게 됩니다.
3.5.2 어휘 환경 (Lexical environment)
함수를 호출할 때마다 그 함수 안에서 만들어진 **지역 변수들을 담는 프레임(frame)**이 생성되고, 그것이 어떤 환경에서 평가되며, 이 조합으로 새로운 환경이 만들어집니다.
용어를 정확히 짚고 갈게요. **프레임(frame)**은 변수들의 집합이고, **환경(environment)**은 프레임들의 중첩입니다(동등하게 말하면, 가장 안쪽 프레임에다 둘러싸는 환경을 더한 것입니다).
환경은 변수에 할당되거나 다른 객체 안에 포함될 수 있습니다. 다만 이들은 표준 객체가 아닙니다 — 특히 할당할 때 복사되지 않아요.
클로저(closure, mode가 "function"인 객체) 객체는 정의의 일부로 그것이 생성된 환경을 담습니다(기본적으로 그렇습니다. 환경은 environment<-로 조작할 수 있습니다). 함수가 이후에 호출되면, 그 평가 환경(evaluation environment)은 클로저의 환경을 둘러싸는 환경으로 하여 생성됩니다. 이 환경이 반드시 호출자(caller)의 환경은 아니라는 점을 주의하세요!
따라서 함수 안에서 변수를 요청하면, 그 변수는 먼저 평가 환경에서, 그다음 둘러싸는 환경에서, 또 그 둘러싸는 환경의 둘러싸는 환경에서…… 식으로 차례로 탐색됩니다. 전역 환경이나 어떤 패키지의 환경에 도달하면, 탐색은 **검색 경로(search path)**를 따라 base 패키지의 환경까지 계속됩니다. 거기서도 찾지 못하면 다음으로 빈 환경으로 진행되고, 결국 실패합니다.
3.5.3 호출 스택 (The call stack)
함수가 호출될 때마다 새 평가 프레임이 만들어집니다. 계산 중의 어느 시점이든 현재 활성화된 환경들은 **호출 스택(call stack)**을 통해 접근할 수 있어요. 함수가 호출될 때마다 **컨텍스트(context)**라고 하는 특수한 구성이 내부적으로 만들어져 컨텍스트 목록에 놓입니다. 함수가 평가를 마치면 그 컨텍스트는 호출 스택에서 제거됩니다.
호출 스택 위쪽에서 정의된 변수들을 사용할 수 있게 하는 것을 **동적 스코프(dynamic scope)**라고 합니다. 그러면 변수의 바인딩은 (시간상) 가장 최근의 변수 정의로 결정됩니다. 이것은 R의 기본 스코핑 규칙과는 배치돼요. R은 기본적으로 함수가 정의된 환경의 바인딩을 사용하니까요(어휘 스코프). 모델 수식을 사용하고 조작하는 함수 같은 일부 함수들은 호출 스택에 직접 접근해서 동적 스코프를 흉내 낼 필요가 있습니다.
호출 스택에 대한 접근은 이름이 sys.로 시작하는 일련의 함수들로 제공됩니다. 간단히 나열하면 다음과 같아요.
sys.call 지정된 컨텍스트의 호출(call)을 얻습니다. sys.frame 지정된 컨텍스트의 평가 프레임을 얻습니다. sys.nframe 활성 컨텍스트 전부의 환경 프레임을 얻습니다. sys.function 지정된 컨텍스트에서 호출되고 있는 함수를 얻습니다. sys.parent 현재 함수 호출의 부모를 얻습니다. sys.calls 활성 컨텍스트 전부의 호출을 얻습니다. sys.frames 활성 컨텍스트 전부의 평가 프레임을 얻습니다. sys.parents 활성 컨텍스트 전부의 숫자 레이블을 얻습니다. sys.on.exit 지정된 컨텍스트가 종료될 때 실행될 함수를 설정합니다. sys.status sys.frames, sys.parents, sys.calls를 호출합니다. parent.frame 지정된 부모 컨텍스트의 평가 프레임을 얻습니다.
3.5.4 검색 경로 (Search path)
평가 환경 구조에 더해, R에는 다른 곳에서 찾지 못한 변수를 검색하는 환경들의 검색 경로가 있습니다. 이는 두 가지 용도로 쓰여요. 함수 패키지들과, attach된 사용자 데이터입니다.
검색 경로의 첫 번째 요소는 전역 환경이고 마지막은 base 패키지입니다. Autoloads 환경은 요청 시 로드될 수 있는 프록시 객체를 담는 데 사용됩니다. 다른 환경들은 attach나 library로 경로에 삽입됩니다.
네임스페이스(namespace)를 가진 패키지는 다른 검색 경로를 가집니다. 이런 패키지의 객체에서 R 객체 탐색이 시작되면, 먼저 패키지 자체가, 그다음 그 imports가, 그다음 base 네임스페이스, 마지막으로 전역 환경과 나머지 일반 검색 경로가 검색됩니다. 그 효과는, 같은 패키지 안의 다른 객체에 대한 참조가 그 패키지로 해석되고, 객체들이 전역 환경이나 다른 패키지에 있는 같은 이름의 객체에 의해 가려지지(mask) 않는다는 것입니다.
더 알아보기
- 뒤이어 나오는 장 4 Functions에서 함수의 정의와 인자 처리, 클로저를 더 깊이 다룹니다.
- Indexing 관련 세부 동작은
?Extract, 데이터 프레임 인덱싱은?[.data.frame도움말을 참고하세요. - 평가 환경·검색 경로를 직접 실험해 보려면
environment(),search(),parent.frame(),sys.calls()등을 콘솔에서 실행해 보면 도움이 됩니다. - 원문에서 3.3.2 이름의 전파와 3.3.3 차원 속성 소절은 미완성(stub) 상태입니다. 완성된 규칙이 필요하면 R 도움말의 Arithmetic, Syntax 페이지를 참고하는 편이 정확합니다.