← 이전 | 목록 | 다음 →

함수

지난 시간 마지막에 말씀드렸죠. 드디어 여러분도 마법을 직접 만들 차례입니다.

그리고 오늘, 드디어 그 이야기도 할 겁니다.

print("Hello World!")

기억나십니까. 03강에서 이거 치고 “마법입니다”로 퉁쳤던 그 코드요.

지금까지 여러분이 쓴 print(), range(), append()… 이것들 전부 “마법입니다” 한마디로 넘어갔습니다. 이제 여러분이 함수를 직접 만들 줄 알게 될 거고, 그 지식으로 print()를 우주 끝까지 쫓아가서 파헤칠 것입니다. 도망갈 곳 없습니다. 오늘 강의 끝에서 다시 만납시다.

함수란

함수란, 코드 여러 줄을 이름 하나로 묶어놓은 것입니다.

한번 만들어봅시다.

def greet():
    print("안녕하세요!")

def는 “정의하다(define)”의 줄임말입니다. 그 뒤에 함수 이름, 그다음 (), 그다음 콜론. 낯이 익죠? if, for, while 다 이 생김새였습니다. 파이썬은 한번 정한 문법을 끝까지 우려먹습니다.

근데 이거 이렇게 실행해도 아무 일도 안 일어납니다.

def greet():
    print("안녕하세요!")

결과:


왜냐고요? 정의만 했지, 호출은 안 했으니까요. 레시피를 써놨다고 요리가 저절로 되는 게 아닙니다. 만든 함수를 실제로 쓰려면 이름 뒤에 ()를 붙여서 불러야 합니다.

def greet():
    print("안녕하세요!")

greet()

결과:

안녕하세요!

이 “이름 뒤에 ()를 붙인다”는 것도 낯이 익어야 정상입니다. print("...")도, range(5)도 다 이 모양이었습니다. 여러분이 지금까지 부르고 다닌 게 전부 함수였습니다.

몇 번이든 부를 수 있습니다.

def greet():
    print("안녕하세요!")

greet()
greet()
greet()

결과:

안녕하세요!
안녕하세요!
안녕하세요!

세 번 불렀으니 세 번 실행됐습니다. 07강에서 배운 반복문 없이도 코드를 재사용한 겁니다. 다만 세 번 정도야 이렇게 해도 되지만, 백 번 부르고 싶으면 그냥 for를 쓰세요. 함수와 반복문은 싸우는 사이가 아니라 협력하는 사이입니다.

매개변수와 인자

지금까지 만든 greet()는 매번 “안녕하세요!”만 외칩니다. 사람마다 다른 인사를 하고 싶으면 어떻게 할까요?

함수를 부를 때 값을 넘겨줄 수 있습니다.

def greet(name):
    print("안녕하세요, " + name + "님!")

greet("철수")
greet("영희")

결과:

안녕하세요, 철수님!
안녕하세요, 영희님!

여기서 용어를 하나 정리하고 갑시다. 헷갈리기 딱 좋거든요.

정리하면: 매개변수는 빈 상자, 인자는 그 상자에 들어갈 물건입니다. greet("철수")를 부르는 순간, "철수"(인자)가 name(매개변수)이라는 상자에 담깁니다.

똑같은 함수인데 부를 때 뭘 넘기냐에 따라 결과가 달라집니다. 놀랍지 않죠. 그냥 변수에 값 넣는 거랑 똑같습니다. 04강에서 이미 다 배운 겁니다.

매개변수 여러 개

매개변수는 쉼표로 여러 개 받을 수 있습니다.

def introduce(name, age):
    print(name + "는 " + str(age) + "살입니다.")

introduce("철수", 20)

결과:

철수는 20살입니다.

여기서 str(age)라고 쓴 게 보이시나요? age는 숫자(정수, int)인데, 문자열(str)이랑 +로 이어붙이려면 둘 다 같은 종류여야 합니다. 그래서 숫자를 문자열로 바꿔주는 str()을 썼습니다. 이것도 파이썬이 미리 만들어둔 함수입니다. (네, 여러분도 이제 눈치채셨겠지만, 이것도 그냥 함수였습니다.)

숫자랑 문자열을 그냥 +로 붙이면 어떻게 되냐고요?

print("나이: " + 20)

결과:

TypeError: can only concatenate str (not "int") to str

Python이 또 화냅니다. “문자열엔 문자열만 붙일 수 있다”고요. "나이: "는 문자열이고 20은 정수라서, 종류(타입)가 다르면 +로 못 붙입니다. 타입에 대해서는 나중에 더 깊게 다룹니다. 오늘은 “종류가 다르면 섞이지 않는다” 정도만 기억하세요.

인자를 순서대로 안 넣고, 이름을 붙여서 넣을 수도 있습니다.

def introduce(name, age):
    print(name + "는 " + str(age) + "살입니다.")

introduce(age=20, name="철수")

결과:

철수는 20살입니다.

순서가 뒤바뀌었는데도 제대로 나옵니다. age=20, name="철수"처럼 이름을 딱 지정해서 넘기면, 파이썬이 순서 상관없이 알맞은 자리에 넣어줍니다. 이걸 키워드 인자라고 부릅니다. 이따 print()를 파헤칠 때 다시 나오니까 기억해두세요.

반환값

지금까지 함수는 print()로 뭔가 출력만 했습니다. 근데 함수가 계산한 결과값을 돌려받고 싶을 때가 있습니다. 이럴 때 return을 씁니다.

def add(a, b):
    return a + b

result = add(3, 4)
print(result)

결과:

7

add(3, 4)를 부르면, 함수 안에서 a + b를 계산해서 return으로 돌려줍니다. 그 값이 result라는 변수에 저장됩니다.

print() 안 쓰고 그냥 호출만 하면 어떻게 될까요?

def add(a, b):
    return a + b

add(3, 4)

결과:


아무것도 안 나옵니다. return은 값을 돌려줄 뿐, 화면에 출력하는 게 아닙니다. 받은 값을 보고 싶으면 print()로 직접 찍어야 합니다.

print()랑 return을 헷갈려 하는 사람이 많습니다. 여러분은 헷갈리지 마세요. print()는 “화면에 보여줘”, return은 “이 값을 가지고 돌아갈게”입니다. 택배 기사님이 물건을 문 앞에 두고 사진만 찍고 가는 게 print()고, 물건을 여러분 손에 직접 쥐여주고 가는 게 return입니다.

return을 만나면 함수는 그 자리에서 끝

def check(n):
    if n > 0:
        return "양수"
    return "양수 아님"

print(check(5))
print(check(-3))

결과:

양수
양수 아님

check(5)를 부르면, n > 0이 참이라서 "양수"를 return하고, 함수는 그 즉시 끝나버립니다. 아래 있는 return "양수 아님"은 실행조차 안 됩니다.

check(-3)을 부르면, n > 0이 거짓이라서 첫 번째 if는 그냥 지나가고, 마지막 줄 return "양수 아님"이 실행됩니다.

06강에서 배운 if가 여기서도 나옵니다. 06강도 그냥 있던 게 아닙니다.

기본값이 있는 매개변수

매개변수에 기본값을 미리 정해둘 수 있습니다. 그러면 인자를 안 넘겨줘도 알아서 그 값을 씁니다.

def greet(name="이름 없음"):
    print("안녕하세요, " + name + "님!")

greet("철수")
greet()

결과:

안녕하세요, 철수님!
안녕하세요, 이름 없음님!

greet("철수")는 인자를 넘겼으니 그 값을 씁니다. greet()는 아무것도 안 넘겼으니, 미리 정해둔 기본값 "이름 없음"이 대신 들어갑니다.

부모님이 “용돈 얼마 필요해?” 물었을 때 대답을 안 하면 알아서 기본값만큼 주는 것과 비슷합니다. 실제로는 그런 부모님 없습니다. 파이썬만 이렇게 착합니다.

스코프

이제 좀 어려운 얘기를 해봅시다. 함수 안에서 만든 변수는 함수 밖에서 쓸 수 없습니다.

def set_number():
    x = 10

set_number()
print(x)

결과:

NameError: name 'x' is not defined

x를 분명히 만들었는데, 파이썬은 그런 거 모른답니다. 왜냐면 x는 set_number() 함수 안에서만 존재하기 때문입니다. 함수가 끝나는 순간, 함수 안에서 만든 변수는 전부 사라집니다.

이렇게 변수가 “어디서 살아있고 어디서 사라지는지”를 스코프(scope), 우리말로 유효 범위라고 부릅니다.

함수 안에서만 사는 변수를 지역 변수(local variable)라고 부르고, 함수 밖, 즉 어디서든 접근할 수 있는 변수를 전역 변수(global variable)라고 부릅니다.

x = 10  # 전역 변수

def show_x():
    print(x)

show_x()

결과:

10

이건 됩니다. 함수 안에서 함수 밖에 있는 변수를 읽는 건 문제없습니다. 문지기가 안에서 밖을 내다보는 건 자유입니다.

근데 반대는 안 됩니다.

def set_x():
    x = 10  # 이건 지역 변수

set_x()
print(x)

결과:

NameError: name 'x' is not defined

set_x() 안에서 만든 x는, 이름만 같을 뿐 함수 밖의 세상과는 아무 상관 없는 완전히 다른 변수입니다. 함수가 끝나면 같이 증발합니다. 문지기가 밖에서 안을 함부로 못 들여다보는 것과 같습니다.

그래도 굳이 함수 안에서 전역 변수를 진짜로 바꾸고 싶다면, global이라는 키워드를 씁니다.

x = 10

def set_x():
    global x
    x = 20

set_x()
print(x)

결과:

20

global x라고 선언하면, “이 함수 안에서 쓰는 x는 지역 변수가 아니라 진짜 바깥에 있는 그 x다”라고 파이썬한테 미리 알려주는 겁니다. 그러면 함수 안에서 값을 바꿔도 함수 밖까지 반영됩니다.

다만 이거 남발하면 코드 여기저기서 같은 변수를 건드리게 돼서, 나중에 어디서 값이 바뀌었는지 추적하기 지옥이 됩니다. global은 비상용 열쇠입니다. 웬만하면 return으로 값을 돌려주는 방식을 쓰세요.

클로저

이제 좀 더 신기한 걸 보여드리겠습니다. 함수 안에 함수를 또 만들 수 있습니다.

def outer():
    message = "안녕"

    def inner():
        print(message)

    inner()

outer()

결과:

안녕

inner()가 outer() 안에서 정의된 함수입니다. inner()는 자기 자신은 message라는 변수를 안 만들었는데도, 자기를 감싸고 있는 outer()의 message를 읽을 수 있습니다. 아까 배운 스코프 규칙(안에서 밖은 볼 수 있다)이 함수 안 함수에도 그대로 적용됩니다.

여기서 한 발짝 더 나가봅시다. inner()를 실행하는 대신, inner() 자체를 돌려주면 어떻게 될까요?

def outer():
    message = "안녕"

    def inner():
        print(message)

    return inner

my_function = outer()
my_function()

결과:

안녕

이게 좀 이상합니다. outer()는 이미 실행이 끝났습니다. 함수가 끝나면 그 안의 지역 변수는 사라진다고 아까 배웠잖아요. 그럼 message도 진작 사라졌어야 하는데, my_function()을 나중에 불러도 여전히 "안녕"이 나옵니다.

inner()가 outer()를 떠나면서, 자기가 쓰던 변수(message)를 몰래 가방에 챙겨서 나온 겁니다. 이렇게 자기가 태어난 곳의 변수를 기억하고 다니는 함수를 클로저(closure)라고 부릅니다.

한번 활용해봅시다.

def make_multiplier(n):
    def multiply(x):
        return x * n

    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))
print(triple(5))

결과:

10
15

make_multiplier(2)를 부르면, n에 2가 들어간 채로 multiply라는 함수가 만들어져서 돌아옵니다. 이 함수는 n=2를 평생 기억하고 다닙니다. 그래서 double(5)는 항상 5 * 2, 즉 10을 계산합니다.

make_multiplier(3)으로 만든 triple은 n=3을 기억하는 별개의 함수입니다. double이랑 triple은 같은 틀(make_multiplier)에서 나왔지만, 서로 다른 n을 각자 기억하는 완전히 다른 함수입니다.

이게 어디다 쓰냐고요? 지금 당장은 몰라도 됩니다. 나중에 코드를 깔끔하게 짜다 보면, “함수를 만드는 함수”가 필요할 때가 옵니다. 오늘은 “함수도 값처럼 주고받을 수 있고, 함수는 자기가 태어난 환경을 기억할 수 있다”라는 사실만 기억해두세요.

드디어, print()

약속대로 이제 print()를 파헤쳐봅시다.

print()는 사실 여러분이 방금 만든 greet(), add() 같은 함수들이랑 근본적으로 다르지 않습니다. 파이썬이 미리 만들어서 늘 쓸 수 있게 준비해둔 내장 함수(built-in function)일 뿐입니다.

다만 print()는 여러분이 만든 함수보다 좀 더 유연합니다. 값을 몇 개를 넣든 다 받아줍니다.

print(1)
print(1, 2)
print(1, 2, 3, 4, 5)

결과:

1
1 2
1 2 3 4 5

여러분이 지금까지 만든 함수는 매개변수 개수가 정해져 있었습니다. add(a, b)는 딱 2개만 받습니다. 근데 print()는 몇 개를 넣든 다 받습니다. 이게 어떻게 가능하냐면, 매개변수 앞에 *를 붙이면 “몇 개가 오든 다 긁어모아라”라는 뜻이 됩니다.

def my_print(*values):
    print(values)

my_print(1, 2, 3)

결과:

(1, 2, 3)

*values라고 쓰면, 넘어온 인자를 몇 개든 튜플(04강에서 배운 그 튜플입니다) 하나로 묶어서 받습니다. print()도 내부적으로 이런 식으로 동작해서 인자를 몇 개든 받을 수 있는 겁니다.

그리고 아까 배운 키워드 인자, 기억하시나요? print()도 키워드 인자를 받습니다.

print(1, 2, 3, sep=" - ")

결과:

1 - 2 - 3

sep(separator, 구분자)이라는 키워드 인자에 원하는 문자열을 넣으면, 값 사이를 띄어쓰기 대신 그걸로 이어붙입니다. 기본값은 " "(공백 한 칸)이라서, 여러분이 지금까지 아무것도 안 넣었을 때 값 사이에 저절로 띄어쓰기가 들어갔던 겁니다.

print("안녕", end="!!!\n")

결과:

안녕!!!

end라는 키워드 인자는 출력이 끝날 때 마지막에 붙일 문자열입니다. 기본값은 "\n"(줄바꿈)이라서, 여러분이 print()를 여러 번 부르면 자동으로 다음 줄로 넘어갔던 겁니다. 이걸 바꾸면 줄바꿈 없이 이어붙일 수도 있습니다.

정리하면, print()의 정체는 대략 이런 모양입니다.

def print(*values, sep=" ", end="\n"):
    # values를 sep으로 이어붙이고
    # 마지막에 end를 붙여서
    # 화면에 보여준다
    ...

(진짜 파이썬 내부 코드는 아닙니다. 개념만 이렇게 생겼다는 겁니다.)

여러분은 *values가 뭔지, sep이랑 end 같은 키워드 인자가 뭔지, 방금 다 배웠습니다. print()는 마법이 아니라, 그냥 파이썬이 미리 만들어둔 함수 하나였습니다. 여러분도 오늘 배운 걸로 이거랑 똑같이 생긴 함수를 만들 수 있습니다.

다만 values를 실제로 화면 픽셀에 그려내는 부분, 그러니까 운영체제한테 “이 글자들을 모니터에 띄워줘”라고 전달하는 그 아래 단계는 파이썬 언어 자체보다 훨씬 깊은 곳(운영체제, 하드웨어)의 일입니다. 거기까진 오늘 다 못 팝니다.

그건 진짜 마법입니다.

결론

오늘은 함수를 아주 제대로 배웠습니다.

def로 함수 만드는 법도 배웠고, 매개변수와 인자의 차이도 배웠고, 키워드 인자도 배웠고, return으로 값 돌려받는 법도 배웠고, 기본값 있는 매개변수도 배웠고, 스코프(지역 변수, 전역 변수, global)도 배웠고, 클로저까지 배웠고, 드디어 print()의 정체까지 파헤쳤습니다.

이제 여러분도 마법사입니다. 남이 만든 마법만 쓰던 사람에서, 자기 마법을 만드는 사람이 됐습니다. 승진입니다.

이제 09강으로 넘어가십시오(클래스와 객체를 배울 것입니다).

← 이전 | 목록 | 다음 →