함수 (Functions)

서론

데이터 과학자로서 역량을 높이는 가장 좋은 방법 중 하나는 함수를 작성하는 것입니다. 함수를 사용하면 공통적인 작업을 복사해서 붙여넣기보다 더 강력하고 일반적인 방식으로 자동화할 수 있습니다. 함수를 작성하면 복사해서 붙여넣는 방식보다 세 가지 큰 장점이 있습니다:

  1. 함수에 함축적인 이름을 부여하여 코드를 더 이해하기 쉽게 만들 수 있습니다.

  2. 요구사항이 바뀌었을 때 여러 곳이 아닌 단 한 곳의 코드만 업데이트하면 됩니다.

  3. 복사해서 붙여넣을 때 발생할 수 있는 사소한 실수(예: 한 곳의 변수 이름은 바꿨는데 다른 곳은 바꾸지 않음)를 방지할 수 있습니다.

좋은 함수를 작성하는 것은 평생의 여정입니다. 수년간 파이썬을 사용한 후에도 새로운 기술과 오래된 문제에 접근하는 더 나은 방법을 계속 배울 수 있습니다. 이 장의 목표는 함수의 모든 비전적인 세부 사항을 가르치는 것이 아니라, 즉시 적용할 수 있는 몇 가지 실용적인 조언으로 여러분을 시작하게 하는 것입니다.

함수 작성을 위한 실용적인 조언과 더불어, 이 장에서는 코드 스타일을 지정하는 방법에 대한 몇 가지 제안도 제공합니다. 좋은 코드 스타일은 올바른 문장 부호와 같습니다. 문장부호가없어도살아갈수는있겠지만있어야읽기가훨씬수월합니다! 문장 부호의 스타일이 다양하듯이 코드 스타일도 여러 가지 변형이 가능합니다. 여기서는 우리가 사용하는 스타일을 제시하지만, 가장 중요한 것은 일관성을 유지하는 것입니다.

사전 준비

이 장에서는 pandasnumpy 패키지가 필요합니다.

함수 (Functions)

함수는 입력을 받고, 기능을 수행하며, 출력을 반환합니다. 함수는 ’함수를 정의하다’라는 의미의 def 키워드로 시작합니다. 그 다음 함수 이름이 오고, 그 뒤에 괄호 ()가 오는데 이 괄호 안에는 함수의 인수(arguments)와 키워드 인수(keyword arguments)가 포함될 수 있습니다. 그 뒤에 콜론 :이 옵니다. 함수의 본문은 왼쪽 끝에서 들여쓰기 되어야 합니다. 함수 인수는 이름 뒤의 괄호 안에 정의되며, 여러 입력은 쉼표로 구분됩니다. 모든 출력은 return 키워드 뒤에 제공되며, 여러 변수가 있을 경우 역시 쉼표로 구분합니다.

단일 인수(arg)를 가진 매우 간단한 함수 예제를 보겠습니다:

코드 보기
def welcome_message(name):
    return f"안녕하세요 {name}님, 환영합니다!"


# 들여쓰기가 없는 이 코드는 함수의 일부가 아닙니다.
name = "Ada"
output_string = welcome_message(name)
print(output_string)
안녕하세요 Ada님, 환영합니다!

함수의 강력한 기능 중 하나는 입력 인수에 대한 기본값을 정의할 수 있다는 점입니다. 이를 키워드 인수(kwargs)라고 합니다. name에 대한 기본값과 환영 메시지 및 점수라는 두 가지 출력을 정의하여 이를 실제로 확인해 보겠습니다.

코드 보기
def score_message(score, name="학생"):
    """함수를 설명하는 독스트링(doc-string)입니다.
    Args:
        score (float): 원점수
        name (str): 학생 이름
    Returns:
        str: 환영 메시지.
        float: 정규화된 점수.
    """
    norm_score = (score - 50) / 10
    return f"안녕하세요 {name}님", norm_score


# 들여쓰기가 없는 이 코드는 함수의 일부가 아닙니다.
name = "Ada"
score = 98
# 이름 입력 안 함
print(score_message(score))
# 이름 입력 함
print(score_message(score, name=name))
('안녕하세요 학생님', 4.8)
('안녕하세요 Ada님', 4.8)

```{.callout-note} 인수와 키워드 인수 :class: tip

인수(arguments)는 함수가 항상 필요로 하는 변수입니다. 예를 들어 def add(a, b): return a + b에서 ab가 이에 해당합니다. 이것들이 없으면 함수가 작동하지 않습니다! 함수 인수는 종종 args라고 불립니다.

키워드 인수(Keyword arguments)는 함수에 선택적으로 전달되는 변수입니다. 예를 들어 def add(a, b, c=5): return a + b - c에서 c가 이에 해당합니다. 함수를 호출할 때 c의 값을 제공하지 않으면 자동으로 c=5가 사용됩니다. 키워드 인수는 종종 kwargs라고 불립니다.



```{.callout-note} 연습 문제
`return` 문 뒤에 쉼표로 구분된 여러 개의 반환값이 있는 함수의 반환 타입은 무엇일까요?

마지막 예제에서 함수에 일부 텍스트를 추가한 것을 보셨을 것입니다. 이것은 독스트링(doc-string), 즉 문서화 문자열입니다. 사용자가(아마도 미래의 여러분이) 함수가 무엇을 하는지 이해하도록 돕기 위해 존재합니다. score_message 함수에 help()를 호출하여 이것이 실제로 어떻게 작동하는지 보겠습니다:

코드 보기
help(score_message)
Help on function score_message in module __main__:

score_message(score, name='학생')
    함수를 설명하는 독스트링(doc-string)입니다.
    Args:
        score (float): 원점수
        name (str): 학생 이름
    Returns:
        str: 환영 메시지.
        float: 정규화된 점수.

```{.callout-note} 연습 문제 입력이 “coding for economists”와 같으면 하이파이브 유니코드 문자를 반환하고, 그렇지 않으면 슬픈 표정인 “:-/”를 반환하는 함수를 작성해 보세요.

기본 인수로 빈 문자열을 받는 두 번째 인수를 추가하되, 만약 값이 전달되면 반환 메시지에 추가(결합)되도록 하세요. 이를 사용하여 “:-/ here is my message.”라는 결과물이 나오도록 해보세요.

여러분의 함수를 위한 독스트링을 작성하고 help를 호출해 보세요.


args와 kwargs에 대해 더 배우려면, 이 [짧은 비디오 튜토리얼](https://calmcode.io/args-kwargs/introduction.html)을 확인해 보세요.

## 언제 함수를 작성해야 할까요?

코드 블록을 두 번 이상 복사해서 붙여넣었다면(즉, 이제 같은 코드의 사본이 세 개라면) 함수 작성을 고려해야 합니다.
예를 들어, 이 코드를 보십시오.
이 코드는 무엇을 하나요?

::: {#7a744085 .cell execution_count=4}
``` {.python .cell-code}
import numpy as np
import pandas as pd

df = pd.DataFrame(np.random.normal(size=(10, 4)), columns=["a", "b", "c", "d"])

df["a"] = (df["a"] - df["a"].min()) / (df["a"].max() - df["a"].min())
df["b"] = (df["b"] - df["b"].min()) / (df["b"].max() - df["a"].min())
df["c"] = (df["c"] - df["c"].min()) / (df["c"].max() - df["c"].min())
df["d"] = (df["d"] - df["d"].min()) / (df["d"].max() - df["d"].min())

:::

각 열의 범위를 0에서 1 사이로 리스케일링(rescaling)한다는 것을 알아차리셨을 수도 있습니다. 하지만 실수를 발견하셨나요? df["b"]에 대한 코드를 복사해서 붙여넣을 때 에러가 있었습니다: 누군가 ab로 바꾸는 것을 잊었습니다. 반복되는 코드를 함수로 추출하는 것은 이런 종류의 실수를 방지하기 때문에 좋은 아이디어입니다.

함수를 작성하려면 먼저 코드를 분석해야 합니다. 입력이 몇 개인가요?

df["a"] - df["a"].min() / (df["a"].max() - df["a"].min())

이 코드에는 df["a"]라는 하나의 입력만 있습니다. 입력을 더 명확하게 하기 위해 일반적인 이름을 가진 임시 변수를 사용하여 코드를 다시 작성하는 것이 좋습니다. 여기서는 단일 숫자 벡터만 필요하므로 이를 x라고 부르고 함수에 넣겠습니다.

함수는 def 키워드로 시작합니다. 이름이 오고, 그 뒤에 인수와 키워드 인수가 들어갈 수 있는 괄호 ()가 옵니다. 그 뒤에 콜론이 오고 본문은 들여쓰기 됩니다.

따라서 파이썬에서 함수는 다음과 같은 형태를 가집니다:

def 함수_이름(<입력들>):
    <입력에 대해 수행할 코드>
    <적절한 경우 return>

여기에 적용해 보면 다음과 같습니다:

def rescale(x):
    return (x - x.min()) / (x.max() - x.min())

이 코드에는 여전히 중복이 있습니다. 데이터의 최소값을 두 번 계산하고 있으므로, 한 번만 계산하는 것이 합리적입니다:

코드 보기
def rescale(x):
    minimum = x.min()
    return (x - minimum) / (x.max() - minimum)

중간 계산 결과들을 이름이 있는 변수로 뽑아내는 것은 코드가 무엇을 하는지 더 명확하게 해주기 때문에 좋은 습관입니다.

새로운 함수를 만드는 데는 세 가지 핵심 단계가 있습니다:

  1. 함수의 이름을 선택해야 합니다. 여기서는 벡터를 0과 1 사이로 리스케일링하므로 rescale을 사용했습니다.

  2. function 내부의 입력, 즉 인수들을 나열합니다. 여기서는 인수가 하나뿐입니다. 더 많다면 function(x, y, z)와 같은 형태가 될 것입니다. (또한 인수 뒤에 data=와 같은 이름이 있는 키워드 인수를 가질 수도 있습니다.)

  3. 여러분이 개발한 코드를 함수의 본문에 배치합니다. 이는 def ...: 바로 뒤에 오는 블록입니다.

전체적인 과정을 주목하세요: 우리는 간단한 입력으로 작동하는 방식을 알아낸 후에야 함수를 만들었습니다. 작동하는 코드에서 시작하여 함수로 바꾸는 것이 훨씬 쉽습니다. 함수부터 만들고 나서 작동시키려고 하면 더 어렵습니다.

이 시점에서 몇 가지 다른 입력으로 함수를 확인해 보는 것이 좋습니다:

코드 보기
rescale(pd.Series([-10, 0, 10]))
0    0.0
1    0.5
2    1.0
dtype: float64
코드 보기
rescale(pd.Series([1, 2, 3, np.nan, 5]))
0    0.00
1    0.25
2    0.50
3     NaN
4    1.00
dtype: float64

더 많은 함수를 작성하게 되면 결국 이러한 비공식적이고 대화형인 테스트들을 공식적이고 자동화된 테스트로 바꾸고 싶어질 것입니다. 그 과정을 유닛 테스트(unit testing)라고 합니다. 안타깝게도 이 책의 범위를 벗어납니다.

이제 함수가 있으므로 원래의 예제를 단순화할 수 있습니다:

코드 보기
df["a"] = rescale(df["a"])
df["b"] = rescale(df["b"])
df["c"] = rescale(df["c"])
df["d"] = rescale(df["d"])

원본과 비교했을 때, 이 코드는 이해하기 더 쉽고 한 부류의 복사-붙여넣기 실수를 제거했습니다. 여러 열에 대해 동일한 작업을 하고 있기 때문에 여전히 약간의 중복이 있습니다. 이것 또한 제거할 수 있는데, 그 방법은 책의 뒷부분에서 다루겠습니다.

함수의 또 다른 장점은 요구사항이 변경되었을 때 한 곳만 수정하면 된다는 것입니다. 예를 들어, 변수 중 일부에 무한대 값이 포함되어 있음을 발견했고 rescale()이 (사실상) 실패한다고 가정해 보겠습니다:

코드 보기
rescale(pd.Series([1, 2, 3, np.inf, 5]))
0    0.0
1    0.0
2    0.0
3    NaN
4    0.0
dtype: float64

코드를 함수로 추출했으므로 한 곳에서만 수정하면 됩니다:

코드 보기
def rescale(x):
    x = x.replace(np.inf, np.nan)
    minimum = x.min()
    return (x - minimum) / (x.max() - minimum)


rescale(pd.Series([1, 2, 3, np.inf, 5]))
0    0.00
1    0.25
2    0.50
3     NaN
4    1.00
dtype: float64

이것은 “반복하지 마라(do not repeat yourself, DRY)” 원칙의 중요한 부분입니다. 코드에 반복이 많을수록 무언가 변경되었을 때 업데이트해야 할 곳을 기억하기가 더 어려워지며(항상 그렇듯이요!), 시간이 지남에 따라 버그가 발생할 가능성이 커집니다.

함수는 인간과 컴퓨터를 위한 것입니다

함수가 컴퓨터뿐만 아니라 인간을 위한 것이라는 점을 기억하는 것이 중요합니다. 대부분의 경우 파이썬은 함수 이름이 무엇인지, 어떤 주석이 달려 있는지 신경 쓰지 않지만, 이는 인간 독자들에게 중요합니다. 이 섹션에서는 인간이 이해할 수 있는 함수를 작성할 때 염두에 두어야 할 점들을 논의합니다.

함수의 이름은 중요합니다. 이상적으로 함수의 이름은 짧으면서도 함수가 하는 일을 명확하게 떠올릴 수 있게 해야 합니다. 어렵죠! 하지만 Visual Studio Code의 자동 완성이 긴 이름을 쉽게 칠 수 있게 해주므로 짧은 것보다 명확한 것이 낫습니다.

일반적으로 함수 이름은 동사여야 하고, 인수는 명사여야 합니다. 몇 가지 예외가 있습니다: 함수가 매우 잘 알려진 명사를 계산하거나(compute_mean보다 mean이 나음), 객체의 어떤 속성에 접근하는 경우(get_coefficients보다 coef가 나음) 명사가 괜찮습니다. “get”, “compute”, “calculate”, “determine”과 같은 매우 포괄적인 동사를 사용하고 있다면 명사가 더 나은 선택일 수 있다는 신호입니다. 여러분의 최선의 판단을 따르시고 나중에 더 좋은 이름이 생각나면 주저하지 말고 함수 이름을 바꾸세요.

# 동사도 아니고 설명적이지도 않음
my_awesome_function()
# 길지만 명확함
impute_missing()
collapse_years()

함수 이름이 여러 단어로 구성된 경우, 각 소문자 단어가 언더스코어로 구분되는 “snake_case”를 사용하세요. 비슷한 일을 하는 함수 군이 있다면 이름과 인수가 일관되도록 하세요. 그것들이 연결되어 있음을 나타내기 위해 공통 접두사를 사용하세요. 자동 완성을 통해 접두사를 입력하면 해당 패밀리의 모든 멤버를 볼 수 있으므로 공통 접미사보다 낫습니다.

# 좋음
input_select()
input_checkbox()
input_text()
# 별로 좋지 않음
select_input()
checkbox_input()
text_input()

이러한 디자인의 좋은 예가 pandas 패키지입니다: 데이터를 읽어오기 위해 어떤 함수가 필요한지 정확히 기억나지 않을 때 pd.read_를 입력하면 자동 완성이 옵션들을 보여주어 기억을 되살려줍니다.

함수의 범위 (Scope)

범위(Scope)는 코드의 어떤 부분에서 다른 부분을 볼 수 있는지를 의미합니다. 지역(local), 전역(global), 그리고 비지역(non-local)이라는 세 가지 다른 범위를 염두에 두어야 합니다.

지역 (Local)

함수 내부에서 변수를 정의하면 코드의 나머지 부분에서는 이를 ‘볼’ 수도 없고 사용할 수도 없습니다. 예를 들어 변수를 생성하는 함수와 해당 변수를 호출하는 예제입니다:

def var_func():
    str_variable = 'Hello World!'

var_func()
print(str_variable)

이 코드는 에러를 발생시킵니다. 여러분의 일반적인 코드 입장에서는 str_variable이 함수 외부에서 존재하지 않기 때문입니다. 이것이 함수 내부에만 존재하는 지역 변수의 예입니다.

함수 내부에서 변수를 만들고 이를 유지하고 싶다면, 다음과 같이 return str_variable을 사용하여 명시적으로 밖으로 전달해야 합니다:

코드 보기
def var_func():
    str_variable = "Hello World!"
    return str_variable


returned_var = var_func()
print(returned_var)
Hello World!

전역 (Global)

함수 외부에서 선언된 변수는 모든 곳에서 접근할 수 있기 때문에 전역 변수라고 합니다:

코드 보기
y = "나는 전역 변수입니다"


def print_y():
    print("함수 내부의 y:", y)


print_y()
print("함수 외부의 y:", y)
함수 내부의 y: 나는 전역 변수입니다
함수 외부의 y: 나는 전역 변수입니다