나뭇잎 교과서

함수와 매개변수

print, len, sorted처럼 이미 함수를 많이 써 왔습니다. 이제 직접 함수를 만들 차례입니다. 함수를 부르는 순간 컴퓨터 안에서는 무슨 일이 일어나고, 넘긴 값은 함수 안에서 어떻게 쓰일까요?

먼저 알면 좋은 것 변수와 대입리스트
본문

함수와 매개변수

본문

두 함수를 만들었다. 하나는 숫자를 받아 그 매개변수에 5를 넣고, 다른 하나는 리스트를 받아 그 리스트에 원소 하나를 덧붙인다. 함수를 부른 뒤 밖에서 값을 확인하면, 숫자는 그대로인데 리스트에는 원소가 늘어 있다. 둘 다 함수 안에서 바꾸었는데 왜 결과가 다를까? 함수를 부르는 순간 컴퓨터 안에서는 무슨 일이 일어나고, 넘긴 값은 함수 안에서 어떻게 쓰일까?

그림 1. 함수가 다른 함수를 부를 때마다 호출 스택에 프레임이 하나씩 쌓인다(주황이 지금 실행 중인 함수). 함수가 끝나면 맨 위 프레임이 사라지고 바로 아래 함수가 이어서 실행된다.
먼저 알아야 할 것변수와 자료형, 리스트, 조건문과 반복문(정보), 함수의 뜻(수학).

1.1.1함수 호출과 스택 프레임

함수는 이름을 붙인 코드 묶음이다. 함수를 부르면 컴퓨터는 지금 실행하던 위치를 기억해 두고, 그 함수만을 위한 작업 공간을 새로 만든다. 이 작업 공간을 스택 프레임이라 한다. 프레임에는 넘겨받은 값을 담는 매개변수, 함수 안에서 만든 지역 변수, 끝나고 돌아갈 위치가 들어 있다. 함수 안에서 또 다른 함수를 부르면 그 위에 새 프레임이 쌓인다. 함수가 값을 돌려주며 끝나면 맨 위 프레임이 사라지고, 기억해 둔 위치로 돌아가 이어서 실행한다. 프레임들이 쌓인 모습을 호출 스택이라 한다.

정리 1.1.1 매개변수에 넘겨지는 것

함수를 부를 때 인수의 값이 매개변수라는 새 이름에 연결된다. 매개변수에 새 값을 대입하면 그 이름이 새 값을 가리킬 뿐, 밖의 변수는 바뀌지 않는다. 그런데 리스트처럼 바뀔 수 있는 객체를 넘기면 매개변수와 밖의 변수가 같은 객체를 가리키므로, 함수 안에서 객체 자체를 고치면(예를 들어 append) 밖에서도 바뀐 모습이 보인다.

그림 1에서 호출 깊이를 바꾸어 보자. main이 평균(점수)을 부르면 평균의 프레임이 쌓이고, 평균이 합계(점수)를 부르면 그 위에 또 쌓인다. 각 프레임의 지역 변수는 그 프레임 안에서만 쓰여, 이름이 같아도 서로 섞이지 않는다. 그래서 함수를 나누어 만들어도 변수 이름이 부딪치지 않는다. 그리고 함수가 끝나면 그 지역 변수도 프레임과 함께 사라진다. 처음 질문의 숫자는 함수 안의 매개변수만 새 값을 가리키다가 프레임과 함께 사라진 것이다.1

1.1.2좋은 함수의 조건

함수를 잘 나누면 같은 코드를 되풀이해 쓰지 않아도 되고, 고칠 곳이 한 군데로 모인다. 좋은 함수는 한 가지 일만 하고, 이름만 보고도 무엇을 하는지 알 수 있다. 필요한 값은 매개변수로 받고 결과는 return으로 돌려주어, 함수 밖의 변수에 몰래 기대지 않는다. 그러면 같은 입력에 늘 같은 출력이 나와 시험하기 쉽다. 함수 안에서 넘겨받은 리스트를 고쳐야 한다면, 그 사실을 함수 이름이나 설명에 밝혀 두어야 부르는 쪽이 헷갈리지 않는다. 기본값을 준 매개변수는 자주 쓰는 값을 생략하게 해 준다.

함수 안에서 한 일넘긴 것밖의 변수 x = 5 (새 값 대입)숫자 3그대로 3 목록 = [9] (새 값 대입)리스트 [1, 2]그대로 [1, 2] 목록.append(9) (객체 수정)리스트 [1, 2][1, 2, 9]로 바뀜
그림 2. 이름에 새 값을 대입하는 것과, 이름이 가리키는 객체를 고치는 것은 다르다.

1.1.3예제 풀이

예제 1.1.2

함수 def 덧붙이기(목록, 값=0): 목록.append(값); return len(목록)와 def 두배(x): x = x * 2; return x가 있다. a = [1, 2], b = 3일 때 n = 덧붙이기(a, 5), m = 두배(b), k = 덧붙이기(a)를 차례로 실행한 뒤 a, b, n, m, k의 값을 구하여라.

덧붙이기(a, 5)에서 매개변수 목록은 a와 같은 리스트를 가리키므로 append로 그 리스트에 5가 붙어 a는 [1, 2, 5]가 되고, n은 3이다. 두배(b)에서 x는 3에서 시작해 6을 새로 가리키고, 6을 돌려주므로 m은 6이지만 b는 그대로 3이다.

덧붙이기(a)는 값을 생략했으므로 기본값 0이 쓰여 a는 [1, 2, 5, 0], k는 4가 된다. 따라서 a = [1, 2, 5, 0], b = 3, n = 3, m = 6, k = 4이다. 즉 함수 밖의 값을 바꾸고 싶다면 결과를 return으로 돌려받아 다시 대입하는 것이 가장 분명하다. 그래서 b를 바꾸려면 b = 두배(b)처럼 쓴다.

아래 탐구 카드에서 매개변수와 지역 변수가 호출 스택에서 어떻게 생기고 사라지는지 따라가 보자.

  1. 파이썬은 호출 스택이 너무 깊어지면 기본으로 약 1000단계에서 RecursionError를 낸다. 끝나지 않는 재귀가 메모리를 다 쓰지 않게 막는 안전장치이다.
마무리

함수를 부르면 매개변수, 지역 변수, 돌아갈 위치를 담은 스택 프레임이 호출 스택에 쌓이고, 함수가 끝나면 그 프레임이 사라진다. 매개변수는 인수의 값에 연결된 새 이름이라, 새 값을 대입하면 밖에 영향이 없지만 리스트처럼 바뀔 수 있는 객체를 고치면 밖에서도 보인다. 그래서 처음 질문의 숫자는 함수 안의 이름만 새 값을 가리키다 사라졌고, 리스트는 밖과 같은 객체를 고쳐 바뀐 모습이 남았다.

탐구 1

함수에 넘긴 리스트는 왜 함수 밖에서도 바뀔까?

함수 정의와 호출 · 매개변수 · 지역 변수 · 객체 참조 전달

              

출력—

초록 테두리 프레임이 지금 실행 중인 함수이다. 그림에서는 값이 같은 작은 정수를 한 객체로 그렸다(CPython도 −5~256의 정수는 미리 만들어 둔 객체를 함께 쓴다).

  1. 질문함수 안에서 매개변수 box에 원소를 추가했더니, 함수 밖의 nums까지 바뀌었다. 그런데 같은 함수 안에서 x를 10배로 바꿔도 밖의 n은 그대로이다. 무엇이 다를까?
  2. 복원파이썬의 변수는 값을 담는 상자가 아니라 객체를 가리키는 이름표이다. a = b는 b가 가리키는 객체에 이름표 a를 하나 더 붙이는 일이다. 함수는 입력을 받아 일을 하고 결과를 돌려주는 코드 묶음이다.
  3. 설명def를 실행하면 함수 객체만 만들어지고 몸체는 실행되지 않는다(정의). 함수를 호출하면 ① 새 프레임이 생기고 ② 매개변수 이름이 인자가 가리키는 객체를 그대로 가리키며 ③ 몸체를 실행하고 ④ return의 값을 호출한 자리로 돌려준 뒤 프레임이 사라진다. 정의할 때 쓴 이름(box, x)을 매개변수, 호출할 때 넘긴 값(nums, n)을 인자라 한다. 파이썬은 객체를 복사하지 않고 참조를 넘기므로 이를 객체 참조 전달(call by object reference)이라 부른다.
  4. 연결리스트·딕셔너리는 고칠 수 있는(mutable) 객체라 append, xs[0] = …로 객체 자체가 바뀌고, 같은 객체를 가리키는 모든 이름에서 그 변화가 보인다. 정수·문자열·튜플은 고칠 수 없는(immutable) 객체라 x = x * 10은 새 객체를 만들고 지역 이름표만 옮긴다. 함수 안에서 대입한 이름은 그 함수의 지역 변수이므로, 전역에 같은 이름이 있어도 서로 다른 변수이다.
  5. 적용세 예제를 한 단계씩 실행해 보자. append와 box = box + [x]에서 2번 줄 다음에 box의 화살표가 어디를 가리키는지 비교해 보자. ‘기본값 · 지역 변수’ 예제에서 함수 안의 total과 전역 total이 같은 객체를 가리킨 적이 있는지 확인해 보자. n을 바꾸면 출력은 어떻게 달라지는가?
  6. 판단“파이썬은 리스트는 참조로, 정수는 값으로 전달한다”는 설명은 틀렸다. 전달 방식은 언제나 같고, 차이는 객체를 고칠 수 있는지와 함수 안에서 객체를 고쳤는지, 이름을 다시 대입했는지에 있다. 원본을 지키려면 add_item(nums.copy(), n)처럼 복사본을 넘기거나, 함수가 새 값을 return하게 설계한다.

확인조건이 달라지면

다음 코드를 실행하면 무엇이 출력될까?

def f(xs, k):
          xs[0] = 99
          k = k + 1

      a = [1, 2]
      b = 3
      f(a, b)
      print(a, b)
읽기 2

함수는 어떻게 값을 받고, 무엇을 돌려줄까?

읽기 · 함수 정의 · 매개변수의 종류 · 반환값 · 유효 범위

같은 계산을 프로그램 여러 곳에서 쓰면 코드를 복사해 붙이기 쉽다. 그런데 계산에 실수가 있으면 붙여 넣은 곳을 모두 찾아 고쳐야 한다. 함수는 이 문제를 어떻게 해결하고, 함수를 쓸 때 무엇을 정해야 할까?

1함수로 나누는 까닭

함수는 일을 하는 코드에 이름을 붙인 것이다. 한 번 정의하면 이름으로 몇 번이든 부를 수 있고, 고칠 곳도 한 곳뿐이다. 더 큰 쓸모는 추상화이다. sorted(xs)를 쓸 때 정렬이 안에서 어떻게 일어나는지 몰라도 되는 것처럼, 함수를 쓰는 쪽은 “무엇을 넣으면 무엇이 나오는가”만 알면 된다. 큰 문제를 작은 함수로 나누면 각 함수를 따로 시험하고 고칠 수 있다.

def bmi(weight, height):          # 정의: 이름, 매개변수
          return weight / height ** 2    # 몸체와 반환값

      print(bmi(60, 1.7))               # 호출: 인자 60, 1.7 → 20.76…

정의할 때 괄호 안에 쓴 이름(weight, height)이 매개변수이고, 호출할 때 넘기는 값(60, 1.7)이 인자이다. 함수가 받을 값과 돌려줄 값을 먼저 정하고 나서 몸체를 쓰면, 함수의 쓰임이 분명해진다.

2인자를 넘기는 여러 방법

방식예뜻
위치 인자power(2, 10)순서대로 매개변수에 짝지어진다. base=2, exp=10
키워드 인자power(exp=10, base=2)이름으로 짝짓므로 순서를 바꿔도 된다.
기본값def power(base, exp=2):인자를 생략하면 기본값을 쓴다. power(5) → 25
가변 인자def total(*nums):넘긴 위치 인자들을 튜플 하나로 받는다. total(1, 2, 3)에서 nums = (1, 2, 3)

기본값이 있는 매개변수는 기본값이 없는 매개변수 뒤에 써야 한다. def f(a=1, b):는 문법 오류이다.

3반환값

return을 만나면 함수는 그 자리에서 끝나고 값을 호출한 곳으로 돌려준다. 그 아래 코드는 실행되지 않는다.

return이 없거나 값 없이 return만 쓰면 None을 돌려준다. 화면에 print하는 것과 값을 돌려주는 것은 다르다. print한 값은 다른 계산에 쓸 수 없다.

return q, r처럼 쓰면 튜플 하나를 돌려주고, 받는 쪽에서 q, r = f(…)처럼 나누어 받을 수 있다. 내장 함수 divmod(17, 5)가 (3, 2)를 돌려주는 것과 같은 방식이다.

4유효 범위, 이름은 어디에서 보일까

함수 안에서 대입한 이름은 그 함수의 지역 변수이다. 호출할 때 생겨서 함수가 끝나면 사라진다. 파이썬은 이름을 찾을 때 지역 → 바깥 함수 → 전역 → 내장 순서로 찾는다. 그래서 함수 안에서 전역 변수를 읽기는 할 수 있지만, 대입하는 순간 그 이름은 지역 변수가 된다.

count = 0
      def inc():
          count = count + 1     # UnboundLocalError: 지역 변수 count를 값이 생기기 전에 읽음

      def inc2():
          global count          # 전역 count를 쓰겠다고 밝힘
          count = count + 1

global은 꼭 필요할 때만 쓴다. 여러 함수가 전역 변수를 고치면 어디서 값이 바뀌었는지 추적하기 어렵다. 대개는 값을 매개변수로 받고 결과를 return하는 편이 낫다.

판단기본값은 함수를 정의할 때 한 번 계산된다. def add(x, xs=[]):처럼 리스트를 기본값으로 쓰면, 호출할 때마다 같은 리스트 객체가 재사용되어 이전 호출에서 넣은 값이 남아 있다. 바뀔 수 있는 객체가 기본값으로 필요하면 xs=None으로 두고 함수 안에서 if xs is None: xs = []로 새로 만든다.

확인조건이 달라지면

다음 코드를 실행하면 화면에 무엇이 나올까?

def area(w, h=2):
          print(w * h)

      r = area(3)
      print(r)
AI 출제 문항연습문제 풀어보기13문항
Esc

↑↓ 고르기Enter 열기