티스토리 뷰

5. Functions

openbookproject의 Beginning Python Programming for Aspiring Web Developers 5장 번역  [Copyright Notice]

사람들은 머릿속에서 한번에 여러 개의 데이터와 정보 조각을 다루는데 익숙하지 않다. 연구 결과에 의하면 대부분의 사람들은 한꺼번에 최대 7개의 일을 다룰 수 있다. 컴퓨터는 반면 수천 개의 일도 동시에 기억하고 다루는데 문제가 없다.

사람이 수천줄에 달하는 복잡한 프로그램을 작성할 수 있게 하기 위해 프로그래밍 언어는 프로그래머가 여러 개의 명령문 연속에 이름을 붙이는 추상화라는 기능을 제공한다. 이 이름은 나중에 그 세부적인 사항을 알 필요없이 그 명령문들을 가르킬 수 있다. 이 장에서 다루는 함수는 파이썬에서 제공하는 가장 중요한 추상화 기능의 하나다.

5.1. 함수 정의와 사용

프로그래밍 문맥에서 함수는 원하는 일을 하는 명령문의 연속에 이름을 붙인 것이다. 이 명령문의 연속은 함수 정의부에서 명시된 것들이다.

파이썬에서 함수를 정의하는 것은 다음과 같은 형식을 가진다.

def 이름( 매개변수리스트 ): 문장들

만들어지는 함수에는 어떤 이름이든 붙일 수 있다. 물론 파이썬 키워드는 제외해야 한다. 매개변수 리스트는 이 함수를 사용하는 쪽에서 제공해야 하는 정보를 나타낸다. 문장들 부분에는 얼마든지 많은 문장이 들어갈 수 있다. 그 문장들은 def 위치에서 한번 들여쓰기 되어야 한다. 

함수 정의부는 for 루프나 if문처럼 여러 개의 문장을 포함할 수 있는 복합 구조다. 함수 정의부는 def로 시작해서 콜론(:)으로 끝나는 헤더로 시작하고 포함되는 문장들 부분은 몸체부(body)라고 한다.

5.2. 고등학교 수학에서 배운 함수

우리는 수학적인 함수를 알고 잇다. 아래 그림과 같이 함수 기계라고 하는 것을 본 적이 있을 것이다.

이 그림의 의미는 함수가 기계처럼 입력 x를 받아 그것을 출력 f(x)로 변환한다는 것이다. 파이썬에서 함수도 그런 방식으로 생각할 수 있다. 수학에서의 함수처럼 입력을 받아 출력으로 변환하여 내보내는 것이 함수다.

다음과 같은 삼차원 함수를 예를 들어 생각해 보자.  (여기서 x^2는 x의 2제곱을 나타낸다)

f(x) = 3x^2 - 2x + 5

다음은 똑같은 함수를 파이썬으로 작성한 것이다.

def f(x): 
	return 3 * x ** 2 - 2 * x + 5

새로운 함수를 정의한다고 그 함수가 실행되지는 않는다. 함수안에 있는 일을 하게 하려면 함수 호출(function call)을 해야 한다. 함수 호출은 실행하고 싶은 함수의 이름을 쓰고 뒤에 함수 정의의 매개변수 리스트에 있는 값들을 써주는 것이다. 이것을 인수(arguments, 호출할 때 주는 값)라고 한다.

다음은 위에서 정의한 함수 f를 호출한 예다.

>>> f(3) 
26 
>>> f(0) 
5 
>>> f(1) 
6 
>>> f(-1) 
10 
>>> f(5) 
70

쉘에서 이러한 함수 호출이 나오기 전에 f 함수의 정의가 먼저 입력되어야 한다.

>>> def f(x): 
	...  
	return 3 * x ** 2 - 2 * x + 5 
	... 
>>>

함수 호출에서 써준 인수가 함수의 매개변수에 자동으로 지정된다고 생각할 수 있다.

즉 위의 f 함수 호출에서는 마치 우리가 x = 3, x = 0, x = 1, x = -1, x = 5 같은 지정문을 호출하기 전에 각각 수행한 것과 같은 효과가 난다. (묵시적으로 파이선이 알아서 지정해 준 것이라고 볼 수 있다.)

5.3. return 문

return 문은 함수의 몸체부에서 함수가 수행을 멈추고 return 뒤의 수식을 계산한 결과를 호출한 문장에게 돌려주게 된다. 호출한 곳이 리턴 된 값으로 바뀐다고 볼 수 있다.

>>> result = f(3) 
>>> result 
6 
>>> result = f(3) + f(-1) 
>>> result 
36

리턴문 뒤에 아무 값도 없는 경우에도 여전히 값이 호출부에 전달된다.

>>> def mystery(): 
    ... 
    return
    ... 
>>> what_is_it = mystery() 
>>> what_is_it 
>>> type(what_is_it) <class 'NoneType'> 
>>> print(what_is_it) 
None

None은 파이썬의 NoneType이라는 타입의 하나밖에 없는 값이다. 나중에 None을 이용해서 모르는 값이나 아직 지정되지 않은 값을 자주 나타내게 된다. 지금으로서는 일단 그것이 뒤에 값을 쓰지 않은 return 문에 의해 돌아가게 되는 값이라고만 알고 넘어가자.

모든 파이썬 함수는 값을 리턴한다. 호출된 함수가 수행하다가 return 문을 한번도 실행하지 않고 몸체부의 끝을 만나면 자동으로 None 값이 리턴된다.

>>> def do_nothing_useful(n, m): 
    ... 
    x = n + m 
    ... 
    y = n - m 
    ... 
>>> do_nothing_useful(5, 3) 
>>> result = do_nothing_useful(5, 3) 
>>> result 
>>> 
>>> print(result) 
None

위의 do_nothing_useful 함수는 아무 값도 리턴하지 않으므로 None이 리턴된다. result 변수에 None 값이 저장되었으나 파이썬 쉘에서 None 값은 아무것도 보여주지 않는다. print를 해야 None 값을 확인할 수 있다.

리턴 문 뒤에 나오는 문장들은 실행되지 않는다. 이런 코드를 도달되지 않는 코드 또는 죽은 코드라고 한다.

>>> def try_to_print_dead_code():
    ...
    print("This will print...")
    ...
    print("...and so will this.")
    ...
    return
    ...
    print("But not this...")
    ...
    print("because it's dead code!")
    ...
>>> try_to_print_dead_code()
This will print
...
...
and so will this.
>>>

5.4. 실행 흐름

함수는 처음 사용되기 전에 정의되어야 한다. 함수의 호출에서 문장들이 어떤 순서로 실행되는지를 알아보자. 이것을 실행흐름(flow of execution)이라고 한다. 실행은 항상 프로그램의 첫번째 문장에서 시작된다. 문장은 한번에 하나씩 위에서부터 아래로 차례로 수행된다. 함수 정의부에서는 아무것도 실행되지 않는다. 정의부를 만나면 단지 함수안에 있는 문장들을 호출될 때 실행할 수 있게 기억할 뿐이다.

함수 호출은 실행 흐름에서 우회로와 같다. 호출을 만나면 그 문장을 실행하기 위해 다음 문장으로 가는 대신 호출된 함수의 첫 줄로 점프한다. 그리고 거기 있는 모든 문장을 수행한 후 또는 return 문을 만나면 호출한 곳으로 돌아온다.

간단할 것 같지만 한 함수가 또 다른 함수를 호출할 수 있다는 점 때문에 일은 복잡해 진다. 함수의 중간에서 다른 함수를 호출하는 문장을 만날 수 있다. 그 다른 함수를 수행하는 중에 또다른 함수의 호출을 만날 수 있다. 그래서 실행 흐름의 문제는 간단치가 않다.

다행히 파이썬은 지금 어디에 있는지 어디서 왔는지 다 기억하고 있어서 함수가 끝나면 호출한 곳으로 찾아서 갈 수 있게 해 준다. 프로그램은 마지막 문장을 실행하고 나면 종료한다.

그러므로 프로그램을 읽을 때는 그냥 위에서 아래로 읽어가는 것이 아니고 실행의 흐름을 따라가면서 읽어야 한다. 다음 프로그램을 보자.

def f1(): 
	print("Moe") 
def f2(): 
	f4() 
	print("Meeny") 
def f3(): 
	f2() 
	print("Miny") 
	f1() 
def f4(): 
	print("Eeny") 
f3()

이 프로그램의 출력은 다음과 같다.

Eeny 
Meeny
Miny 
Moe

[연습문제] 위의 프로그램을 실행 흐름에 따라 가면서 왜 이런 결과가 나왔는지 이해해 보자.

5.5. 캡슐화와 일반화

캡슐화(encapsulation)란 코드의 부분을 함수로 패키징하는 것이다. 그렇게 하므로서 함수가 주는 장점을 얻을 수 있게 된다.

일반화(generalization)란 어떤 특수한 것을 좀더 일반적인 것이 되게 만드는 것이다. 예를 들어 어떤 수의 자리수를 세는 것을 임의의 정수의 자리수를 세는 것으로 일반화할 수 있다.

이 과정이 어떻게 동작하는지 보기 위해 4203이라는 숫자의 자리수를 세는 프로그램을 먼저 작성해 보자.

number = 4203 
count = 0 
while number != 0: 
	count += 1 
	number //= 10 
	print(count)

이 프로그램은 while 루프를 자리수만큼 반복하게 될 것이다. 이 프로그램은 개수세기 프로그램의 많이 쓰이는 계산 패턴을 보여준다. 변수 count는 0으로 초기화되고 루프를 실행할 때마다 1씩 증가한다. 루프가 종료할 때 count는 결과를 포함하고 있다. 루프가 반복한 회수가 즉 숫자의 자리수가 된다. 

캡슐화를 위한 첫번째 단계는 이 논리를 함수로 묶어주는 것이다.

def num_digits(): 
	number = 4203 
	count = 0 
	while number != 0:
		count += 1 
		number //= 10 
	return count 

print(num_digits())

이 프로그램을 실행하면 이전과 같은 결과를 얻지만 여기서는 함수가 호출되었다. 이렇게 함수로 바꾸는 것이 오히려 코드가 전보다 길어지고 좋은 점이 없는 것 같지만 다음 단계에서는 함수이 강점이 무엇인지 보여준다.

def num_digits(number): 
	count = 0 
	while number != 0: 
		count += 1 
		number //= 10 
	return count 
    
print(num_digits(4203))

계산할 값을 매개변수화함으로써 우리는 숫자의 자리수를 세는 로직을 임의의 정수에 적용할 수 있게 되었다. 호출부 print(num_digits(710))는 3을 출력할 것이다. 이것을 print(num_digits(1345109))로 바꾸면 이번엔 7을 출력할 것이다.

이 함수는 버그를 포함하고 있다. 만약 num_digits(0)를 호출하면 0이 돌아오는데 사실 정답은 1이다. 또한 num_digits(-23)과 같이 음수로 호출하면 무한루프에 빠지게 된다. 이러한 문제를 해결하는 것은 연습문제로 남겨둔다. 이렇게 모든 경우에 적용할 수 있게 함수를 개선하는 것을 일반화라고 한다.

5.6. 조합(Composition)

수학 함수와 마찬가지로 파이썬 함수도 조합될 수 있다. 즉 어떤 함수 호출의 결과를 다른 함수의 인수로 넣을 수 있다.

>>> def f(x): 
	... 
	return 2 * x 
	... 
>>> def g(x): 
	... 
	return x + 5 
	... 
>>> def h(x): 
	... 
	return x ** 2 - 3 
>>>f(3) 
6 
>>> g(3) 
8 
>>> h(4) 
13 
>>> f(g(3)) 
16 
>>> g(f(3)) 
11 
>>> h(f(g(0))) 
97 
>>> 

위와 같이 g(3)의 호출 결과를 f()의 매개변수로 보낼 수 있다. 이것은 g(3)을 먼저 계산한 후 그 결과인 8을 f(8)로 호출한 것과 같은 순서로 실행된다.

변수를 인수 자리에 넣을 수도 있다.

>>> # 앞 예제의 함수를 그대로 사용
>>> val = 10 
>>> f(val) 
20 
>>> f(g(val)) 
30 
>>>

여기서 변수 val은 함수가 받는 매개변수 x와는 아무런 상관이 없다. 이것은 f(val)이 호출될 때 x = val이라는 지정문을 수행하는 것과 같다. 호출한 쪽에서 어떤 이름이나 값을 넣든 상관없이 그 값은 함수 내에서는 x로 쓰게 된다.

5.7. 함수도 데이터다

당신이 정의한 함수도 파이썬에서는 데이터로 취급된다. 놀랍게도 'function'이라는 타입이 있다.

>>> def f(): 
	... 
	print("Hello from function f!") 
	... 
>>> type(f) 
<type 'function'> 
>>> f() 
Hello, from function f! 
>>>

함수라는 값은 리스트의 요소가 될 수도 있고 변수에 지정될 수도 있다. 위에서 살펴본 f, g와 h를 이용해서 다음과 같은 코드를 만들 수 있다.

>>> do_stuff = [f, g, h] 
>>> for func in do_stuff: 
	...  
	func(10) 
	... 
20 15 97

이 코드의 결과가 마지막 줄처럼 나오게 되는 이유를 생각해 보자. 리스트 do_stuff에 있는 함수들이 차례로 func에 지정되고 함수를 가진 변수 func는 변수지만 함수호출처럼 10을 인자로 호출할 수 있다. 그래서 f(10), g(10), h(10)의 순서로 호출되어 결과가 20, 15, 97의 순서로 나온 것이다.

5.8. 리스트 매개변수

리스트를 인수로 함수에 보내는 것은 위에서 살펴본 것과 약간 다른 효과를 내게 된다. 매개변수에 그 리스트 인자를 지정한 효과는 동일한데, 그 경우 매개변수에서 리스트의 내용을 변경하면 그것은 호출한 쪽의 리스트도 바뀌게 된다. 다음은 리스트를 받아 각 요소의 값을 두 배로 바꾸는 코드다.

def double_stuff_v1(a_list): 
	index = 0 
	for value in a_list: 
		a_list[index] = 2 * value 
		index += 1

이 함수를 테스트하기 위해 우리는 그 함수를 double.py 파일에 저장하고 파이썬 쉘에서 그 파일을 import하여 실험을 해 보자.

>>> from pure_v_modify import double_stuff_v1 
>>> things = [2, 5, 'Spam', 9.5] 
>>> double_stuff_v1(things) 
>>> things 
[4, 10, 'SpamSpam', 19.0]

[Note] import될 코드는 .py 확장자를 가진 파일로 저장되어야 한다. 그리고 import할 때는 확장자는 쓰지 않는다.

main 함수 쪽의 변수 things와 double_stuff 함수 안에 있는 a_list 매개변수는 실제로는 같은 리스트를 가리키고 있는 두 개의 다른 이름이다. (그림은 함수 이름과 변수 이름이 바뀌었음)

두 개의 다른 변수들이 같은 객체를 가리키고 있으므로 함수에서 a_list로 변경한 값이 호출한 쪽 things에서도 그대로 보인다.

5.9. 순수 함수와 수정 함수

함수는 인수를 여러 개 받아서 그 값을 바꾸는 경우 수정함수(modifier)라고 하고 그러한 변경을 부수효과(side effects)라고 한다.

순수 함수란 부수효과가 없는 함수다. 호출한 프로그램에게 매개변수를 받지만 그것을 수정하지 않고 오로지 반환값으로만 결과를 돌려준다. 다음의 double_stuff_v2 는 순수 함수다.

def double_stuff_v2(a_list): 
	new_list = [] 
	for value in a_list: 
		new_list += [2 * value] 
	return new_list

이 함수는 인수로 넘겨진 리스트를 바꾸지 않는다.

>>> from pure_v_modify import double_stuff_v2 
>>> things = [2, 5, 'Spam', 9.5] 
>>> double_stuff_v2(things) 
[4, 10, 'SpamSpam', 19.0] 
>>> things 
[2, 5, 'Spam', 9.5] 
>>>

만약 순수 함수를 이용해서 things를 바꾸고 싶다면 호출한 쪽에서는 함수의 리턴 값을 받아서 다시 지정해야 한다.

>>> things = double_stuff(things) 
>>> things 
[4, 10, 'SpamSpam', 19.0] 
>>>

(5.10절) 그렇다면 어느 것이 나을까?

수정 함수에서는 순수 함수가 하는 일은 무엇이든 할 수 있다. 사실 일부 프로그래밍 언어는 순수 함수만 허용한다. 순수 함수만 쓰는 프로그램은 수정 함수를 포함한 프로그램에 비해 개발하기 더 쉽고 오류 가능성이 적다고 알려져 있다. 그럼에도 불구하고 수정함수가 더 편한 경우가 많고 일부 문제에서는 순수 함수 기반의 함수형 프로그램이 효율성이 떨어지는 경우도 있다.

일반적으로 우리는 무리가 없다면 순수 함수를 쓰길 권장하고 꼭 필요할 때만 수정 함수를 쓰는 것이 좋다. 이러한 프로그래밍 스타일을 함수형 프로그래밍이라고 한다.

5.11. 다형성과 덕 타이핑 (Polymorphism and duck typing)

같은 함수를 다른 타입으로 호출할 수 있게 하는 것을 다형성이라고 한다. 파이썬에서는 다형성은 쉽다. 파이썬 함수는 타입을 덕 타이핑에 의해 다루기 때문이다. 덕 타이핑이란 넘겨진 매개변수가 함수에서 하는 모든 연산에 대해 문제만 없다면 함수는 아무 문제 없이 호출을 수행한다. 다음의 간단한 예제가 덕 타이핑이 무엇인지 보여준다.

>>> def double(thing): 
	... 
	return 2 * thing 
	... 
>>> double(5) 
10 
>>> double('Spam') 
'SpamSpam' 
>>> double([1, 2]) 
[1, 2, 1, 2] 
>>> double(3.5) 
7.0 
>>> double(('a', 'b')) 
('a', 'b', 'a', 'b') 
>>> double(None) 
Traceback (most recent call last): File "<stdin>", line 1, 
in <module> File "<stdin>", line 2, in double TypeError: 
unsupported operand type(s) for *: 'int' and 'NoneType' 
>>>

*(곱하기) 연산은 정수, 스트링, 리스트, 플로트, 튜플 등에 모두 적용될 수 있다. 그러므로 double 함수를 호출할 때 이 들중 어느 것을 인수로 주어도 문제가 없다. 그러나 None 타입에 대해서는 * 연산이 정의되어 있지 않다. 그러므로 None 값으로 double 함수를 호출하면 오류가 발생한다.

5.12. 이차원 테이블

이차원 테이블이란 행과 열의 교차로 데이터를 읽어올 수 있는 표다. 구구단 표가 좋은 예제다. 1부터 6까지의 곱을 출력하는 예를 보자.

시작으로는 일단 2의 곱을 한줄에 출력하는 for 문을 생각해 볼 수 있다.

for i in range(1, 7): 
	print(2 * i, end=" ") 
print()

여기서 사용된 range 함수는 1부터 시작하는 수열을 가진다. 1에서 6의 모든 값이 i에 차례로 지정되고 나면 루프는 종료한다. 루프를 한번 돌 때마다 2 * i를 세칸 간격으로 출력한다.

여기서 print 함수에 end="   "를 추가 인수로 주어서 줄바꿈 대신 빈칸을 넣게 했다. 루프가 끝난 후에 줄바꿈을 한번 출력한다. 출력은 다음과 같다.

2 4 6 8 10 12

이제 여기서 출발해서 캡슐화와 일반화를 해 보자.

(5.13절) 추가 캡슐화

다음 함수는 위의 루프가 한 일을 담당하게 하는데 여기서 일반화를 추가하여 임의의 n단을 출력할 수 있게 만들었다.

def print_multiples(n): 
	for i in range(1, 7): 
		print(n * i, end=" ") 
	print()

캡슐화를 위해서는 첫번째 함수 정의의 헤더를 넣는 것으로 충분하다. 함수 이름과 매개변수 리스트를 써주어야 한다. 일반화를 위해서는 2를 값으로 주는 대신 매개변수 n으로 대체했다.

이 함수를 2 대신 3으로 호출하면 다음 결과를 얻을 것이다.

3 6 9 12 15 18

4를 인수로 주면 결과는 다음과 같다.

4 8 12 16 20 24

이제 어떻게 구구단을 출력하면 될지 짐작할 것이다. print_multiples 함수를 다른 인수를 주어 반복해서 호출하면 된다. 그러기 위해 새로운 루프를 돌릴 필요가 있다.

for i in range(1, 7): 
	print_multiples(i)

이것은 우리가 앞에서 봤던 루프와 유사한데 단지 안에 코드를 함수 호출로 바꾸었을 뿐이다. 그럼 이제 우리는 구구단 표를 출력해 볼 수 있다.

1 2 3 4 5 6 
2 4 6 8 10 12 
3 6 9 12 15 18 
4 8 12 16 20 24 
5 10 15 20 25 30 
6 12 18 24 30 36

(5.14) 추가 캡슐화

캡슐화를 추가로 해 본다면 우리는 구구단 표를 만드는 코드 전체를 함수로 만들 수 있다.

def print_mult_table(): 
	for i in range(1, 7): 
		print_multiples(i)

이런 과정은 일반적인 개발 단계와 비슷하다. 우리는 코드를 일단 작성해 보고 돌려본 다음 그것이 잘 동작하면 함수로 둘라쌀 부분을 독립시키게 된다.  이런 접근법은 특히 함수가 익숙하지 않은 초보 단계에서는 매우 유용하다. 이런 연습을 통해 언젠가는 바로 함수를 설계할 수 있게 될 것이다.

5.15. Local variables

여기서 왜 같은 변수 i를 print_multiples  print_mult_table 두 개 함수에서 동시에 쓸 수 있는지 궁금할 것이다. print_multiples에서 i를 바꾸면 print_mult_table의 i가 바뀌는 것 아닌가?

물론 아니다. 두 함수가 사용하는 변수 i는 같은 변수가 아니다.

변수는 함수 정의부 안에서 사용된 경우 로컬(지역)이다. 지역변수는 그 함수 이외의 부분에서는 접근할 수없다. 그 얘기는 다른 함수라면 얼마든지 같은 변수 이름을 써도 된다는 것이다.

파이썬은 함수의 모든 문장을 검사해서 변수에 값을 지정하는 것이 나타나면 그 이름의 지역 변수를 새로 만들겠다는 것으로 이해한다.

이 프로그램에 대한 스택 그림은 두 변수 i가 서로 다른 변수임을 보여준다. 각 코드에서 접근한 i는 다른 변수고 서로 영향을 미치지 않는다.

print_mult_table의 값은 1에서 6까지 변한다. 위 그림에서는 그것이 3이다. print_mult_table에서 루프를 돌 때마다 print_multiples를 출력하는데 그 때 i를 인수로 넘긴다. 그 값은 매개변수 n에 지정된다. 

print_multiples 안에서 i의 값은 다시 1에서 6으로 변한다. 위 그림에서는 i가 2인 상태다. 여기서 i 값을 바꾸어도 print_mult_table의 i 값은 3으로 그대로 있다.

다른 함수에서 같은 이름의 지역 변수를 가지는 것은 전혀 문제가 되지 않고 오히려 일반적으로 많이 쓰인다. 특히 i나 j 같은 이름은 루프 변수로 자주 쓰인다. 다른 함수에서 썼다고 이 함수에서 사용하지 못한다면 프로그램에 너무 많은 변수 이름이 생겨서 오히려 가독성을 떨어뜨린다. (코드를 알아보기 어렵다)

5.16. 재귀적인 자료구조

파이썬의 리스트나 튜플 같은 데이터 타입은 매우 다양한 방식으로 구성될 수 있다. 리스트나 튜플은 중첩될 수도 있고 데이터를 구성하는 방법은 무수히 많은 가능성을 가진다. 목적에 따라 사용하기 쉬운 방식으로 데이터를 구성하는 방법을 자료구조라고 한다.

선거를 위해 투표를 집계한다고 해보자. 표는 임의의 순서로 오는데 우리는 전체 집계 뿐 아니라 동별, 구별, 시별, 도별로 따로 집계를 해야 할 수가 있다. 투표 데이터를 어떻게 정리할 것인가를 생각하다가 중첩된 숫자의 리스트를 사용하기로 결정했다. 

중첩 숫자 리스트는 요소가 다음 둘 중의 하나다.

  1. 숫자
  2. 중첩된 숫자 리스트

중첩된 숫자 리스트의 정의 안에 중첩된 숫자 리스트가 다시 나타났다. 이런 재귀적인 정의는 수학과 컴퓨터 과학에서 자주 사용된다. 이것을 이용하면 내부에 자기자신을 포함하는 재귀적인 자료구조를 표현할 수 있다. 재귀적인 정의는 순환적인 것이 아니다. 재귀에서 차례로 한 레벨씩 중첩을 벗겨나가면 어떤 지점에서는 하나의 단순 요소만 가진 상태에 도달할 것이기 때문이다.

중첩된 숫자 리스트의 모든 값을 더하는 것을 생각해 보자. 파이썬의 빌트인 함수 중에 sum이 있는데 이것은 숫자로만 된 리스트나 튜플에 대해서 합을 구해 준다.

>>> sum([1, 2, 8]) 
11 
>>> sum((3, 5, 8.5)) 
16.5 
>>>

그러나 중첩 리스트에 대해서는 sum 함수가 동작하지 않는다.

>>> sum([1, 2, [11, 13], 8])
Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 
unsupported operand type(s) for +: 'int' and 'list' 
>>>

문제는 세 번째 요소 [11, 13]가 리스트여서 나머지 1, 2, 8과 더할 수 없는 값이기 때문이다.

5.17. 재귀

재귀적으로 중첩된 숫자 리스트의 모든 숫자를 더하려면 각 요소를 더하면서 요소가 리스트면 그 리스트를 더한 값을 전체의 합에 더해야 한다. 그 중첩된 리스트 안에 또 리스트가 있을 수도 있다.

현대적 프로그래밍 언어는 보통 재귀라고 하는 기능을 지원한다. 재귀는 함수가 그 정의부 안에서 자기 자신을 호출할 수 있는 것을 말한다. 이 기능 덕에 파이썬 코드에서는 위와 같은 재귀적으로 중첩된 숫자 리스트를 쉽게 더할 수 있다.

def recursive_sum(nested_num_list): 
	the_sum = 0 
	for element in nested_num_list: 
		if type(element) == list: 
			the_sum = the_sum + recursive_sum(element) 
		else: 
			the_sum = the_sum + element 
	return the_sum

함수 recursive_sum의 몸체부는 매개변수로 받은 nested_num_list의 요소를 차례로 반복하면서 element에 넣고 그것이 숫자값이면(else 부분) 그 값을 the_sum에 그냥 더한다. element가 리스트면 recursive_sum이 element를 인자로 다시불려진다.  함수 정의부 안에 있는 이 문장은 자기 자신(포함된 함수)를 호출하는데 이것을 재귀 호출이라고 한다.

재귀는 컴퓨터과학에서 가장 아름답고 멋진 기법 중 하나다. 약간 더 복잡한 문제로 주어진 재귀적 중첩 리스트로부터 가장 큰 수를 찾는 문제를 살펴보자.

def recursive_max(nested_num_list): 
	""" 
		>>> recursive_max([2, 9, [1, 13], 8, 6]) 
		13
		>>> recursive_max([2, [[100, 7], 90], [1, 13], 8, 6]) 
		100 
		>>> recursive_max([2, [[13, 7], 90], [1, 100], 8, 6]) 
		100
		>>> recursive_max([[[13, 7], 90], 2, [1, 100], 8, 6]) 
		100 
	""" 
	largest = nested_num_list[0] 
	while type(largest) == type([]): 
		largest = largest[0] 
	for element in nested_num_list: 
		if type(element) == type([]): 
			max_of_elem = recursive_max(element) 
			if largest < max_of_elem: 
				largest = max_of_elem 
		else: 
			# element is not a list 
			if largest < element: 
				largest = element 
	return largest

함수의 설명부를 """ ... """ 안에 넣어서 이 함수가 어떻게 동작하는지 보여주고 있다.

한가지 까다로운 부분은 최대값을 처음에 어떻게 초기화하는가이다. 리스트의 첫번째 요소를 그냥 largest에 넣을 수 없다. 왜냐하면 그 값이 숫자일지 리스트일지 알 수 없기 때문이다. 이 문제를 해결하기 위해 우리는 while 루프를 돌려서 첫번째 요소가 리스트가 아닐 때까지 반복했다. 이렇게 하면 첫번째 요소가 계속 중첩해서 리스트인 경우를 해결할 수 있다. (예: [[[[[[3, 4], 15], 6], 7], 8], 9, 4]면 largest의 초기값은 3이어야 한다)

위에서 살펴본 두 예제는 재귀적인 호출이 일어나지 않는 경우를 포함한다. 그것을 종료 조건(순환을 멈추는 조건, base case)라고 한다. 종료 조건이 없다면 재귀 함수는 무한 재귀에 빠지게 된다. 그러면 그 프로그램은 무한히 함수 호출만 계속하다가 에러를 만나게 되는데, 파이썬에서는 최대 재귀 깊이가 정해져 있어서 함수 호출의 수가 거기 도달하면 실행 오류가 발생된다.

다음 프로그램을 작성해서 infinite_recursion.py로 저장해 보자.

# 
# infinite_recursion.py 
# 
def recursion_depth(number): 
	print("Recursion depth number {].".format(number)) 
	recursion_depth(number + 1) 

recursion_depth(0)

파이썬 쉘에서 이 파일을 실행하면 많은 출력이 프린트된 후 다음과 같은 오류 메시지를 만나게 될 것이다.

... File "infinite_recursion.py", line 3, in recursion_depth recursion_depth(number + 1)
RuntimeError: maximum recursion depth exceeded

우리는 어떤 경우에도 종료 조건이 없어서 이런 오류가 발생하는 재귀 함수를 작성하지 말아야 한다. 

5.18. 예외 (Exceptions)

런타임 에러가 발생하면 그것은 예외를 생성한다. 프로그램은 그 지점에서 실행을 멈추고 파이썬은 추적결과를 출력해 주는데 출력은 보통 어떤 예외가 일어났다는 것을 보여준다. 예를 들어 0으로 나누는 예외가 발생했다고 해 보자.

>>> print 55/0 
Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: 
integer division or modulo by zero 
>>>

존재하지 않는 리스트 요소를 접근하는 경우는 다음과 같은 오류가 발생한다.

>>> a = [] 
>>> print a[5] Traceback (most recent call last): File "<stdin>", line 1, 
in <module> IndexError: list index out of range 
>>>

튜플의 요소를 바꾸려고 하면?

>>> tup = ('a', 'b', 'd', 'd') 
>>> tup[2] = 'c' 
Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 
'tuple' object does not support item assignment 
>>>

각 경우에 마지막 두 줄이 에러 메시지다. 오류의 종류와 콜론 뒤에 어떤 오류인지를 보여준다.

가끔은 예외가 발생하는 연산을 수행해야 할 때도 있다. 그러나 프로그램이 종료하는 것은 원치 않는다면? 그 경우 우리는 try와 except 문장을 이용해 예외를 처리할 수 있다.

예를 들면 우리는 파일을 열 때 사용자에게 파일의 이름을 입력하게 할 수 있다. 그런데 만약 그 파일이 없다면 우리는 프로그램을 종료하기 보다는 예외를 처리해서 수행을 계속하고 싶다.

filename = input('Enter a file name: ') 
try: 
	f = open (filename, "r") 
except: 
	print('There is no file named', filename)

try 문장은 그 안에 포함된 블록의 문장들을 실행한다. 예외가 발생하지 않으면 excep 문장은 무시된다. 만약 예외가 발생하면 except 부분에 있는 문장들을 실행한 후 그 다음으로 실행을 계속한다.

이러한 예외처리를 함수 안에 포함시킬 수 있다. 다음 함수 exists는 파일이름을 받아 그 파일이 존재하면 true를 리턴하고 없으면 false를 리턴한다.

def exists(filename): 
	try: 
		f = open(filename) 
		f.close() 
		return True 
	except: 
		return False

여러 개의 except 블럭을 이용해 여러 종류의 예외를 처리할 수도 있다. (예외에 대해서는 별도의 장에서 다룬다).

프로그램에서 오류 조건을 감지 하면 예외를 발생시킬 수도 있다 . 다음은 사용자로부터 나이를 입력을 받고 숫자가 음수면 예외를 일으키는 예제다.

# 
# learn_exceptions.py 
# 
def get_age(): 
	age = int(input('Please enter your age: '))
	if age < 0: 
		raise ValueError('{} is not a valid age'.format(age))
	return age

5.20. 수학의 재귀 함수

몇 가지 잘 알려진 수학 함수는 재귀적으로 정의된다. 팩토리얼이 그런 예인데, !를 이용해 다음과 같이 정의된다.

0! = 1 
n! = n(n-1)

이 함수를 파이썬으로 쉽게작성할 수 있다.

def factorial(n): 
	if n == 0: 
		return 1 
	else: 
		return n * factorial(n-1)

또다른 잘 알려진 예로 피보나치 수열이 있다. 

fibonacci(0) = 1 
fibonacci(1) = 1 
fibonacci(n) = fibonacci(n-1) + fibonacci(n-2)

이것을 파이썬으로 옮기면 다음과 같다.

def fibonacci (n): 
	if n == 0 or n == 1: 
		return 1 
	else: 
		return fibonacci(n-1) + fibonacci(n-2)

factorial(1000)을 호출하면 최대 재귀 깊이를 넘어서게 된다. 또한 fibonacci(35)를 실행해 보면 그것이 엄청나게 오래 걸리는 것을 알 수 있다. 이런 재귀적인 함수는 사실 반복적인 형태로 작성하는 것이 더 바람직하다. 실제로 위 두 개 함수는 쉽게 반복 구조로 바꿀 수 있는 예들이다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함