함수

함수 에서 R

1개의 연습 문제

함수 소개

함수는 기초 개념에서 다음과 같은 예제와 함께 소개했어요:

squareit <- function(x) {
    x * x
}

squareit(3)
#> [1] 9

# shorter form
squareit_short <- function(x) x ^ 2

squareit의 정의를 자세히 살펴보면 여러 부분을 구분할 수 있어요:

  • 함수는 매개변수 x를 받아요.
  • 함수 본문이 있는데, 보통 중괄호 { }로 감싸요.
  • 함수 객체가 만들어지면 변수 squareit에 할당돼요.

R에서 함수는 숫자나 문자열과 마찬가지로 일급 객체예요. 따라서 squareit <- function...은 문법적으로 x <- 42와 똑같은 할당이에요.

Advanced

함수의 구성 요소에 직접 접근하는 일은 보통은 드물지만, 아주 쉬워요.

class(squareit)
#> [1] "function"
formals(squareit)
#> $x

body(squareit)
#> {
#>     x * x
#> }

formals()로 얻은 매개변수는 $x 문법을 쓰는 배열처럼 보여요. 사실 타입은 키-값 쌍을 담는 특별한 배열인 pairlist예요.

body는 실행 가능한 코드이고, 타입은 language예요 (아마 신경 쓸 필요는 없는 부분이에요).

인자

R은 Python이나 Julia 같은 다른 스크립트 언어와 달리 위치 인자와 키워드 인자를 뚜렷하게 구분하지 않아요.

함수를 호출할 때 값은 위치로 넘기거나 이름으로 넘길 수 있어요. 후자는 인자가 많아 순서를 기억하기 어려운 복잡한 함수에 유용해요.

f <- function(x, y) x / y

# call positionally
f(4, 2)
#> [1] 2

# call by name
f(y = 2, x = 4)
#> [1] 2

선택적 인자

함수 정의에서 인자의 기본값을 지정할 수 있는데, 기본값이 없는 인자들 뒤에 와야 해요.

그러면 기본값을 그대로 쓸지, 다른 값으로 덮어쓸지 선택할 수 있어요.

g <- function(x, y = 2) x / y

# default y value
g(6)
#> [1] 3

# explicit y value
g(6, 3)
#> [1] 2

추가 인자

임의 개수의 추가 인자를 받으려면 정의에 ... (줄임표)를 사용해요. 함수 호출에서 넘긴 여분의 값들을 벡터로 바꿀 수도 있지만, 이 ... 인자를 활용하는 다른 방법도 있으니 계속 읽어봐요 (다른 여러 언어에서는 "varargs"라고 불러요).

var_f <- function(x, y, ...) {
  print(c(...))
}

var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"

함수 환경

앞에서 매개변수와 본문이 모두 함수의 구성 요소라고 했어요.

사실 세 번째 구성 요소가 더 있어요. 바로 함수가 정의된 환경이에요.

이것은 중첩 함수의 예로 잘 설명할 수 있어요:

outer_func <- function(x) {
  inner_func <- function(y) {
    x * y
  }
  
  inner_func(3)
}

outer_func(5)
#> [1] 15

함수 호출은 바깥 함수에 x = 5를 넘기고, 이 값은 그 함수 본문 안에서 사용할 수 있어요.

안쪽 함수는 바깥 함수 본문의 일부이고, x의 값에 접근할 수 있어요. 다르게 말하면 x = 5가 안쪽 함수의 환경에 있는 거예요.

기술적으로는 이것을 클로저라고 해요.

환경은 ... 인자에서 특히 중요한데, 이렇게 넘긴 값은 함수 본문 안의 함수 호출로 그대로 전달할 수 있거든요. 바깥 함수는 ... 인자가 무슨 뜻인지 알 필요도, 신경 쓸 필요도 없어요.

f_var <- function(x, ...) {
  sum(x, ...)
}

x <- c(1, 2, NA, 6)

# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA

# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9

이 기법은 stringr 같은 Tidyverse 라이브러리에서 아주 많이 쓰여요. stringr의 많은 함수는 stringi나 base R의 저수준 함수를 사용하기 편하게 감싼 래퍼예요. str_*() 함수에 넘긴 추가 인자는 그대로 그 저수준 함수로 전달돼요.

함수를 반환값으로 사용하기

바깥 함수는 안쪽 함수(이름이 있든, 아래에서 설명하는 익명 함수든)를 정의하고 그것을 반환값으로 사용할 수 있어요.

반환된 함수는 자신이 정의된 환경을 함께 담고 있어요.

times_y <- function(x) {
  # anonymous function - see next section
  \(y) x * y
}

f <- times_y(3)
class(f)
#> [1] "function"

f(4)
#> [1] 12

익명 함수

함수를 정의할 때는 보통 만들어진 함수 객체를 변수에 묶어요:

x <- 2

# function definition captures x from the environment
doubleit_short <- function(y) x * y

이렇게 하면 스크립트 뒤에서 함수를 쓰기 편하지만, 꼭 변수에 묶어야 하는 건 아니에요. 짧고 한 번만 쓰는 함수는 그 자리에서 바로 유용하게 쓸 수 있어요. 이름을 붙이지 않은 함수를 익명 함수라고 해요.

익명 함수는 워낙 자주 쓰여서 (R v4.1.0부터) 정의하는 축약 문법이 생겼어요. function이라는 단어를 백슬래시 \로 바꾸면 돼요.

이 문법의 예는 반환값을 다룬 앞 섹션에서 사용했어요.

수정 시 복사

R에서는 벡터의 개별 원소에 값을 할당할 수 있어요. 벡터를 함수 인자로 넘기고, 반환하기 전에 함수 본문에서 수정하면, 수정된 벡터를 얻게 돼요.

그런데 원래 벡터는 어떻게 됐을까요?

f <- function(vec) {
  vec[1] <- 42
  vec
}

vals <- c(1, 3, 4)

# f() returns a modified vector
f(vals)
#> [1] 42  3  4  

# the original vector is unchanged.
vals
#> [1] 1 3 4

R은 데이터 과학을 위해 만들어진 언어예요. 그 데이터를 모으는 데는 시간과 노력이 많이 들고, 어쩌면 어마어마한 비용이 들 수도 있어요. 그러니 데이터를 손상시키지 않는 게 중요해요!

일반적인 원칙은 (몇 가지 예외를 빼면) 수정 시 복사예요. 어떤 객체(예를 들어 벡터)가 나중에 문제를 일으킬 수 있는 방식으로 바뀌면, R은 수정된 복사본을 돌려주고 원본은 그대로 둬요.

큰 데이터 구조를 복사하는 데는 계산 비용이 많이 들 수 있지만, 대안이 데이터 손상이라면 보통 이쪽이 더 나은 선택이에요.

Caution

같은 데이터를 반복해서 복사하게 만드는 연산을 조심해요.

(Tidyverse 저자) Hadley Wickham의 말을 옮기면:

루프는 원래 느리지 않아요. 하지만 루프 안에 느린 연산을 넣기 쉽게 만드는 건 위험할 정도예요.

벡터화나 고차 함수가 이런 종류의 성능 저하를 피하는 데 도움이 돼요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요

함수 배우기