함수

함수는 여러 명령어들을 계속해서 반복 사용할 필요가 있을 때 사용합니다. 가령 학생의 국어, 영어, 수학 시험 점수의 합계와 평균을 알고 싶으면 다음과 같이 프로그램을 작성할 수 있습니다.

국어 = 90
영어 = 95
수학 = 92

합계 = 국어 + 영어 + 수학
평균 = 합계 / 3
print("합계:", 합계)
print("평균:", 평균)

하지만 다른 학생의 성적의 합계와 평균을 구하려고 하면 위의 코드를 반복해서 작성해 주어야 합니다.

# 지원 성적
지원국어 = 90
지원영어 = 95
지원수학 = 92

지원합계 = 지원국어 + 지원영어 + 지원수학
지원평균 = 지원합계 / 3
print("합계:", 지원합계)
print("평균:", 지원평균)

# 지수 성적
지수국어 = 93
지수영어 = 90
지수수학 = 96

지수합계 = 지수국어 + 지수영어 + 지수수학
지수평균 = 지수합계 / 3
print("합계:", 지수합계)
print("평균:", 지수평균)

이와 같이 코딩하는 것은 지루하고 성가신 작업입니다. 반복 사용되는 구역을 하나의 단위로 묶어 달라지는 것만 입력하게 프로그램을 만들 수 있는 것이 함수입니다.

함수 정의

함수는 예약어 def 뒤에 함수이름, 소괄호 (), 콜론 :을 차례로 적고 다음 줄부터 들여쓰기를 해서 함수 구역을 작성합니다. 정의된 함수를 부를 때는 함수이름과 괄호()를 사용합니다. 괄호 안에는 함수에서 사용될 변수이름을 쉼표(,)를 이용하여 나열할 수 있습니다. 소괄호 안에 사용되는 변수이름을 함수의 매개변수(parameter)라고 부릅니다. 만일 함수에서 매개변수가 필요하지 않다면 적지 않아도 됩니다.

def <함수이름>(<매개변수1, 매개변수2, ..., 매개변수n>): # 매개변수는 필요없으면 안 적어도 됩니다.
    <함수몸통>

매개 변수 없는 함수

다음과 같이 매개변수가 없는 간단한 함수를 작성해봅니다.

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

함수를 정의하기 위해서는 항상 def라는 예약어로 시작해야 합니다. 그리고 함수이름 안녕이 나오고 다음으로 반드시 괄호 ()를 열고 닫고 콜론 :으로 함수 머리부분을 마친다. 함수의 몸통은 다음 줄 들여쓰기와 함께 시작되고 들여쓰기 구역이 끝나면 함수의 선언이 끝이 난다. 다음과 같이 함수이름과 괄호를 입력해서 함수를 호출할 수 있습니다.

안녕()
안녕하세요

매개변수가 있는 함수

위에서 작성했던 합계와 평균을 구하는 문제를 함수를 사용해서 바꿔봅니다. 위에서 학생들이 바뀌면 점수가 달라지지만 합계와 평균을 구하는 방법과 출력하는 부분은 같은 것을 알 수 있습니다. 학생들에 따라 바뀌는 국어, 영어, 수학 점수를 함수의 매개변수로 설정하고 그 값에 따른 합계, 평균을 출력하는 방법은 동일하므로 이 부분을 함수의 몸통에서 계산하게 합니다.

def 합계_평균(, , ):
     =  +  + 
     =  / 3
    print("합계: {}, 평균: {:.2f}".format(, ))

함수이름은 합계_평균이고 매개변수는 각각 , , 입니다. 이와 같이 함수이름, 매개변수와 같은 식별자(identifier)는 한글을 사용해도 됩니다.

함수를 호출하기 위해서는 함수이름과 함께 매개변수에 대응되는 값들을 괄호 안에 넣어주면 됩니다.

함수를 부를 때 괄호 안에 들어가는 값들을 `인수 <https://docs.python.org/3/glossary.html#term-argument>`__(argument)라고 부릅니다.

합계_평균(90, 95, 92)
합계: 277, 평균: 92.33

여기서 인수들은 각각, 90, 95, 92가 됩니다.

함수는 필요할 때 언제든지 부를 수 있습니다. 두 번째 부를 때 합계_평균(93, 90, 96) 함수의 인수에 다른 학생의 점수를 넣으면 그 학생의 합계와 평균이 출력이 됩니다.

합계_평균(93, 90, 96)
합계: 279, 평균: 93.00

정리하면 매개변수는 함수를 정의할 때 괄호 안에 사용되는 식별자이고 인수는 함수를 부를 때 괄호 안에 넣는 값을 의미합니다.

위 코드에서 학생의 이름도 같이 넘겨주어 출력하면 알아보기 편리할 것입니다.

def 나은_합계_평균(이름, , , ):
     =  +  + 
     =  / 3
    print("{}의 성적:\n국어: {}, 영어: {}, 수학: {}".format(이름, , , ))
    print("합계: {}".format())
    print("평균: {:.1f}".format())

나은_합계_평균("지원", 90, 95, 92)
나은_합계_평균("지수", 93, 90, 96)
지원의 성적:
국어: 90, 영어: 95, 수학: 92
합계: 277
평균: 92.3
지수의 성적:
국어: 93, 영어: 90, 수학: 96
합계: 279
평균: 93.0

print("평균: {0:.1f}".format(평))에서.1f을 보여줄 때 부동 소수점 아래 한 자리만 표시하라는 뜻입니다.

직접하기

  • 국어, 영어, 수학 점수를 하나의 리스트로 입력받아 합계와 평균을 구하는 함수를 작성하고 실행하시오. 함수의 이름과 매개변수를 리스트_합계_평균(국영수)와 같이 하시오.

직사각형 그리기 예제

거북이를 이용하여 다양한 크기의 직사각형을 그리기를 원한다고 해봅니다. 가로, 세로의 길이가 변할 때마다 새로운 코드를 작성해 주어야 할 것입니다.

import turtle

연필 = turtle.Pen()

# 가로 300, 세로 200인 직사각형
연필.fd(300) # fd()는 forward()의 줄임말
연필.left(90)
연필.fd(200)
연필.left(90)
연필.fd(300)
연필.left(90)
연필.fd(200)
연필.left(90)

# 가로 200, 세로 100인 직사각형
연필.fd(200)
연필.left(90)
연필.fd(100)
연필.left(90)
연필.fd(200)
연필.left(90)
연필.fd(100)
연필.left(90)

가로, 세로의 길이를 입력받아 직사각형을 그리는 함수를 만들면 매번 코드를 만들 필요가 없어질 것입니다.

import turtle

연필 = turtle.Pen()

def 직사각형(가로, 세로): # 함수 이름은 직사각형
    for _ in range(2):
        연필.fd(가로)
        연필.left(90)
        연필.fd(세로)
        연필.left(90)

직사각형(300, 200)
직사각형(200, 100)
  • 함수이름을 직사각형이라 짓고 매개변수로 가로, 세로를 설정합니다.

  • for문을 이용해서 가로, 세로 그리기를 2번 반복하여 직사각형을 그린다.

직접하기

  • 한 변의 길이를 매개변수로 하는 정삼각형을 그리는 함수를 만들어, 그 함수를 이용해 길이가 50, 100, 150 인 정삼각형을 만들자.

  • 원 둘레를 네 가지 색깔(빨강, 파랑, 초록, 노랑)로 색칠하는 함수 프로그램을 만드시오. 함수는 반지름을 매개변수로 갖고 원의 중심은 (0,0)이 되게 작성하시오. 이 함수를 4번 반복실행하여 반지름이 50, 100, 150, 200인 다음과 같은 그림을 그리시오.

매개변수와 인수

매개변수는 5가지 종류로 구분할 수 있습니다. 위치-또는-키워드 positional-or-keyword 매개변수, 위치전용 positional-only, 키워드 전용 keyword-only, 가변 위치 var-positional 매개변수와 가변 키워드 var-keyword 매개변수로 나눌 수 있습니다. 인수 argument란 함수를 부를 때 매개변수에 대응되어 넘겨주는 값을 말합니다.

위치-또는 키워드 매개변수 positional-or-keyword

위치-또는-키워드 매개변수란 일반적인 매개변수를 의미합니다.

In [1]: def speak(stmt1, stmt2="만나서 반가워요!"):
   ...:   print(stmt1, stmt2)
   ...: 
   ...: speak(stmt1="안녕하세요.", stmt2="반갑습니다.") # 키워드 인수
   ...: speak("여러분!")                               # 위치 인수
   ...: 
안녕하세요. 반갑습니다.
여러분! 만나서 반가워요!

speak 함수에서 소괄호 안에 있는 stmt1, stmt2가 위치-또는-키워드 매개변수가 됩니다.

위치 전용 positional-only 매개변수

/ 앞에 정의된 매개변수를 위치 전용 매개변수라고 합니다. 다음에서 posonly1posonly2 가 위치전용 매개변수입니다.

def func(posonly1, posonly2, /, positional_or_keyword): ...

위치전용 매개변수는 키워드를 이용해서 인수를 대입할 수 없습니다. 다음 두 가지 함수를 비교해 보세요.

다음은 위치-또는-키워드 매개변수를 이용해 정의한 함수입니다.

In [2]: def inc(x):
   ...:   return x + 1
   ...: 
   ...: inc(x=10) # 정상 작동
   ...: 
Out[2]: 11

하지만 다음과 같이 / 를 이용해서 위치전용 매개변수로 지정하면 에러가 발생합니다.

In [3]: def inc(x, /):
   ...:   return x + 1
   ...: 
   ...: inc(x=10) # error
   ...: 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-704cd9a45166> in <module>
      2   return x + 1
      3 
----> 4 inc(x=10) # error

TypeError: inc() got some positional-only arguments passed as keyword arguments: 'x'

위치전용 매개변수는 위치-또는-키워드 매개변수보다 앞에 위치해 있어야 합니다.

def greet(name, /, greeting):
  print(f'{name}, {greeting}')

키워드 전용 매개변수 keyword-only

키워드 전용 매개변수란 인수를 대입할 때 매개변수=값과 같이 할당 기호 =를 사용해야만하는 변수를 말합니다. 함수 정의할 때 * 뒤에 오는 매개변수는 키워드 전용 매개변수입니다. 다음에서 kw_only1, kw_only2 가 키워드 전용 매개변수 입니다.

def func(arg, *, kw_only1, kw_only2): ...
In [4]: def to_fahrenheit(*, celsius):
   ...:   return 32 + celsius * 9 / 5
   ...: 
   ...: to_fahrenheit(40) # 에러 발생
   ...: 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-04dd89c59fbd> in <module>
      2   return 32 + celsius * 9 / 5
      3 
----> 4 to_fahrenheit(40) # 에러 발생

TypeError: to_fahrenheit() takes 0 positional arguments but 1 was given
In [5]: to_fahrenheit(celsius=40) # 정상 작동
Out[5]: 104.0

때때로 함수의 인수의 갯수가 상황에 따라서 달라져야 할 필요가 있습니다. 이 때 사용할 수 있는 매개변수가 가변 매개변수입니다. 가변 매개변수는 가변 위치 매개변수와 가변 키워드 매개변수로 나눌 수 있습니다.

가변 위치 매개변수 var-positional

가변 위치 매개변수는 별표(*)와 매개변수이름을 함께 적어 표시합니다. 함수를 부를 때 위치 인수들이 차례로 가변 위치 매개변수 이름에 튜플로 할당됩니다.

def func(*args, **kwargs): ... # args가 가변 위치 매개변수

가변 위치 매개변수를 갖는 함수를 예를 들어봅니다.

def 합계(*항목들):
     = 0
    for 항목 in 항목들:
         += 항목
    print('합계 =', )

합계(1, 2, 3, 4, 5)
합계 = 15
  • *항목들 에서 항목들이 가변 위치 매개변수입니다.

  • 합계(1, 2, 3, 4, 5)의 위치 인수들 1, 2, 3, 4, 5가 차례로 항목들 변수에 튜플로 할당됩니다. 즉, 항목들 = (1, 2, 3, 4, 5)와 같이 할당됩니다.

가변 위치 매개변수 다음에 오는 매개변수는 반드시 키워드 인수로 입력해야 합니다.

가변 키워드 매개변수 var-keyword

가변 키워드 매개변수는 별표 두 개(**)에 이어서 가변 키워드 매개변수 이름을 사용합니다. 다음에서 **kwargs이 가변 키워드 매개변수 입니다.

def func(*args, **kwargs): ... # kwargs가 가변 키워드 매개변수

함수를 부를 때 키워드 인수로 입력한 값들이 가변 키워드 매개변수이름에 사전(dict)형식으로 대입됩니다. 가변 키워드 매개변수는 매개변수들 중 맨 마지막에 위치해야 합니다.

In [6]: def 영화(**키워드들):
   ...:   for 키워드 in 키워드들:
   ...:     print(키워드, ":", 키워드들[키워드])
   ...: 
   ...: 영화(제목="클래식", 감독="곽재용", 각본="곽재용", 출연="손예진, 조인성, 조승우, 이기우", 개봉일="2003년 1월 30일")
   ...: 
제목 : 클래식
감독 : 곽재용
각본 : 곽재용
출연 : 손예진, 조인성, 조승우, 이기우
개봉일 : 2003년 1월 30일
  • 함수 영화(제목="클래식", 감독="곽재용", 각본="곽재용", 출연="손예진, 조인성, 조승우, 이기우", 개봉일="2003년 1월 30일")를 키워드 인수로 입력하면 키워드들 가변 키워드 매개변수에 사전(dict)형식으로 입력이 됩니다.

튜플사전형을 참조하세요.

매개변수 기본값

어떤 경우에는 함수를 부를 때 인수를 선택적으로 넘겨주어 사용자가 값을 넘겨주지 않으면 자동으로 기본값을 사용하도록 하는 것이 편할 때가 있습니다. 이런 경우, 매개변수에 기본값 default value을 지정하면 됩니다. 함수를 선언할 때 원하는 매개변수 뒤에 할당 연산자(=)와 기본값을 입력하여 지정합니다.

def 말하기(문장, *, 횟수=1): # 횟수는 기본값 매개변수, 1은 기본값
    print(문장 * 횟수) # 문장을 횟수만큼 출력합니다.

말하기("안녕하세요. 예은님!") # 기본값 매개변수를 지정하지 않았기 때문에 기본값 1회 출력
말하기("예은", 5) # 기본값 매개변수 부분에 5를 지정함으로 5회 출력
  • def 말하기(문장, 횟수=1): 중에서 횟수이 기본값 매개변수이고 기본값은 1로 설정합니다.

  • 말하기("안녕하세요. 예은님!")에서 말하기() 함수를 부를 때 매개변수 횟수에 해당되는 부분이 입력되지 않았으므로 횟수는 기본값 1을 사용하게 되어 한 번만 출력합니다.

  • 말하기("예은", 5)에서 "예은" 다음에 오는 5가 키워드 매개변수 횟수에 해당하는 인수값으로 5회를 출력하게 됩니다.

직접하기

  • 직사각형 그리기에서 매개변수로 가로, 세로 길이와 기본 매개변수로 선두께=1, 선색깔="red"을 기본 인수값으로 갖는 함수 프로그램을 작성하고 실행하시오.

매개변수 순서

위치 전용, 일반, 키워드 전용 매개변수들을 함께 사용할 때는 /* 순서로 구분해서 사용해야 합니다.

In [7]: def headline(text, /, border="~", *, width=50):
   ...:   return f" {text} ".center(width, border)
   ...: 

text 는 위치 전용 매개변수, border 는 일반(위치-또는-키워드) 매개변수이고 * 뒤에 오는 width 는 키워드 전용 매개변수 입니다.

In [8]: headline("Positional-only Arguments")
Out[8]: '~~~~~~~~~~~ Positional-only Arguments ~~~~~~~~~~~~'

text 는 위치 전용 매개변수이므로 키워드 인수로 입력할 수 없습니다.

In [9]: headline(text="This doesn't work!") # 에러
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-9-931d1983ffb7> in <module>
----> 1 headline(text="This doesn't work!") # 에러

TypeError: headline() got some positional-only arguments passed as keyword arguments: 'text'

border 는 위치-또는-키워드 매개변수이므로 키워드를 사용할 수도 하지 않아도 됩니다.

In [10]: headline("Python 3.8", "=")
Out[10]: '=================== Python 3.8 ==================='
In [11]: headline("Real Python", border=":")
Out[11]: ':::::::::::::::::: Real Python :::::::::::::::::::'

width 는 키워드 인수로 입력되야 합니다.

In [12]: headline("Python", "@", width=38)
Out[12]: '@@@@@@@@@@@@@@@ Python @@@@@@@@@@@@@@@'
In [13]: headline("Python", "@", 38) # 에러
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-69457be22fc5> in <module>
----> 1 headline("Python", "@", 38) # 에러

TypeError: headline() takes from 1 to 2 positional arguments but 3 were given

키워드 인수(keyword argument)

함수를 부를 때 괄호 안에 사용되는 인수(argument)키워드 인수(keyword argument)와 위치 인수(positional argument)로 나눌 수 있습니다. 키워드 인수란 매개변수(또는 식별자) = 과 같이 사용되는 인수를 말하고 그렇지 않은 모든 인수를 위치 인수라고 합니다. 말하기() 함수 예제를 살펴봅니다.

def 말하기(문장, 횟수=1):
    print(문장 * 횟수)

말하기(문장="안녕하세요. 예은님!") # 키워드 인수
말하기("예은", 5) # 위치 인수
말하기(횟수=5, 문장="예은") # 키워드 인수를 사용하여 인수 순서 변경
안녕하세요. 예은님!
예은예은예은예은예은
예은예은예은예은예은
  • 말하기(문장="안녕하세요. 예은님!") 에서 문장="안녕하세요. 예은님!"이 키워드 인수입니다. 문장이 매개변수 이름(식별자)이고 "안녕하세요. 예은님!"이 인수값입니다.

  • 말하기("예은", 5)에서 인수 "예은", 5는 위치인수입니다.

  • 말하기(횟수=5, 문장="예은")에서와 같이 키워드 인수를 사용하면 순서를 바꿀 수 있습니다.

위치 인수는 반드시 키워드 인수 앞에 위치해야 합니다. 다음과 같이 키워드 인수 뒤에 위치 인수를 사용하면 에러가 발생합니다.

말하기(횟수=5, "예은")
  File "<ipython-input-133-cabeff611f37>", line 1
    말하기(횟수=5, "예은")

^
SyntaxError: positional argument follows keyword argument

매개변수 기본값과 가변 객체

매개변수의 기본값으로 가변 객체를 사용할 때 주의해야 합니다. 다음과 같이 애완동물에 대한 리스트를 만들려고 합니다.

def 애완동물리스트(동물, 리스트=[]):
    리스트.append(동물)
    return 리스트
고양이 = 애완동물리스트('스코티시 폴드')
고양이
['스코티시 폴드']
 = 애완동물리스트('치와와')

['스코티시 폴드', '치와와']

함수의 몸통 부분은 여러번 실행할 수 있지만 함수의 머리 부분을 포함한 정의는 단 한번만 실행이 됩니다. 따라서 애완동물리스트의 매개변수 리스트는 처음 정의를 실행할 때 한 번 만들어지므로 함수를 호출하더라도 같은 객체를 공유하게 됩니다.

각 함수를 실행할 때마다 다르게 리스트를 만들고 싶으면 다음과 같이 합니다.

def 애완동물리스트1(동물, 리스트=None):
    if 리스트 is None:
        리스트 = []
    리스트.append(동물)
    return 리스트
 = 애완동물리스트1('먼치킨')

['먼치킨']
 = 애완동물리스트1('시추')

['시추']

객체 싸기(packing) 및 풀기(unpacking)

반복가능 객체의 성분들을 변수에 대입하는 것을 싸기라고 합니다.

a = [1, 2, 3]

거꾸로 자료들을 풀어 변수에 대입하는 것을 풀기라고 합니다.

a, b, c = (1, 2), 3, '풀기'
a
(1, 2)
b
3
c
'풀기'
*x, = range(5)
x
[0, 1, 2, 3, 4]
x, *y, z = range(5)
x, y, z
(0, [1, 2, 3], 4)
[x, *y, z] = range(5)
x, y, z
(0, [1, 2, 3], 4)

리스트 또는 튜플의 성분들을 순서대로 위치 인수에 넘겨주기 위해서는 *를 이용하면됩니다. 리스트 또는 튜플 앞에 *를 붙여 인수에 대입하면 리스트 또는 튜플이 해체되어 성분들이 순서대로 인수로 대입됩니다. 일반적으로 *<반복가능 객체>이면 됩니다. 내장 함수 range() 는 시작, 끝, 간격을 인수로 입력 받을 수 있습니다.

인수들 = (1, 10, 2) # 튜플

list(range(*인수들)) # 인수들 튜플을 풀어서 1, 10, 2를 인수로 넘긴다.
[1, 3, 5, 7, 9]

마찬가지 방법으로, **를 이용하여 사전 객체를 풀어서 키워드 인수 형식으로 넘겨줄 수 있습니다.

def 지저귀기(**새들):
    for  in 새들:
        print(, "는 ", 새들[], sep="")

새들 = {"참새": "짹짹", "갈매기":"끼룩끼룩", "매미":"맴맴"} # 사전 객체
지저귀기(**새들) # 사전 객체를 풀어 키워드 인수로 변경하여 넘겨줍니다.
참새는 짹짹
갈매기는 끼룩끼룩
매미는 맴맴

직접하기

  • 가변 위치 매개변수만을 갖는 더하기 함수 더하기(*숫자들)를 만들고 그 함수를 이용하여 1부터 1000까지 더하기를 하시오.

return 문

함수가 끝날 때 함수의 결과를 return 문을 이용해서 반환할 수 있습니다. 예를 들면 합계를 구하는 함수를 부르면 그 결과값으로 합계를 반환하면 그 반환값을 가지고 다른 곳에서 사용할 수 있을 것입니다. return 문은 return 뒤에 객체를 적어 주면 그 객체가 함수의 종료와 함께 반환됩니다.

def 합계(*항목들):
     = 0
    for 항목 in 항목들:
         += 항목
    return 

국어합 = 합계(86, 90, 76)
영어합 = 합계(90, 64, 93)
print('총합 =', 국어합 + 영어합)
총합 = 499
  • return 을 실행하면 함수가 종료가 되면서 을 반환합니다.

함수 실행 중에 return문을 만나면 그 즉시 함수는 종료됩니다. 즉, 함수 구역에서 return 아래 어떤 문장도 실행이 되질 않고 함수를 끝내고 return 문장과 함께 있는 객체가 반환이 됩니다.

def 몫과나머지(, 나눌수):
    if not 나눌수:
        return None, None
     =  // 나눌수
    나머지 =  % 나눌수
    return , 나머지
, 나머지 = 몫과나머지(7, 3)
print("몫: {}, 나머지: {}".format(, 나머지))
: 2, 나머지: 1

return 문 뒤에 아무것도 지정하지 않으면 None을 반환합니다.

def 반환없음():
    print("return이 없으면 None객체를 반환합니다.")

 = 반환없음()
print("반환값:", )
return이 없으면 None객체를 반환합니다.
반환값: None

None은 파이썬의 예약어로 아무것도 없다는 표시입니다. 함수가 종료될 때 return 문이 없으면 암시적으로 None을 반환합니다.

직접하기

  • 원의 반지름을 입력받아 원의 면적을 반환하는 함수를 작성하자.

변수 유효범위

지역 변수

함수 안에서 만들어진 변수는 함수를 벗어나서는 사용할 수 없다. 이러한 변수를 지역 변수(local variable)라고 합니다. 다음 프로그램을 살펴봅니다.

def 함수1():
    y = 5 # 함수 안에서 y값 만듦
    print("함수 안에서 만든 y 값:", y)

함수1() # 함수를 부릅니다.
print(y) # 함수 안에서 만든 지역 변수 y를 함수 밖에서 부른면 에러
함수 안에서 만든 y : 5
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-149-ae384abcefd6> in <module>()
      4
      5 함수1() # 함수를 부릅니다.
----> 6 print(y) # 함수 안에서 만든 지역 변수 y를 함수 밖에서 부른면 에러


NameError: name 'y' is not defined

위 코드를 실행하게 되면 함수()는 정상적으로 실행되지만, print(y)를 실행하는 순간 NameError: name 'y' is not defined에러가 발생하게 됩니다. 함수 안에서 선언된 변수 y는 함수가 실행될 때 만들어 졌다가 함수 실행을 끝내는 순간 사라지기 때문에 y변수 이름이 존재하지 않는다는 에러를 발생하게 됩니다.

다음 함수2() 안에 있는 x는 첫 줄에서 정의된 전역변수 x = 1이 대입되어 x += 1을 실행하면 x = 2가 될 것이라고 예상할 수 있지만 다음과 같은 오류가 발생합니다.

x = 1

def 함수2():
    x += 1 # x = x + 1
    print(x)

함수2()
---------------------------------------------------------------------------

UnboundLocalError                         Traceback (most recent call last)

<ipython-input-2-9ce1f0ab791a> in <module>()
      5     print(x)
      6
----> 7 함수2()


<ipython-input-2-9ce1f0ab791a> in 함수2()
      2
      3 def 함수2():
----> 4     x += 1
      5     print(x)
      6


UnboundLocalError: local variable 'x' referenced before assignment

x = x + 1에서, 좌변의 변수 x는 함수 안에서 처음으로 등호에 의해서 값을 할당하려고 할 때 자동으로 지역변수로 선언이 됩니다. 그런데 우변의 x는 값이 할당이 안된 상태에서 더하기를 하려고 해서 오류를 발생하게 됩니다.

다음의 경우도 함수3() 안에서 x = 1로 처음으로 값이 할당되기 때문에 x는 지역변수로 정의됩니다. 그런데 print(x)에서 변수에 값이 할당되기전에 x를 사용하려고 해서 오류가 발생됩니다. 자세한 것은 모듈의 이름공간 부분에서 다룬다.

x = 1

def 함수3():
    print(x)
    x = 1

함수3()
---------------------------------------------------------------------------

UnboundLocalError                         Traceback (most recent call last)

<ipython-input-5-f80809c15b0a> in <module>()
      5     x = 1
      6
----> 7 함수3()


<ipython-input-5-f80809c15b0a> in 함수3()
      2
      3 def 함수3():
----> 4     print(x)
      5     x = 1
      6


UnboundLocalError: local variable 'x' referenced before assignment

매개변수와 지역변수

함수의 매개변수로 선언된 변수는 함수 안에서만 사용되는 지역변수입니다.

x = 10 # 전역변수

def 함수2(x):
    print("함수 안에 있는 x:", x) # 함수를 부를 때 넣은 x값 출력
    x = 5 # 함수 안에서 x값 변경과 동시에 지역변수로 변합니다.
    print("함수 안에서 변경된 x 값:", x) #변경된 x값 출력

함수2(x) # 전역변수 x=10을 건네줍니다.
print("함수 밖에 있는 x:", x)
함수 안에 있는 x: 10
함수 안에서 변경된 x : 5
함수 밖에 있는 x: 10

x = 10이라고 선언되었기 때문에 함수2(x)를 부를 때 x대신 10이 인수로 넘어간다. print("함수 안에 있는 x:", x) 에서는 10을 출력하게 되고 다음 문장 x = 5에서 위에 있던 x를 무시하고 새로운 지역 변수 x를 만들게 됩니다. 함수2()가 끝나는 시점까지 x는 지역변수가 됩니다. 함수를 끝내고 다음 문장 print("함수 밖에 있는 x:", x)을 실행하면 함수 안에서 만들어졌던 지역변수 x는 사라지고 전역변수 x=10이 그대로 출력이 됩니다.

global 문

함수 안에서 변경된 값이 계속해서 함수가 끝난 후에도 적용되게 하고 싶을 때는 global 예약어를 사용하면 됩니다.

x = 10 # 전역변수

def 함수3():
    global x
    print("함수 안에서 변경되기 전 x:", x)
    x = 5 # 함수 안에서 x값 변경
    print("함수 안에서 변경된 후 x 값:", x) # 변경된 x값 출력

함수3()
print("함수 실행 끝낸 후 x:", x)
함수 안에서 변경되기  x: 10
함수 안에서 변경된  x : 5
함수 실행 끝낸  x: 5

함수를 실행하기 전에 변수 x=10이 값을 가지고 있고 함수 실행을 하면 global x라는 문장을 통해서 이미 있는 변수 x는 전역변수로 간주를 합니다. 따라서 함수 안에서 x의 값을 변경하면 함수가 끝난 후에도 변경된 값이 적용이 됩니다. 전역변수를 여러 개 선언하고 싶으면 global x, y, z와 같이 합니다.

global 변수는 사용되기 전에 선언이 되어 있어야 합니다. 다음은 xglobal 변수로 선언되기전에 x = 1이라고 할당을 해서 함수가 정의되는 과정에서 오류가 발생합니다.

def 함수4():
    x = 1
    global x
    print(x)
  File "<ipython-input-6-197b991c8018>", line 3
    global x
    ^
SyntaxError: name 'x' is assigned to before global declaration

함수 밖에서 전역변수로 설정이 되지 않은 상태에서 함수 안에서 전역변수로 설정을 해서 사용할 수도 있습니다.

def 함수5():
    global x
    x = 10
    print('함수 안에서: ', x)

함수5()
print('함수 밖에서: ', x)
함수 안에서:  10
함수 밖에서:  10

global 변수는 꼭 필요한 경우가 아니면 사용하지 말아야 합니다. 왜냐면 global 변수가 선언된 함수를 프로그램 중에서 호출할 때마다 변수의 값이 변하기 때문에 값의 상태를 예측할 수 없기 때문입니다.

가변 객체 변수

파이썬에서는 함수 안의 변수에 새로운 객체가 할당되기 전까지는 그 변수는 전역변수로 간주합니다. 물론 그 변수는 이미 선언되어 있어야 합니다. 그렇지 않고 새로운 객체를 변수에 할당하는 순간에 그 변수는 지역변수가 됩니다. 따라서 다음과 같이 가변 객체(mutable object)인 리스트 성분을 함수 안에서 변경한다고 해서 지역변수가 되는 것은 아니다.

x = [1, 2, 3] # 가변 객체 리스트

def 함수4():
    print("함수 안에 있는 x:", x) # 전역변수 x값 출력
    x[0] = 5 # 함수 안에서 x의 첫번째값 변경
    print("함수 안에서 변경된 x 값:", x) # 함수 안에서 변경된 x값 출력

함수4()
print("함수 실행 후 x:", x) # 함수 안에서 변경된 값이 출력됩니다.
함수 안에 있는 x: [1, 2, 3]
함수 안에서 변경된 x : [5, 2, 3]
함수 실행  x: [5, 2, 3]

리스트는 가변 객체이기 때문에 함수4() 안에서 x[0] = 5 문을 실행하면 함수 안에서 x의 성분을 변경할 수 있습니다. 함수 안에서 리스트 객체가 새로 만들어진 것이 아니기 때문에 함수 실행이 끝나도 x는 계속 전역변수를 유지하게 됩니다. 따라서 바뀐 값이 적용되어 출력됩니다. 가변 객체란 객체를 구성하는 성분을 변경할 수 있는 객체를 의미하고 성분을 바꿀 수 없는 객체를 불변 객체(immutable object)라고 합니다. 자세한 것은 클래스 부분에서 다루게 됩니다. 대표적인 불변 객체로는 숫자형(int, float, complex), 문자열(str) 및 튜플(tuple)형이 있습니다.

함수 객체

함수도 객체이기 때문에 변수에 할당할 수 있습니다.

def 함수():
    print("함수")

f = 함수
f()
함수

함수 이름으로 변수에 할당하는 것과 함수()를 할당하는 것은 다르다. 함수()은 함수를 호출하여 얻어진 반환값이기 때문에 반환값이 할당됩니다.

def 함수():
    print("함수")

f = 함수()
print(f)
함수
None

lamda 식

이름이 없는 간단한 함수를 만들 때 lambda 식을 이용합니다. 사용방법은 다음과 같습니다.

lambda <매개변수1, 매개변수2, ..., 매개변수n> : <반환될 표현식>

다음은 제곱을 하는 함수를 람다식을 이용해서 만든 것입니다.

f = lambda x : x ** 2
f(2)
4

다음은 람다식을 이용해서 함수의 반환값이 다시 함수가 되도록 만든 것입니다.

def 증분(n):
    return lambda x: x + n

증분2 = 증분(2)
증분5 = 증분(5)

증분(2)의 반환값은 람다함수로서 람다함수의 인수값에 2를 더한 값을 반환합니다.

증분2(10)
12

증분(5)도 람다함수로 5를 더한 값을 반환합니다.

증분5(10)
15

다음은 리스트 항목을 sort() 메소드를 이용해 정렬하는 예입니다.

pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
pairs.sort(key=lambda pair: pair[1])
pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]
  • 리스트의 sort 메소드는 key 인수값으로 하나의 매개변수만을 갖는 함수를 취합니다. 이 함수의 반환값들을 이용해 비교 연산자 < 연산을 수행하여 리스트 항목을 정렬합니다.

  • key=lambda pair: pair[1]에서 lambda pair: pair[1] 부분이 함수이고 함수의 입력 매개변수는 pair이고 이 매개변수에 리스트의 항목이 하나씩 입력됩니다. 콜론 뒤에 수식 pair[1]이 함수의 반환값으로 가령 pair = (1, 'one')이면 pair[1]'one'이 되는 것입니다.

  • 따라서 각각 반환되는 함수값은 두번째 성분인 'one', 'two', 'three', 'four'가 되고 이것들을 < 연산을 가지고 정렬을 하면 결과와 같이 나오는 것입니다.

생성자(generator)

생성자는 반복자 iterator 의 특수한 한 형태입니다. 생성자 함수generator function 는 함수 안에 yield를 사용하여 값을 반환하는 함수입니다. 생성자 함수가 처음 호출되면, 그 함수 실행 중 처음으로 만나는 yield 에서 값을 반환하고 함수 실행을 멈춥니다. 생성자 함수가 다시 호출되면, 직전에 실행되었던 yield 문 다음부터 다음 yield 문을 만날 때까지 문장들을 실행하고 yield에서 실행을 멈추고 값을 반환합니다. 더 이상 실행할 yield가 없을 때가지 이러한 과정을 반복합니다. 이러한 생성자 함수를 호출하여 생성자 generator 객체를 만듭니다.

다음은 간단한 생성자 함수와 그 호출 예를 보인 것입니다. 여기서 생성() 함수는 생성자 함수로서 3개의 yield 문을 가지고 있습니다. 따라서 한번 호출시마다 각 yield 문에서 실행을 중지하고 값을 리턴하게 됩니다.

In [14]: def 생성함수():
   ....:   yield 1
   ....:   yield 2
   ....:   yield 3
   ....: 

생성자 함수를 호출하여 생성자를 만들어 아래와 같이 변수 g에 할당합니다. 그러면 g는 생성자 객체가 됩니다.

In [15]: g = 생성함수()
   ....: type(g)
   ....: 
Out[15]: generator

생성자 호출은 내장함수 next(생성자)를 이용할 수 있습니다. next를 처음 호출하면 처음으로 만나는 yield 1에서 실행을 멈추고 yield 뒤에 있는 값 1을 반환합니다.

In [16]: print(next(g))
1

다시 next를 실행하면 yield 1 다음 문장 즉, yield 2에서 실행을 멈추고 2를 반환합니다.

In [17]: print(next(g))
2

next를 실행하면 yield 2 다음 문장, yield 3에서 실행을 멈추고 3을 반환합니다.

In [18]: print(next(g))
3

생성자가 더이상 반환할 값이 없으면 StopIteration 예외를 발생시킵니다.

In [1]: print(next(g))
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-167-1dfb29d6357e> in <module>()
----> 1 print(next(g))

StopIteration:

for 문을 이용해서 자동으로 반복자를 가져오고 next 함수를 실행시켜 값을 가져옵니다.

In [19]: for x in 생성함수():
   ....:   print(x)
   ....: 
1
2
3

리스트나 집합 set 과 같은 자료형에 대한 반복자는 해당 객체가 이미 모든 값을 가지고 있는 경우이나, 생성자는 모든 값을 갖지 않은 상태에서 yield에 의해 하나씩만 값을 만들어 가져온다는 차이점이 있습니다. 이러한 생성자는 자료가 매우 커서 모든 자료를 한꺼번에 반환할 수 없는 경우나, 모든 자료를 미리 계산하면 속도가 느려서 필요한 자료 요청시 처리하는 것이 좋은 경우 등에 사용됩니다.

직접하기

  1. 구구단 출력하는 프로그램을 생성자 함수 구구단()으로 만들고 for문을 이용해 호출해 보세요. 즉 다음을 실행하면 구구단이 출력되어야 합니다.

    for 구구 in 구구단():
      print(구구)
    
    1 x 1 = 1
    1 x 2 = 2
    1 x 3 = 3
    1 x 4 = 4
    1 x 5 = 5
    1 x 6 = 6
    1 x 7 = 7
    1 x 8 = 8
    1 x 9 = 9
    2 x 1 = 2
    .
    .
    .
    8 x 9 = 72
    9 x 1 = 9
    9 x 2 = 18
    9 x 3 = 27
    9 x 4 = 36
    9 x 5 = 45
    9 x 6 = 54
    9 x 7 = 63
    9 x 8 = 72
    9 x 9 = 81
    

생성자 축약

생성자 식은 생성자 축약(generator comprehension)으로도 불리우는데, 리스트 축약 list comprehension 과 외관상 유사합니다. 리스트 축약은 앞뒤를 대괄호 [...]로 표현한다면, 생성자 축약은 소괄호 (...)를 사용합니다. 하지만 생성자 축약은 리스트 축약과 달리 실제 값 전체를 반환하지 않고, 그 표현식만을 갖는 생성자 객체만 반환합니다. 즉 실제 실행은 하지 않고, 표현식만 가지며 위의 yield 방식으로 늦은 연산(Lazy Operation)을 수행하는 것입니다.

아래 예제는 1부터 10까지의 숫자에 대한 제곱값을 생성자 축약으로 표현한 것으로 여기서 생성자 축약을 할당받은 변수 g는 생성자 타입 객체입니다.

In [20]: g = (x*x for x in range(1, 11))
In [21]: print(type(g))
   ....: 
<class 'generator'>

for 문을 사용하여 10개의 next() 문을 실행하여 처음 10개에 대한 제곱값을 출력한 것입니다.

In [22]: for i in range(10):
   ....:   print(next(g))
   ....: 
1
4
9
16
25
36
49
64
81
100

또는 다음과 같이 for문에 직접 생성자를 넣어서 실행할 수 있습니다.

In [23]: for x in g:
   ....:   print(x)
   ....: 

그런데 위와 같이 실행하면 아무런 결과가 나오질 않는 것을 알 수 있습니다. 이것은 생성자 g는 위에서 next 문을 통해 이미 끝까지 값을 반환했기 때문에 더이상 반환할 것이 없어 for 문이 실행이 되질 않는 것입니다. 이때는 다시 생성자를 만들어 실행해야 합니다.

In [24]: g = (x*x for x in range(1, 11))
   ....: for x in g:
   ....:   print(x)
   ....: 
1
4
9
16
25
36
49
64
81
100

내장함수 sum과 같이 반복가능 객체를 인수로 받는 함수에서 생성자 축약을 인수로 넘겨줄 수 있습니다.

In [25]: h = (x * x for x in range(11))
   ....: sum(h)
   ....: 
Out[25]: 385

또는 직접 인수로 생성자 축약을 대입해도 됩니다. 이때 소괄호는 생략할 수 있습니다.

In [26]: sum( x * x for x in range(11))
Out[26]: 385

거북이 이벤트 함수

거북이는 마우스 클릭 또는 키보드의 키눌림 이벤트를 알아채서 뭔가를 할 수 있는 함수를 제공합니다. 이벤트(event)란 프로그램에 의해서 감지되고 처리될 수 있는 동작이나 사건을 말합니다. 키보드 또는 마우스 또는 휴대폰의 터치스크린 등은 사용자가 상응하는 동작을 하는 순간 이벤트를 감지하여 처리합니다. 키보드를 누르는 순간, 마우스를 움직이거나 클릭하는 순간, 터치스크린을 터치하는 순간에 이벤트가 발생하는 것입니다. 거북이는 사용자가 마우스를 클릭 했을 때 onclick(func)이라는 함수가 실행되어 onclick 인수인 func를 작동시킨다. func는 클릭 한 곳의 좌표 (x, y)를 매개변수로 하는 함수이어야 합니다. onclick(func)함수가 실행될 때, func 함수에 클릭한 곳의 좌표 (x, y)를 인수로 넘겨줍니다. 그러면 func(x, y) 함수가 실행되는 것입니다.

import turtle

 = turtle.Screen()
 = turtle.Pen()

def 글씨쓰기(x, y):
    .pu()
    .goto(x, y)
    문자열 = "(" + str(x) + ', ' + str(y) + ")"
    .write(문자열)

.onclick(글씨쓰기)
.mainloop()
  • 창.onclick(글씨쓰기)를 실행하면 거북이는 마우스가 클릭 되기를 무한정 기다리다가 마우스가 창을 클릭하는 순간 onclick 인수인 글씨쓰기 함수를 부릅니다.

  • 글씨쓰기 함수를 부를 때 클릭한 곳의 x, y 좌표를 글씨쓰기(x, y)에 자동으로 넘겨주어 글씨쓰기 함수가 실행되는 것입니다.

  • onclick 함수는 마우스가 거북이 창을 클릭할 때마다 호출되어 실행됩니다.

  • 창.mainloop() 함수도 창의 종료 이벤트(창의 오른쪽 위 X 누르기 또는 Alt + F4 누르기)를 무한정 기다리는 이벤트 함수입니다.

직접하기

  • 거북이를 이용하여 마우스를 클릭하면 “코딩은 즐거워!”라는 문자열을 클릭한 곳에 출력하는 프로그램을 작성하시오.

  • 거북이를 이용하여 마우스를 클릭하면 클릭한 곳에 한변의 길이가 20인 정사각형을 그리는 프로그램을 작성하시오.

문서화 문자열

파이썬은 문서화 문자열(documentation string)이라고 불리우는, 줄여서 docstring라 불리우는 편리한 기능을 가지고 있습니다. 문서화 문자열은 프로그램을 알아보기 쉽게 해 주고, 또 나중에 프로그램에 대한 설명서를 작성할 때 유용하게 사용될 수 있는 중요한 도구입니다. 아래 예제와 같이, 문서화 문자열은 프로그램이 실행 중일 때도 읽어올 수 있습니다.

def 함수():
    """이것은 함수를 간단히 설명하는 줄입니다.

    한 줄을 띄우고 문서에 대한 설명을 적습니다.
    문서화 문자열은 함수이름() 콜론 바로 다음 줄에 와야 합니다.
    """

    pass

help(함수)
Help on function 함수 in module __main__:

함수()
    이것은 함수를 간단히 설명하는 줄입니다.

     줄을 띄우고 문서에 대한 설명을 적습니다.
    문서화 문자열은 함수이름() 콜론 바로 다음 줄에 와야 합니다.
  • 줄2, 문서화 문자열은 함수이름 선언 바로 다음 줄에서 따옴표 3개를 사용한 문자열을 이용합니다.

  • 줄2, 첫 줄은 보통 한 줄로 적으며 마침표로 끝냅니다.

  • 줄3, 둘째 줄은 빈 줄로 놓습니다.

  • 줄4, 셋째 줄부터 필요한 설명들을 적습니다.

  • 줄8, 문서화 문자열이 끝난 다음 줄부터 함수의 몸통이 시작됩니다.

소행성 게임

거북이를 이용하여 움직이는 소행성과 우주선이 부딪혀 소행성을 파괴하는 게임입니다. 아래 코드는 게이머와 소행성의 움직임만 작성한 것입니다.

import turtle
import random
import math

 = turtle.Screen() # 거북이 창

우주선 = turtle.Pen() # 게이머
우주선.color('blue')
우주선.shape('turtle')
우주선.penup()
우주선.speed(0)

소행성1 = turtle.Pen() # 소행성1
소행성1.color('red')
소행성1.shape('circle')
소행성1.penup()
소행성1.speed(0)
소행성1.goto(random.randint(-300, 0), random.randint(-300, 300))

소행성2 = turtle.Pen() # 소행성2
소행성2.color('red')
소행성2.shape('circle')
소행성2.penup()
소행성2.speed(0)
소행성2.goto(random.randint(-300, 0), random.randint(-300, 300))

def 왼쪽():
    우주선.goto(우주선.pos() + (-10, 0))

def 오른쪽():
    우주선.goto(우주선.pos() + (10, 0))

def ():
    우주선.goto(우주선.pos() + (0, 10))

def 아래():
    우주선.goto(우주선.pos() + (0, -10))

.onkeypress(왼쪽, "Left")
.onkeypress(오른쪽, "Right")
.onkeypress(, "Up")
.onkeypress(아래, "Down")
.listen()

def 놀자():
    소행성1.forward(2)
    소행성2.forward(2)
    .ontimer(놀자, 10)

놀자()

.mainloop()
  • 우주선은 게이머이고 speed(0)함수를 이용하여 움직이는 속도를 가장 빠르게 했습니다. 1이 가장 느리고 숫자가 10까지 올라갈수록 더 빨라진다. 보통 속도는 6이고 가장 빠른 속도는 0입니다.

  • 소행성1.goto(random.randint(-300, 0), random.randint(-300, 300))에서 randint(x, y)을 이용하여 소행성이 시작하는 위치를 x의 위치는 -300에서 0까지, y의 위치는 -300에서 300까지 무작위 수로 설정했습니다.

  • 왼쪽, 오른쪽, , 아래로 이동하는 함수를 onkeypress() 함수에 건네주어 키보드의 왼쪽, 오른쪽, 위, 아래 키가 눌릴 때마다 작동하게 했습니다.

  • 왼쪽, 오른쪽, , 아래로 이동하는 함수안에 우주선.goto(우주선.pos() + (-10, 0))을 이용하여 키가 눌릴 때의 우주선 위치 우주선.pos()+10 또는 -10 만큼을 더해서 우주선를 움직이게 했습니다.

  • 창.ontimer(놀자, 10)에서 ontimer(함수, 시간) 함수는 주어진 시간(ms 단위) 간격으로 함수를 실행합니다. 시간의 단위는 ms(mille second)로 1/1000초를 의미합니다. 따라서 놀자() 함수를 0.01초 간격으로 실행하면서 forward(2)가 실행되어 소행성들이 앞 쪽으로 2만큼 움직입니다.

연습

  1. 섭씨 온도를 화씨 온도로 바꾸는 함수 섭씨2화씨(섭씨)를 작성하고 실행하시오.

  2. 위의 섭씨2화씨(섭씨) 함수의 문서화 문자열을 넣고 help()함수를 이용해서 함수의 설명을 확인해 보시오.

  3. 정사각형() 함수를 만들어 거북이를 이용하여 다음과 같은 그림을 그리시오.

  4. 다음과 같은 그림을 그리시오.

  5. 별의 시작 위치 x, y와 한 변의 길이를 매개변수로 하는 별 그리는 함수를 작성하고 실행하시오. 함수의 이름과 매개변수를 별그리기(x, y, 한변)로 하시오.

  6. 위의 별그리기(x, y, 한변)함수를 이용하여 별의 시작 위치및 한 변의 길이를 무작위 수를 생성하여 10개를 그리시오. 반복문을 이용하시오.

  7. 거북이를 이용하여 키보드 오른쪽 화살표를 누르면 오른쪽으로 회전하여 50만큼 이동하고, 왼쪽 화살표를 누르면 왼쪽으로 회전하여 50만큼 이동하도록 프로그램을 만드시오. 거북이 창 메소드 onkeypress(func, key)listen() 함수를 이용하시오. 여기서 key는 왼쪽은 'Left', 오른쪽은 'Right'입니다.

  8. 거북이를 이용하여 창을 클릭한 곳에 별이 그려지는 프로그램을 작성하시오. onclick() 함수를 이용하시오. 위에서 작성한 별그리기 함수를 수정해서 사용하시오.

  9. 위 소행성 게임 예제에서 소행성과 우주선과의 거리가 20보다 작으면 “꽝!”이라는 문자를 우주선의 위치에 출력하는 함수 사정거리()를 작성하시오. 놀자() 함수에 사정거리() 함수를 넣어 우주선이 소행성 근처에 접근하면 “꽝!”이라는 문자가 출력되게 프로그램을 수정하시오.

  10. 두 점 (x1, y1), (x2, y2)를 사용자로부터 입력받아 두 점을 지나는 직선을 그리는 함수 직선그리기(x1, y1, x2, y2)를 만들고 실행하시오. 그래프 위에 두 점과 두 점의 좌표를 함께 표시하시오.