12. 함수#

프로그램은 명령문의 합성으로 구현된다. 많은 프로그램은 몇 백 또는 몇 천 줄의 명령문으로 구성되며, 경우에 따라 몇 십만, 몇 백만. 심지어 천만 줄 이상의 명령문으로 구성된 프로그램도 있다.

몇 백 줄 이상의 명령문으로 구성된 프로그램을 구현하다 보면 특정 기능을 수행하는 명령문을 반복해서 사용하곤 한다. 또한 프로그램의 소스코드를 여러 개의 소스코드 파일로 나누어 작성하고 관리할 필요성도 대두된다. 이때 함수와 모듈을 활용한다.

여기서는 먼저 함수에 대해 이야기하고 이어지는 장에서 모듈을 소개한다.

12.1. 함수 호출과 표현식#

함수function는 간단히 말해 명령문에 붙인 이름이다. 예를 들어 아래 코드를 실행하면 "함수란 특정 명령문을 대신한다." 라는 문장이 화면에 출력된다.

print('함수란', '특정 명령문을 대신한다.')
함수란 특정 명령문을 대신한다.

print() 함수가 대신하는 명령문, 즉 print라는 이름이 붙은 명령문이 작동하여 인자로 지정된 아래 두 문자열을 공백을 사이에 두고 연속적으로 화면에 출력한다.

  • 첫째 인자: 함수란

  • 둘째 인자: 특정 명령문을 대신한다.

print라는 이름이 붙은 명령문이 정확히 어떻게 구현되었는지 사용자는 쉽게 알지 못하며, 또 굳이 알 필요도 없다. 화면에 문자열 등의 값을 출력하고 싶을 때 print() 함수를 사용한다는 사실만 알면 된다.

함수는 아래 형식의 함수 호출function call을 통해 활용된다.

함수이름(인자1, 인자2, ..., 인자n)

함수 호출에 필요한 인자의 수는 함수의 정의에 의존한다. print() 함수는 0 개 이상의 인자를 사용할 수 있다. int(), float() 등의 형변환 함수는 0개 또는 하나의 인자를 사용한다.

int("3")
3
float("3.14")
3.14

인자를 사용하지 경우에도 함수 호출은 열고닫기 소괄호 ()를 반드시 사용해야 한다. 예를 들어 int() 함수와 float() 함수를 인자 없이 호출하면 각각 0과 0.0을 계산한다.

int()
0
float()
0.0

반면에 print() 함수를 인자 없이 호출하면 아무 것도 출력되지 않는다.

print()

함수 호출 표현식과 값

함수 호출 표현식은 함수 호출 형식으로 값을 값을 표현하는 표현식(4.1절)이다. 예를 들어 int(3.14)는 정수 3을, float('3.14')는 부동소수점 3.14를, type(3.14)는 부동소수점의 자료형인 float를, type('3.14')는 문자열의 자료형인 str를 표현한다.

파이썬과 값

파이썬에서는 코드에서 다루는 모든 게 값이다. 따라서 부동소수점 3.14의 자료형인 float, 문자열 '3.14'의 자료형인 str 등도 값이다.

사실 함수 호출 표현식이 표현하는 값은 함수를 호출하여 실행했을 때 계산되는 값이다.

int(3.14)
3
float('3.14')
3.14
type(3.14)
float
type('3.14')
str

함수 호출 표현식은 함수에 의해 계산되는 값을 대신하여 필요에 따라 활용될 수 있다. 예를 들어 아래 코드는 int() 함수가 계산한 결과를 제곱한다.

y = int(3.14)
y**2
9

반면에 아래 코드는 type() 함수에 의해 계산된 자료형에 따라 다른 명령문을 실행하도록 하였다. 변수 x가 가리키는 값이 문자열이기에 type(x) == str이 참이되어 print(x * 2)가 실행된다.

x = '3.14 '

if type(x) == str:
    print(x * 2)
else:
    print(x / 2 )
3.14 3.14 

12.2. 함수 선언#

함수는 아래 형식으로 선언된다.

def 함수이름(매개변수1, 매개변수2, ...):
    명령문

def 키워드로 시작하는 줄은 함수의 기본 정보를 담은 헤더header이고 나머지는 함수의 본문body이다. 함수의 본문은 함수가 호출되었을 때 실행되어야 하는 명령문을 포함한다.

함수를 정의할 때 다음 세 가지 사항에 주의한다.

  • 함수 이름과 매개변수 이름: 변수 이름 짓기와 동일한 조건을 따른다.

  • 매개변수의 종류와 수: 함수를 정의하는 사용자가 정한다.

  • 함수 헤더의 끝에 위치한 콜론 ::

    • 함수 헤더의 끝을 나타낸다.

    • 함수 본문 명령문은 들여쓴다. 들여쓰기는 보통 Tab 키를 이용한다.

12.2.1. 매개변수와 인자#

매개변수parameter함수의 본문에서만 사용되는 변수이며 함수를 호출할 때 사용하는 인자argument를 함수 본문의 명령문에 전달한다. 예를 들어 아래 코드는 정수 또는 부동소수점 두 개가 주어지면 두 수의 합을 계산하는 함수 myAdd를 정의한다. 두 개의 수를 인자로 받아 합을 계산해야 하기에 두 개의 매개변수를 사용한다.

  • left 매개변수: 덧셈 연산의 첫째 인자

  • right 매개변수: 덧셈의 연산의 둘째 인자

def myAdd(left, right):
    sum_ = left + right
    return sum_

이제 myAdd() 함수를 두 개의 인자와 함께 호출하면 두 인자의 합을 계산한다. 예를 들어 아래 코드는 -2와 5의 합을 계산한다.

myAdd(-2, 5)
3

12.2.2. 함수 호출과 반환값#

myAdd(-2, 5) 가 호출되었을 때 파이썬 실행기 내부에서 실제로 실행되는 명령문은 두 단계로 구성된다.

첫째, 매개변수 각각에 대해 주어진 인자의 순서대로 변수 할당 명령문이 실행된다.

left = -2
right = 5

leftright 두 매개변수가 함수의 본문에서 사용될 때 이렇게 할당된 값을 이용한다. 이런 의미에서 매개변수가 인자를 함수의 본문에 전달한다고 말한다

둘째, myAdd() 함수의 본문에 사용된 명령문이 차례대로 실행된다.

sum_ = left + right
return sum_

sum_ = left + right 명령문이 실행되어 sum_ 변수에 정수 3이 할당된다. 끝으로 return sum_ 명령문이 실행된 후에 바로 함수 호출의 실행 과정이 종료된다.

함수 반환값

함수는 호출되면 앞서 설명한 대로 매개변수에 지정된 인자가 할당된 상태에서 본문 명령문이 차례대로 실행된다. 그러다가 아래 모양의 반환 명령문이 실행되는 순간 바로 함수의 실행이 종료된다.

return 표현식

그런데 함수의 실행이 완전히 종료되기 전에 return 명령문에 지정된 표현식이 표현하는 값이 반환값으로 지정된다. 예를 들어 myAdd(-2, 5)가 호출되면 sum_ 변수에 할당된 3이 함수의 종료와 함께 반환값으로 지정된다.

함수 호출 표현식은 이때 지정된 반환값을 대신하여 언제든지 재활용될 수 있다. 예를 들어 아래 코드는 3을 대신하여 myAdd(-2, 5)가 변수 할당에 활용된다.

sum_of_minus2_5 = myAdd(-2, 5)

반면에 아래 코드는 반환값을 바로 곱셈 연산에 활용한다.

myAdd(-2, 5) * 3
9

이처럼 myAdd(-2, 5)과 같은 함수 호출 표현식은 해당 함수 호출의 반환값과 동등하게 사용된다.

return ... 명령문의 위치

함수의 본문이 실행되는 도중에 return ... 명령문이 실행되는 순간 반환값이 지정되면서 동시에 함수의 실행이 중지된다. 따라서 어떠한 경우에도 두 개의 return ... 명령문이 실행되지 않기에 함수의 반환값은 하나다.

하지만 그렇다고 해서 함수 본문에 return ... 명령문이 한 번만 사용된다는 의미는 아니다. 예를 들어, 아래 myAddProd() 함수는 두 인자의 크기를 비교한 결과에 따라 덧셈 또는 곱셈을 실행한 결과를 반환값으로 지정한다.

def myAddProd(left, right):
    if left < right:
        sum_ = left + right
        return sum_
    else:
        prod_ = left * right
        return prod_

두 인자의 크기에 따라 덧셈 또는 곱셈의 결과가 반환되지만 하지만 어떤 경우에도 return sum_return prod_ 가 동시에 실행되지는 않는다. 이런 의미에서 함수의 반환값은 항상 하나다.

myAddProd(-2, 5)
3
myAddProd(5, -2)
-10

심지어 아래 useless() 함수처럼 return ... 명령문을 연속해서 작성하더라도 마찬가지다. 먼저 실행된 return sum_ 명령문에 의해 sum_에 할당된 값이 반환되면 바로 함수의 실행을 멈춘다. 즉, return prod_ 는 절대로 실행되지 않는다.

def useless(left, right):
    sum_ = left + right
    prod_ = left * right
    return sum_
    return prod_          # 절대 실행되지 않음
useless(-2, 5)
3

12.2.3. None 반환값#

아래 코드에 정의된 double_int() 함수의 정의는 return ... 명령문을 사용하지 않는다. 즉, 반환값을 지정하지 않는다.

def double_int(num_param):
    double_param = num_param * 2

return ... 명령문이 없는 함수를 호출하면 반환값이 지정되지 않는다. 예를 들어 double_int(5)를 실행해도 아무런 값이 표시되지 않는다.

double_int(5)

아래 코드처럼 double_int(5)에 의해 계산된 값을 화면에 출력하려 하면 None 이라는 이상한 값이 출력된다.

print(double_int(5))
None

앞서 설명한 대로 double_int(5)가 호출되면 아래 코드가 실행된다.

num_param = 5
double_param = num_param * 2

따라서 double_param 변수에 10이 할당되는데 이후에 실행할 아무런 명령문도 없기에 반환값이 지정되지 않은 채로 함수의 실행이 종료된다. 파이썬 실행기는 이런 경우에 반환값을 None으로 강제 지정한다.

따라서 double_int() 함수는 실제로는 아래 코드처럼 정의된 것으로 간주된다.

def double_int(num_param):
    double_param = num_param * 2
    return None                  # None 반한값 지정

None 값

None은 ‘아무런 의미가 없음’을 의미하는 값이다. 따라서 0, 비어있는 문자열, 비어 있는 리스트처럼 거짓으로 취급된다.

bool(None)
False

반면에 None도 하나의 값이라서 변수 할당에 사용되어 저장될 수 있다.

x = double_int(5)
print(x)
None

하지만 예를 들어 연산 등에 활용하면 허용되지 않는 인자라는 이유로 TypeError 오류가 발생한다.

x + 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[28], line 1
----> 1 x + 1

TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

None 대 0

숫자 0은 ‘하나도 없다’, ‘비어 있다’ 등을 표현하는 값인 반면에 None은 아예 어떤 의미도 없음을 의미하는 값이다.

print() 함수의 반환값

반환값이 None인 대표적인 함수가 print() 함수이다. 그런데 print() 함수를 아래와 같이 활용하면 마치 인자를 그대로 반환하는 것처럼 보인다.

print(3.14)
3.14

하지만 화면에 출력되는 문자열은 반환값이 아니다. 다만 print() 함수의 본문에 포함된 명령문 중에서 지정된 값을 화면에 출력하는 명령문이 실행된 결과에 불과하다. 앞서 설명한 대로 print(3.14)의 반환값은 None임을 아래 코드가 확인해준다.

먼저 print(3.14) 가 가리키는 값을 변수 x에 할당하자. 아래 코드를 실행했을 때 보여지는 3.14는 반환값이 아니라 지정된 인자가 화면에 출력된 결과이다.

x = print(3.14)
3.14

x에 할당된 값을 확인하려 하면 아무 것도 보여지지 않는다.

x

x에 할당된 None을 확인하려면 print() 함수를 이용해야 한다.

print(x)
None

12.2.4. 람다 함수#

특별한 목적으로 한 번만 사용되는 함수가 필요할 때, 그리고 함수의 본문이 매우 간단할 때 함수의 이름을 지정하지 않으면서 아래 형식으로 함수를 정의할 수 있다.

lambda 매개변수1, ..., 매개변수n : 표현식

lambda(람다)는 그리스어 알파벳 λ(lambda)를 가리키며, 람다를 이용한다는 이유로 람다 함수라 부른다.

예를 들어 두 인자의 곱을 반환하는 람다 함수는 아래와 같이 정의된다.

  • ab: 매개변수 2개

  • 콜론 기호 : 뒤의 표현식 a * b: 람다 함수의 반환값

lambda a, b : a * b
<function __main__.<lambda>(a, b)>

람다 함수는 자신을 가리키는 이름이 없기에 람다 함수를 호출하려면 항상 람다 함수 정의 자체를 사용해야 해서 조금 불편하다. 아래 코드는 2와 5의 곱을 계산하는 람다 함수 호출식이다.

(lambda a, b : a * b)(2, 5)
10

이렇듯 람다 함수는 함수 자체를 활용하기에는 많이 불편하지만 람다 함수 고유의 기능이 따로 있기에 여기서 간단하게 소개해 둔다. 예를 들어 17장에서 람다 함수가 사용된 코드를 살펴볼 것이다.

12.3. 위치 인자와 키워드 인자#

함수의 인자는 위치 인자키워드 인자 두 종류로 구분된다.

  • 위치 인자positional argument: 반드시 순서에 맞게 지정해야 하는 인자

  • 키워드 인자keyword argument: 필요에 따라 추가로 지정할 수 있는 인자

함수를 정의할 때 한 종류의 인자만 사용하도록 해도 되고 두 종류의 인자를 섞어서 사용하도록 해도 된다. 단, 두 종류의 인자를 모두 사용하도록 하려면 위치 인자를 먼저, 키워드 인자를 나중에 오도록 해야 한다. print() 함수를 이용하여 위치 인자와 키워드 인자의 활용법을 소개한다.

12.3.1. print() 함수의 인자#

여러 개의 값을 화면에 출력하려면 print() 함수에 여러 개의 인자를 지정하여 호출한다. 그러면 인자들이 공백으로 구분되어 함께 한 줄에 출력된다. 예를 들어 아래 코드는 안녕, 파이썬, ! 세 개의 문자열을 공백으로 구분하면서 한 줄에 출력한다.

print('파이썬', '안녕', '!')
파이썬 안녕 !

출력 대상으로 지정된 세 개의 인자가 공백으로 구분되어 한 줄에 출력되는 이유는 sep이라는 키워드 인자를 받는 매개변수 때문이다. 위 코드를 실행하면 파이썬 실행기는 실제로는 아래 코드를 실행한다.

print('파이썬', '안녕', '!', sep=' ')
파이썬 안녕 !

변수 할당 명령문 형식처럼 생긴 sep=' '은 화면에 출력해야할 인자들을 공백으로 구분하여 한 줄에 출력하도록 하는 옵션 기능을 지정한다.

키워드 인자 변경

키워드 인자에 할당된 값은 임의로 바꿀 수 있다. 예를 들어, 출력 대상으로 지정된 각각의 인자를 하이픈 기호-로 구분하려면 즉, 인자와 인자를 하이픈으로 연결하려면 아래 코드에서처럼 sep 매개변수에 문자열 -을 키워드 인자로 지정하면 된다.

print('파이썬', '안녕', '!', sep='-')
파이썬-안녕-!

위 코드에서 print() 함수를 호출할 때 사용된 위치 인자와 키워드 인자는 다음과 같다.

  • 위치 인자: 화면 출력에 사용되는 세 개의 인자, '파이썬', '안녕', '!'

  • 키워드 인자: sep 매개변수에 할당된 문자열 '-'

print() 함수의 키워드 인자

print() 함수의 위치 인자는 0개 이상 원하는 만큼 사용할 수 있다. 반면에 키워드 인자는 sep 이외에 end, file, flush 매개변수에 할당될 수 있다. 실제로 print() 함수의 헤더는 다음과 같다.

print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

매개변수 각각의 역할은 다음과 같다. 키워드 인자들은 특별한 기능을 수행하며, 그 기능을 그대로 사용하는 경우 함수 호출 과정에서 굳이 언급할 필요가 없다.

예를 들어, end 매개변수의 기본 키워드 인자가 줄바꿈이기에 print() 함수를 호출한 다음엔 항상 줄바꿈이 실행된다. 만약에 빈 문자열 ''end 매개변수에 할당하면 줄바꿈을 하지 않는다. 아래 두 코드를 비교하면 end 매개변수의 역할을 확인할 수 있다.

  • 기본 키워드 인자를 그대로 사용한 경우: 첫째 print() 함수 호출에 의해 '파이썬 안녕 !'이 출력된 후에 줄바꿈이 이뤄지고 그 다음에 둘째 print() 함수 호출에 의해 '===' 문자열이 출력됨.

print('파이썬', '안녕', '!')
print('===')
파이썬 안녕 !
===
  • end 매개변수의 인자를 빈 문자열로 지정한 경우: 첫째 print() 함수 호출에 의해 '파이썬 안녕 !'이 출력된 후에 줄바꿈을 하지 않은 채로 둘째 print() 함수 호출에 의해 '===' 문자열이 출력됨.

print('파이썬', '안녕', '!', end='')
print('===')
파이썬 안녕 !===

print() 함수의 fileflush 두 매개변수는 특별한 경우에 활용되며 여기서는 다루지 않는다. 두 매개변수의 간단한 예제는 print() 함수의 키워드 인자 기능에서 확인할 수 있다.

12.3.2. 키워드 인자 사용 함수 정의#

아래 코드는 myAdd10() 함수를 키워드 인자를 이용하여 정의하는 방식을 보여준다. 사용된 두 매개변수의 역할은 다음과 같다.

  • left: 덧셈의 왼쪽 인자로 사용될 값을 할당받을 매개변수

  • right: 덧셈의 오른쪽 인자로 사용될 값을 할당받을 매개변수. 10이 기본 키워드 인자로 지정됨.

def myAdd10(left, right=10):
    return left + right

아래 코드에서처럼 myAdd10() 함수를 호출할 때 둘째 인자가 생략되면 자동으로 10이 대신 사용되어 인자가 주어지면 10을 더한 값이 반환되는 함수처럼 사용된다.

myAdd10(5)
15

실제로는 아래 코드와 동일하게 작동한다.

myAdd10(5, 10)
15

10이 아닌 다른 값을 right 매개변수의 키워드 인자로 지정하려면 아래 코드와 같이 함수 호출을 실행한다.

myAdd10(5, right=20)
25

12.4. 지역 변수와 전역 변수#

12.4.1. 매개 변수와 지역 변수#

아래 square_return() 함수는 하나의 인자와 함께 호출되면 인자의 제곱을 반환한다.

seventeen = 17

def square_return(number):
    square = number ** 2
    return square

square_return() 함수의 매개변수 number와 함수 본문에서 정의된 squre 두 변수는 square_return() 함수와 독립적으로 사용될 수 없다. 이는 함수가 호출되어 실행이 완료된 이후에도 마찬가지다. 이유는 함수가 실행되는 과정에서 선언된 변수들은 모두 함수의 실행이 종료되는 순간 컴퓨터 메모리에서 삭제되어 더 이상 사용될 수 없기 때문이다.

예를 들어 아래 함수 호출을 실행하자.

square_return(seventeen)
289

numbersquare 변수를 확인하려 하면 NameError 오류가 발생한다.

number
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[46], line 1
----> 1 number

NameError: name 'number' is not defined
square
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[47], line 1
----> 1 square

NameError: name 'square' is not defined

즉, numbersquare 두 변수가 존재하지 않는다. 반면에 seventeen 변수는 언제나 사용할 수 있다.

print("seventeen 변수:", seventeen)
seventeen 변수: 17

numbersquare 변수처럼 함수를 선언할 때 사용되는 매개변수와 함수 본문에서 선언되는 변수는 함수가 실행되는 동안에만 의미를 갖는다는 의미에서 지역 변수local variable라 한다. 반면에 seventeen 변수처럼 함수 밖에서도 의미를 갖는 변수는 전역 변수global variable라 부른다.

함수 호출 과정에서 계산된 값들 중에 일부를 함수 호출이 끝난 후에도 활용하려면 반환값으로 지정해야 한다. 예를 들어 17의 제곱을 계산해서 활용하고자 한다면 다음과 같이 반환값을 변수에 할당해서 활용한다.

square17 = square_return(seventeen)
print("17의 제곱:", square17)
17의 제곱: 289

지역 변수와 전역 변수의 이름은 구분해서 작성하는 게 좋다. 그렇지 않으면 아래 코드가 보여주듯이 혼란스러워질 수 있다. 아래 코드에서 square 가 전역 변수와 지역 변수로 사용된다. 하지만 두 변수는 서로 상관이 없다.

seventeen = 17
square = 2

def square_return(number):
    square = number ** 2
    return square

아래 코드에서 마지막에 호출되는 print(squre) 는 전역 변수 square에 할당된 2를 출력한다. 이유는 square_return() 함수의 본문에 사용된 지역 변수 square는 함수가 호출되어 실행되는 과정에서만 존재하고 함수의 실행이 종료되는 순간 컴퓨터 메모리에서 삭제되기 때문이다.

print(square)
2

반면에 square_return() 함수의 본문에서 사용되는 squre 변수는 지역 변수를 가리킨다. 예를 들어 아래 코드에서 return square 명령문의 square는 인자 10의 제곱인 100을 가리키며, 따라서 100이 함수의 반환값으로 지정되었다.

print('10의 제곱:', square_return(10))
10의 제곱: 100

12.4.2. global 키워드#

전역 변수는 함수 내에서의 활용이 제한된다. 아래 코드는 전역 변수를 함수 내에서 활용할 수 있음을 보여준다.

# 전역 변수
a = 15

def add_a(b):
    c = a + b  # 전역 변수 a 활용
    return c

print(add_a(10))
25

하지만 함수 내에서 전역 변수 재할당을 시도하면 오류가 발생한다.

# 전역 변수
a = 15

def add_a(b):
    c = a + b  # 지역 변수 a 활용 시도.
    a = 50     # 지역 변수 a 선언. 너무 늦게 선언됨.
    return c

print(add_a(10))
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Cell In[54], line 9
      6     a = 50     # 지역 변수 a 선언. 너무 늦게 선언됨.
      7     return c
----> 9 print(add_a(10))

Cell In[54], line 5, in add_a(b)
      4 def add_a(b):
----> 5     c = a + b  # 지역 변수 a 활용 시도.
      6     a = 50     # 지역 변수 a 선언. 너무 늦게 선언됨.
      7     return c

UnboundLocalError: cannot access local variable 'a' where it is not associated with a value

이유는 함수 본문에서 변수 할당 명령문이 포함되면 파이썬 실행기가 해당 변수를 지역 변수로 처리하기 때문인데, 위 코드의 6번 줄에 있는 변수 할당문으로 인해 변수 a가 지역 변수가 된다. 그런데 5번 줄에서 아직 선언되지 않은 지역 변수 a를 사용하려 하기에 UnboundLocalError 라는 오류가 발생한다.

반면에 아래 코드는 오류 없이 실행된다. 이유는 지역 변수 a가 먼저 선언된 후에 활용되기 때문이다.

# 전역 변수
a = 15

def add_a(b):
    a = 50    # 지역 변수 a 선언
    c = a + b # 지역 변수 a 활용
    return c

print(add_a(10))
60

전역 변수를 함수 내에서 제한 없이 활용하려면 함수 본문에서 global 키워드를 이용하여 해당 변수를 지정해야 한다. 예를 들어 아래 코드는 전역 변수 a를 함수 내부에 활용뿐만 아니라 재할당까지 할 수 있음을 보여준다.

# 전역 변수
a = 15

print('전역 변수 a가 원래 가리키는 값:', a)

def add_a(b):
    global a
    c = a + b  # 전역 변수 a 활용
    a = 50     # 전역 변수 a 재할당
    return c

print("함수 호출 결과:", add_a(10))
print('전역 변수 a가 가리키는 다른 값:', a)
전역 변수 a가 원래 가리키는 값: 15
함수 호출 결과: 25
전역 변수 a가 가리키는 다른 값: 50

12.5. 순수 대 비순수#

아래 add_impure() 함수는 전역 변수 a의 값을 함수가 실행할 때마다 업데이트 한다. 반면에 fun_a() 함수는 전역 변수 a를 활용만 한다.

a = 15

def add_impure(b):
    global a 
    c = a + b
    a = c     # 전역 변수 a 업데이트
    return c

def fun_a(x):
    return x + a

add_impure() 함수는 호출될 때마다 전역 변수 a의 업데이트 한다. 따라서 동일 인자에 대해서도 다른 값을 반환한다.

add_impure(10)
25
add_impure(10)
35

fun_a() 함수는 전역 변수 a를 활용하기에 add_impure() 함수에 영향을 간점적으로 받는다. 예를 들어 그냥 두 번 호출하면 동일 인자에 대해 동일한 값을 반환한다.

fun_a(1)
36
fun_a(1)
36

그런데 add_impure() 함수가 호출되면 fun_a() 함수의 반환값이 동일 인자에 대해서 달라진다.

add_impure(10)
fun_a(1)
46

반환값을 생성하는 것 이외에 전역 변수등을 변경하여 메모리의 상태를 변화시키는 기능을 함수의 부작용side effect이라 하며, 부작용을 갖는 함수를 비순수 함수impure function라 부른다. 반면에 반환값을 계산할 때 함수의 매개 변수와 함수 본문에서 선언된 지역 변수만을 활용하여 함수 외부로부터 영향을 받거나 영향을 주는 부작용이 없는 함수는 순수 함수pure function라 부른다.

프로그래밍에 비순수 함수가 반드시 필요하지는 않지만 편리성과 효율성 때문에 활용되곤 한다. 하지만 앞서 보았듯이 비순수 함수를 사용하다 보면 전역 변수의 값 등 메모리의 상태가 달라져서 fun_a()와 같은 함수에 의도치 않은 영향을 줄 수 있는 가능성이 매우 높다.

12.6. 예제#

예제 1

아래 코드의 실행 결과를 설명하라.

print("A")

def func() :
    print("B")

print("C")

func()
A
C
B

파이썬 코드는 윗줄의 명령문부터 차례대로 실행되기에 아래 순서대로 명령문이 실행된다.

  • print("A") 실행: 'A' 출력

  • func() 함수 정의 실행

  • print("C") 실행: "C" 출력

  • func() 함수 호출: print("B") 실행. 따라서 "B" 출력

예제 2

아래 코드를 실행할 때 발생하는 오류를 설명하라.

func_hi()

def func_hi() :
  print("Hi")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[64], line 1
----> 1 func_hi()
      3 def func_hi() :
      4   print("Hi")

NameError: name 'func_hi' is not defined

파이썬 코드는 윗줄의 명령문부터 차례대로 실행된다. func_hi() 함수가 정의되기 이전에 호출되면 해당 함수가 아직 정의되지 않았다는 의미의 NameError 오류가 발생한다. 이는 함수 호출 아랫쪽에 func_hi() 함수가 정의되어 있다 하더라도 마찬가지다.

예제 3

아래 코드를 실행했을 때 square 변수가 가리키는 값이 3이 아닌 이유를 설명하라.

number = 5
square = 3

def square_return(number):
    square = number ** 2
    return square + 1

square = square_return(number)
print(square)
26

위 코드 2번 줄에서 square 변수에 3이 할당되었는데, 8번 줄에서 다른 값으로 재할당되었다. 재할당된 값은 square_return(5)에 의해 반환된 값, 즉 5의 제곱에 1을 더한 26이다.

square_return(5)의 반환값을 계산하는 과정에서 사용된 지역 변수 square는 함수의 반환값을 계산하는 데에 사용되었지만 함수의 실행이 종료되면 존재의 의미가 사라진다. 따라서 이후에 재할당된 전역 변수 square와는 무관하다.

예제 4

(1) 아래 코드의 실행 결과를 설명하라.

def counter_func1() :
    i = 1
    while i < 100 :
        print("*" * i)
        return i
        i += 1

count = counter_func1()
print("while 반복문의 본문 반복 횟수:", count, "번")
*
while 반복문의 본문 반복 횟수: 1 번

위 코드의 실행 순서는 다음과 같다.

  • 매개 변수를 사용하지 않는 counter_func1() 함수가 정의된다.

  • count = counter_func1() 변수 할당 명령문이 실행된다. 따라서 count 변수에 할당할 값을 정하기 위해 counter_func1() 함수가 인자 없이 호출된다.

  • counter_func1() 함수 호출

    • 변수 i에 1이 할당되고 while 반복문이 실행된다.

    • i < 100 이 참이기에 먼저 "*" * 1, 즉 "*"가 출력된다.

    • return i 명령문에 의해 당시 i가 가리키는 값인 1을 반환한다. 함수는 실행 도중에 return 명령문에 의해 반환값이 지정되는 순간 바로 지정된 반환값을 반환하면서 실행을 멈춘다. 따라서 변수 업데이트 명령문인 i += 1은 실행되지 않은 채 while 반복문 또한 종료된다.

  • count 변수에 할당된 값은 함수 호출 과정에서 생성된 지역 변수 i가 가리키는 값이며 이 값이 while 반복문의 반복 횟수와도 일치한다.

  • 마지막 줄의 print() 명령문이 실행되어 while 반복문의 본문이 1번만 실행되었음을 확인해준다.

(2) 아래 코드의 실행 결과를 설명하라.

def counter_func2(num) :
    i = 1
    while i < num:
        print(i)
        if i == 3 :
            return i
        print("*" * i)
        i += 1
    print("=" * i)
    return i

return_value = counter_func2(100)
print("반환값", return_value)
1
*
2
**
3
반환값 3

  • return_value 변수에 값을 할당하기 위해 먼저 counter_func2(100)를 호출한다.

  • counter_func2(100) 이 호출되면 i가 가리키는 값이 1에서 3까지 변하는 동안 while 반복문이 실행된다.

  • 이유는 반복문의 본문에 있는 if 조건문에 있는 논리식이 i가 3일 가리킬 때 참이고 그러면 return ... 명령문이 실행되어 함수의 실행이 종료되기 때문이다.

  • 따라서 i가 1일 때 정수 1과 별 하나, 2일 때 정수 2와 별 두개, 3일 때는 정수 3만 출력하고 곧바로 반환값이 3으로 지정된다.

예제 5

문자열의 islower() 메서드는 지정된 문자열이 모두 대문자 알파벳을 포함하지 않는지 여부를 판단한다. 즉, 소문자와 기타 기호만 포함할 때 True를 반환한다.

'abc'.islower()
True
'Abc'.islower()
False
'abc !'.islower()
True

isupper() 메서드는 반대로 판단한다.

'aBC'.isupper()
False
'ABC'.isupper()
True
'ABC !'.isupper()
True

다음 세 개의 함수는 모두 입력된 문자열이 소문자를 포함하고 있는지 여부를 조사하도록 구현되었지만 하나만 제대로 작동한다. 어느 함수가 제대로 작동하는지 확인한 다음 그 이유를 설명하라. 또한 다른 두 함수가 적절하게 구현되지 않은 이유를 설명하라.

def any_lowercase1(text):
    for c in text:
        if c.islower():
            return True
        else:
            return False

def any_lowercase2(text):
    for c in text:
        if 'c'.islower():
            return 'True'
        else:
            return 'False'

def any_lowercase3(text):
    flag = False
    for c in text:
        flag = flag or c.islower()
    return flag

아래 문자열에 대해 세 개의 함수를 테스트한다.

word1 = "ABcDE"
word2 = "ABCDE"

any_lowercase1() 함수

def any_lowercase1(text):
    for c in text:
        if c.islower():
            return True
        else:
            return False

인자로 입력되는 문자열의 첫 문자가 소문자의 대소문자 여부를 확인하고 바로 반환값을 지정한다. 함수는 반환값이 지정되면 바로 실행을 종료하기에 결국 첫 문자만 확인하고 종료한다. 따라서 word1 변수가 가리키는 문자열에 소문자 'c'가 포함되어 있지만 첫 문자가 대문자이기에 False를 반환한다.

any_lowercase1(word1)
False
any_lowercase1(word2)
False

any_lowercase2() 함수

def any_lowercase2(text):
    for c in text:
        if 'c'.islower():
            return 'True'
        else:
            return 'False'

for 반복문의 본문이 함수의 인자를 전혀 활용하지 않는다. 이유는 if 'c'.islower() 에서 사용된 'c'for c in textc와 아무 상관이 없는 영어 알파벳 c 하나로 구성된 문자열이기 때문이다. 또한 'c'가 소문자이기에 결과는 항상 문자열 'True'로 심지어 부울값 True와 아무 상관이 없다.

any_lowercase2(word1)
'True'
any_lowercase2(word2)
'True'

any_lowercase3() 함수

def any_lowercase3(text):
    flag = False
    for c in text:
        flag = flag or c.islower()
    return flag

flag 변수는 소문자를 찾았는지 여부를 기억한다. for c in text 반복문에서 ctext에 포함된 문자 하나하나를 가리킬 때 소문자를 만나는 순간 c.lower()True가 되고 따라서 or 연산자에 의해 flag이 계속해서 True를 가리키게 된다. 즉, 소문자가 하나라도 포함되면 True를 반환한다.

any_lowercase3(word1)
True
any_lowercase3(word2)
False

결국 any_lowercase3() 만 소문자 포함여부를 제대로 결정한다.

예제 6

문자열 find() 메서드에 대응하는 find() 함수를 직접 정의하라. 단, 두 개의 매개변수를 사용하며 각 매개변수의 기능은 다음과 같아야 한다.

  • 매개 변수: 두 개의 문자열을 인자로 받는 word1word2 두 개 사용

  • word1word2를 부분 문자열로 포함할 경우 부분 문자열이 시작하는 인덱스 반환. 포함하지 않을 경우 -1을 반환.

단, 문자열 인덱싱, 슬라이싱만 사용하고 다른 문자열 메서드는 전혀 사용하지 않아야 한다.

예를 들어 'banana' 문자열은 'ana' 문자열을 부분 문자열로 포함한다. 'ana' 부분 문자열이 'banana' 문자열의 어느 인덱스에서 시작하는지를 확인하려면 모든 인덱스에 대해 그곳에서부터 'ana' 문자열의 시작하는지 여부를 슬라이싱으로 대조하면 된다.

예를 들어 1번 인덱스부터 'ana' 문자열이 시작하는지 여부는 다음과 같이 확인한다. 아래 코드에서 3은 'ana' 문자열의 길이를 나타낸다.

'banana'[1:1+3] == 'ana'

아래 find() 함수는 word1 문자열이 word2 문자열을 부분 문자열로 포함되는지 여부를 판단하기 위해 word1의 모든 인덱스에 대해 word2 부분 문자열과 일치 여부를 슬라이싱을 이용하여 확인하는 순간에 해당 인덱스를 반환한다. 또한 부분 문자열을 확인하지 못하면 -1을 반환한다.

def find(word1, word2):
    for idx in range(len(word1)):
        if word2 == word1[idx:idx+len(word2)] :
            return idx
    return -1
print(find('banana', 'ana'))
print(find('heyheyhey', 'ey'))
print(find('waikiki', 'ki'))
print(find('ulala', 'la'))
print(find('chukachuka', 'uKa'))
1
1
3
1
-1

예제 7

아래 grid2() 함수는 2x2 모양의 격자 무늬를 그린다.

pm = "+ - - "
ps = "|     "
plus_sign = "+"
pipe_sign = "|"

def print_pm2():
    pm_line = pm * 2 + plus_sign
    print(pm_line)

def print_ps2():
    ps_line = ps * 2 + pipe_sign
    print(ps_line, ps_line, sep="\n")

def grid2():
    count = 0
    while count < 2:
        print_pm2()
        print_ps2()
        count += 1

    print_pm2()
grid2()
+ - - + - - +
|     |     |
|     |     |
+ - - + - - +
|     |     |
|     |     |
+ - - + - - +

grid2() 함수를 구현하는 코드를 수정해서 아래 조건을 만족하는 grid() 함수를 정의하라. 단, 양의 정수를 인자로 받는 매개 변수 n 을 사용한다.

조건:

n 은 그려지는 격자 칸의 개수를 나타낸다. 즉, grid(2)grid2() 와 동일한 2x2 모양의 격자를 그리며, grid(4) 는 아래처럼 4x4 모양의 격자를 그려야 한다.

+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +

주어진 코드를 수정해서 보다 일반화된 기능을 갖는 코드를 구현하려면 먼저 기존 코드를 자세히 살펴봐야 한다.

grid2() 함수는 print_pm2() 함수와 print_ps2() 함수를 이용한다. 따라서 두 함수의 기능을 살펴 보자.

  • print_pm2() 함수 호출

print_pm2()
+ - - + - - +

즉, + - - 를 두번, 마지막에 +를 출력한다.

  • print_ps2() 함수 호출

print_ps2()
|     |     |
|     |     |

즉, |     를 두번, 마지막에 +를 출력하는 과정을 두 번 반복한다.

결국 print_pm2()print_ps2()를 연속 호출하면 아래 모양이 만들어진다.

print_pm2()
print_ps2()
+ - - + - - +
|     |     |
|     |     |

위 코드를 두 번 반복한 다음에 마지막에 다시 print_pm2() 를 호출하면 아래 모양이 완성된다.

count = 0
while count < 2:
    print_pm2()
    print_ps2()
    count += 1

print_pm2()
+ - - + - - +
|     |     |
|     |     |
+ - - + - - +
|     |     |
|     |     |
+ - - + - - +

grid2() 함수는 위 코드에 붙힌 이름에 불과하다.

위 코드의 또 하나의 핵심은 아래 두 변수가 가리키는 문자열을 격자무늬의 핵심 구성 단위로 사용한다는 점이다.

pm = "+ - - "
ps = "|     "

지금까지의 설명을 참고하면서 양의 정수 n이 주어졌을 때, n x n 모양의 격자무늬를 출력하는 grid() 함수를 구현해보자. 이를 위해 아래 두 아이디어를 활용한다.

  • 먼저 격자무늬의 기본 구성 단위는 pmps가 가리키는 문자열과 동일하다.

  • grid() 함수를 정의하기 위해 print_pm2() 함수와 print_ps2() 함수를 일반화한 아래 두 함수를 이용한다.

  • print_pm(n): n이 지정되면 pm을 그만큼 반복한다.

def print_pm(n):
    pm_line = pm * n + plus_sign
    print(pm_line)
  • print_ps(n): n이 지정되면 ps를 그만큼 반복한다.

def print_ps(n):
    ps_line = ps * n + pipe_sign
    print(ps_line, ps_line, sep="\n")

인자로 2를 사용하면 두 함수 각각 print_pm2() 함수와 print_ps2()함수와 동일한 결과를 낸다.

print_pm(2)
+ - - + - - +
print_ps(2)
|     |     |
|     |     |

반면에 4를 인자로 하면 오른쪽으로 보다 길어진다.

print_pm(4)
+ - - + - - + - - + - - +
print_ps(4)
|     |     |     |     |
|     |     |     |     |

즉 4칸짜리 격자무늬를 그리는 기초 구조로 활용될 수 있다. 두 함수를 연속 실행해보자.

print_pm(4)
print_ps(4)
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |

위 코드를 4번 반복하면 4 x 4 모양의 격자무늬가 거의 완성된다. 반복을 위해 역시 while 반복문을 활용하며, 따라서 grid() 함수는 grid2() 함수와 거의 동일하게 구현된다. 다만 함수의 본문에 사용되는 함수들 또한 일반화된 함수를 사용할 뿐이다.

def grid(n):
    count = 0
    while count < n:
        print_pm(n)
        print_ps(n)
        count += 1

    print_pm(n)
  • n = 3일 때

grid(3)
+ - - + - - + - - +
|     |     |     |
|     |     |     |
+ - - + - - + - - +
|     |     |     |
|     |     |     |
+ - - + - - + - - +
|     |     |     |
|     |     |     |
+ - - + - - + - - +
  • n = 4일 때

grid(4)
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +

지금까지 사용된 코드를 정리하면 아래와 같다. 결론적으로 이전 코드에서 숫자 2에 해당하는 부분을 모두 매개 변수 n으로 대체하였을 뿐이다. 이처럼 함수의 일반화는 주로 기존 코드에서 특정 부분을 매개 변수로 대체하는 과정을 따른다.

pm = "+ - - "
ps = "|     "
plus_sign = "+"
pipe_sign = "|"

def print_pm(n):
    print(pm * n + plus_sign)

def print_ps(n):
    ps_line = ps * n + pipe_sign
    print(ps_line, ps_line, sep="\n")

def grid(n):
    count = 0
    while count < n:
        print_pm(n)
        print_ps(n)
        count += 1

    print_pm(n)
  • n = 6일 때

grid(6)
+ - - + - - + - - + - - + - - + - - +
|     |     |     |     |     |     |
|     |     |     |     |     |     |
+ - - + - - + - - + - - + - - + - - +
|     |     |     |     |     |     |
|     |     |     |     |     |     |
+ - - + - - + - - + - - + - - + - - +
|     |     |     |     |     |     |
|     |     |     |     |     |     |
+ - - + - - + - - + - - + - - + - - +
|     |     |     |     |     |     |
|     |     |     |     |     |     |
+ - - + - - + - - + - - + - - + - - +
|     |     |     |     |     |     |
|     |     |     |     |     |     |
+ - - + - - + - - + - - + - - + - - +
|     |     |     |     |     |     |
|     |     |     |     |     |     |
+ - - + - - + - - + - - + - - + - - +

예제 8

grid() 함수가 실행되었을 때 생성되는 격자의 개수를 반환하도록 반환값을 지정하라.

반환값 지정은 return ... 명령문으로 지정된다. 다만 반환값 지정 명령문이 어디에 위치하느냐가 매우 중요하다.

grid() 함수의 기본 기능은 인자 n과 호출되었을 때 n x n 모양의 격자무늬를 그리는 일이다. 따라서 앞서 구현된 grid() 함수의 본문이 모두 실행된 다음에 출력된 격자무늬에 사용된 격자의 개수인 n * n을 함수 본문의 마지막 명령문으로 지정한다.

def grid(n):
    count = 0
    while count < n:
        print_pm(n)
        print_ps(n)
        count += 1

    print_pm(n)

    return n * n

예를 들어 n = 4일 때 호출되면 4 x 4 모양의 격자무늬를 그리면서 동시에 16을 반환한다. 반환값이 None이 아니기에 변수에 할당해서 활용할 수 있다.

num_of_squares = grid(4)
print("격자 개수:", num_of_squares)
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +
|     |     |     |     |
|     |     |     |     |
+ - - + - - + - - + - - +
격자 개수: 16

예제 9

range() 함수를 이용하여 2 보다 같거나 큰 양의 정수 n이 인자로 주어지면 2부터 n까지의 짝수들의 합을 반환하는 함수 addition_n()을 선언하라.

답:

아래 addition_n() 함수는 n이 인자로 들어오면 2부터 n까지의 정수를 포함하는 값 range(2, n+1)에 대해 for 반복문을 실행한다.

def addition_n(n):
    sum = 0
    for item in range(2, n+1, 2):
        sum = sum + item

    print("2부터", n, "까지 짝수들의 합:", sum)
addition_n(10)
2부터 10 까지 짝수들의 합: 30
addition_n(100)
2부터 100 까지 짝수들의 합: 2550
addition_n(1000)
2부터 1000 까지 짝수들의 합: 250500
addition_n(100000000)
2부터 100000000 까지 짝수들의 합: 2500000050000000

주의사항

range() 함수를 이용하지 않고 리스트를 이용하여 동일한 기능을 수행하는 함수를 구현할 수는 없다. 이유는 리스트 [2, 4, 6, ..., n] 의 길이가 n에 의존해서 달라지는데 변수 n이 가리키는 값을 모르면 리스트 자체를 선언할 수 없기 때문이다.

12.7. 연습문제#

참고: (연습) 함수