함수

파이썬과 같은 프로그래밍 언어에서 함수란 수학에서의 함수 의미와 약간 다릅니다. 함수란 프로그램에서 언제든지 어디서든지 실행할 수 있는 문장들을 모아 놓은 집합입니다.

수학 함수 표현

섭씨를 화씨로 변환하는 수학 함수는 다음과 같습니다.

\[F(C) = \frac{9}{5} C + 32\]

이것을 파이썬 함수로 표현하면 다음과 같습니다.

In [1]: def F(c):
   ...:   return 9 / 5 * c + 32
   ...: 

섭씨 c를 입력받아 화씨값 F(c)를 반환하는 프로그램입니다.

함수 정의

def funtion_name(param1, param2):
  body

모든 파이썬 함수는 def로 시작하고 함수 이름 소괄호 ()를 하고 콜론 : 으로 머리부분 header 을 정의합니다. 소괄호 안에는 입력 매개변수를 쉼표를 이용하여 나열할 수 있습니다. 다음은 다음 줄에 들여쓰기를 하여 몸통부분 body 을 정의합니다. 몸통 부분에 return 문을 이용하여 반환하고자 하는 값을 적습니다. return 문을 만나면 함수는 종료하게 됩니다. return 뒤에 아무것도 없거나 return 문이 없는 함수의 반환값은 None 입니다. 몸통 부분을 끝내려면 들여쓰기를 멈춥니다.

함수 호출

함수를 사용하려면 정의된 함수를 호출해야합니다. 호출하는 방법은 함수이름과 소괄호를 이용합니다.

In [2]: a = F(10)
   ...: print(a)
   ...: print(a + F(30))
   ...: 
50.0
136.0

함수를 호출할 때 입력 되는 값을 인수(argument)라고 합니다.

반복문을 이용해서 다음과 같이 여러 번 호출하여 리스트를 만들수도 있습니다.

In [3]: Cdegrees = [-15, 0, 10, 20, 30]
   ...: Fdegrees = [F(c) for c in Cdegrees]
   ...: Fdegrees
   ...: 
Out[3]: [5.0, 32.0, 50.0, 68.0, 86.0]

반환값을 문자열로 하는 함수를 다음과 같이 정의할 수 있습니다.

In [4]: def F1(c):
   ...:   Fval = 9 / 5 * c + 32
   ...:   return f'섭씨: {c}, 화씨: {Fval}'
   ...: 
   ...: print(F1(20))
   ...: 
섭씨: 20, 화씨: 68.0

프로그램 흐름 이해

프로그램 실행 순서를 이해하는 것이 중요합니다.

In [5]: def F(c):
   ...:   Fval = 9 / 5 * c + 32
   ...:   return Fval
   ...: 
   ...: dc = 10
   ...: c = -30
   ...: while c < 50:
   ...:   print(f'섭씨:{c:5.1f}, 화씨: {F(c):5.1f}')
   ...:   c += dc
   ...: 
섭씨:-30.0, 화씨: -22.0
섭씨:-20.0, 화씨:  -4.0
섭씨:-10.0, 화씨:  14.0
섭씨:  0.0, 화씨:  32.0
섭씨: 10.0, 화씨:  50.0
섭씨: 20.0, 화씨:  68.0
섭씨: 30.0, 화씨:  86.0
섭씨: 40.0, 화씨: 104.0

지역/전역 변수

함수 안에서 정의된 변수를 지역변수 local variable 이라고 합니다. 지역변수는 함수 밖에서는 접근할 수 없습니다.

함수 F() 에서 정의된 변수 Fval은 지역변수이고 이것을 함수밖에서 호출하면 에러가 발생합니다.

In [6]: F(30)
   ...: Fval
   ...: 
        NameError: name 'Fval' is not defined

반면에 함수 밖에서 정의된 변수를 전역변수 global variable 이라고 합니다.

In [7]: def F2(C):
   ...:   Fval = 9 / 5 * C + 32
   ...:   print('함수 F2() 안: C: {}, Fval: {}, r: {}'.format(C, Fval, r))
   ...:   return Fval
   ...: 
   ...: C = 60
   ...: r = 21
   ...: F2(r)
   ...: 
함수 F2() 안: C: 21, Fval: 69.80000000000001, r: 21
Out[7]: 69.80000000000001

위에서 변수 C는 함수 안에서는 지역변수이고 밖에서는 60을 갖는 전역변수입니다. 함수 안에 있는 C는 지역변수로서 전역변수 C를 가리는 hide 역할을 합니다. 함수 안에서 C값을 변경해도 전역변수 C는 영향을 받지 않습니다.

변수 검색 규칙은 먼저 지역변수를 찾고, 없으면 전역변수를 찾습니다. 그래도 못찾으면 내장변수가 있는지를 찾고 없으면 에러를 발생시킵니다.

1. 지역변수
2. 전역변수
3. 내장된 이름

다음 예제를 살펴봅니다.

In [8]: print('내장함수(built-in function):', sum)          # 내장 함수 sum
   ...: sum = 500           # 전역변수 sum 정의
   ...: print('전역초기값:', sum)          # 전역 sum
   ...: 
   ...: def myfunc(n):
   ...:   sum = n + 1       # 지역변수 sum 정의
   ...:   print('지역변수:', sum)
   ...:   return sum
   ...: 
   ...: sum = myfunc(2) + 1 # 전역변수 sum
   ...: print('전역변수:', sum)
   ...: del sum             # 전역변수 sum 삭제 -> 내장함수 sum()으로 작동
   ...: 
내장함수(built-in function): <built-in function sum>
전역초기값: 500
지역변수: 3
전역변수: 4

global 키워드를 사용해서 함수 안에서 전역변수의 값을 변경할 수도 있습니다. 하지만 어쩔수없는 경우를 제외하고는 함수안에서 전역변수 값을 변경하는 것을 피해야합니다.

In [9]: a = 20; b = -2.5   # 전역변수들
   ...: def f1(x):
   ...:   a = 21           # 지역변수
   ...:   return a * x + b
   ...: 
   ...: print('함수 실행전 a:', a) # 20 출력
   ...: 
   ...: def f2(x):
   ...:   global a         # 전역변수 a 사용
   ...:   a = 21           # 전역변수 a 값 변경
   ...:   return a * x + b
   ...: 
   ...: f1(3)
   ...: print('함수 f1(3) 실행 후 a:', a) # 20 출력
   ...: f2(3)
   ...: print('함수 f2(3) 실행 후 a:', a) # 21 출력
   ...: 
함수 실행전 a: 20
함수 f1(3) 실행 후 a: 20
함수 f2(3) 실행 후 a: 21

여러 개 인수들

함수는 여러 개의 인수들을 취할 수 있습니다. 예를 들어 다음과 같은 함수를 생각해봅니다.

\[y(t) = v_0 t - \frac{1}{2} g t^2\]

g는 상수, \(v_0\)는 변하는 수라고 가정합니다. \(y(t)\)는 수학적으로는 t에 대한 함수이지만 \(v_0\)가 변함에 따라 y 값이 변하는 것을 알 수 있습니다. 파이썬에서는 이러한 함수를 2개의 인수를 입력받아 정의할 수 있습니다.

In [10]: def yfunc(t, v0):
   ....:   g = 9.8
   ....:   return v0 * t - 0.5 * g * t ** 2
   ....: 

함수 호출은 다음과 같이 합니다.

In [11]: yfunc(0.1, 6)
   ....: yfunc(0.1, v0=6)
   ....: yfunc(t=0.1, v0=6)
   ....: yfunc(v0=6, t=0.1)
   ....: 
Out[11]: 0.551

인수=값 형태로 인수를 넘겨줄 수도 있습니다. 이러한 인수를 키워드인수라고 부릅니다. 이럴 때는 순서가 바뀌어도 상관없습니다. 하지만 일반 인수와 키워드인수가 동시에 포함된 인수들은 키워드인수가 일반 인수 뒤에 위치해 있어야 합니다.

다음은 키워드인수가 일반 인수보다 앞에 있기때문에 에러를 발생합니다.

In [12]: yfunc(v0=6, 0.1) # 에러
  File "<ipython-input-12-eaaa44c187b0>", line 1
    yfunc(v0=6, 0.1) # 에러
                   ^
SyntaxError: positional argument follows keyword argument

수학 함수를 넘어

앞에서는 수학 함수를 파이썬 함수로 표현하는 것을 보았습니다. 파이썬 함수는 수학함수를 넘어 일련의 문장을 반복해서 실행할 필요가 있을 때 사용됩니다.

예를 들어 파이썬 내장 함수 range와 비슷한 기능을 하는 함수를 만들어 봅니다. 즉, 시작, 끝, 간격을 입력받아 리스트를 반환하는 함수를 작성해봅니다.

In [13]: def makelist(start, stop, inc):
   ....:   value = start
   ....:   result = []
   ....:   while value <= stop:
   ....:     result.append(value)
   ....:     value = value + inc
   ....:   return result
   ....: 
   ....: mylist = makelist(0, 1, 0.2)
   ....: print(mylist)
   ....: 
[0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0]

range 함수는 정수형 리스트만 반환하는 반면 makelist는 실수형 리스트도 반환할 수 있습니다.

여러 개의 반환값

다음과 같이 함수값 및 그것의 미분값도 동시에 얻고 싶다고 해봅니다.

\[\begin{split}y(t) = v_0 t - \frac{1}{2} g t^2 \\ y'(t) = v_0 - gt\end{split}\]

\(y, y'\) 값을 동시에 반환하려면 튜플 형태로 반환하면 됩니다.

In [14]: def yfunc2(t, v0):
   ....:   g = 9.8
   ....:   y = v0 * t - 0.5 * g * t ** 2
   ....:   dydt = v0 - g * t
   ....:   return y, dydt
   ....: 

함수값을 받을 때도 순서대로 쉼표를 이용하여 받습니다.

In [15]: pos, vel = yfunc2(0.6, 3)
   ....: print(pos, vel)
   ....: 
0.03599999999999981 -2.88

또는 다음과 같이 튜플 변수에 저장하여 사용할 수도 있습니다.

In [16]: tup = yfunc2(0.6, 3)
   ....: print(tup)
   ....: 
(0.03599999999999981, -2.88)

\(t\), \(y(t)\), \(y'(t)\)에 대한 표를 for 문을 이용하여 얻을 수 있습니다.

In [17]: t_vals = [0.05 * i for i in range(10)]
   ....: for t in t_vals:
   ....:   pos, vel = yfunc2(t, v0=5)
   ....:   print('t={:<10g} pos={:<10g} velocity={:<10g}'.format(t, pos, vel))
   ....: 
t=0          pos=0          velocity=5         
t=0.05       pos=0.23775    velocity=4.51      
t=0.1        pos=0.451      velocity=4.02      
t=0.15       pos=0.63975    velocity=3.53      
t=0.2        pos=0.804      velocity=3.04      
t=0.25       pos=0.94375    velocity=2.55      
t=0.3        pos=1.059      velocity=2.06      
t=0.35       pos=1.14975    velocity=1.57      
t=0.4        pos=1.216      velocity=1.08      
t=0.45       pos=1.25775    velocity=0.59      

{:<10g}은 왼쪽 정렬 10자리 표현식입니다.

합계산

\(\sin x\) 함수의 근사값을 구하는 함수를 작성하려고 합니다.

\[\sin(x) = x - \frac{1}{3!} x^3 + \frac{1}{5!} x^5 \cdots\]

\(x\)에서 \(n\)항까지 더했을 때의 근사값을 구하려고 합니다.

\[\text{sinSum}(x; n) = \sum_{k=1}^n \frac{(-1)^{k+1}}{(2k-1)!} x^{2k-1}\]
In [18]: def sinSum(x, n):
   ....:   import math
   ....:   sum = 0
   ....:   sign = 1
   ....:   for i in range(1, n+1):
   ....:     sum += sign * x ** (2 * i - 1) / math.factorial(2 * i -1)
   ....:     sign *= -1
   ....:   return sum
   ....: 

약간 바꿔서 정확한 오차와 근사 오차를 함께 반환하도록 하겠습니다.

In [19]: def sinSum2(x, n):
   ....:   import math
   ....:   sum = 0
   ....:   sign = 1
   ....:   for i in range(1, n+1):
   ....:     sum += sign * x ** (2 * i - 1) / math.factorial(2 * i -1)
   ....:     sign *= -1
   ....:   approx_error = sign * x ** (2 * n + 1) / math.factorial(2 * n + 1)
   ....:   exact_error = math.sin(x) - sum
   ....:   return sum, approx_error, exact_error
   ....: 

함수를 호출합니다. 다음과 \(x=1\)에서 오차가 거의 없는 것을 알 수 있습니다.

In [20]: x = 1
   ....: val, approx_error, exact_error = sinSum2(x, 10)
   ....: print('합: {:20.18f}\n근사오차: {:12.10E}\n오차: {:12.10E}'.format(val, approx_error, exact_error))
   ....: 
합: 0.841470984807896505
근사오차: 1.9572941063E-20
오차: 0.0000000000E+00

\(x=10\)에서 값을 보면 오차가 많아서 값으로 사용할 수 없는 것을 알 수 있습니다.

In [21]: x = 10
   ....: val, approx_error, exact_error = sinSum2(x, 10)
   ....: print('합: {:20.18f}\n근사오차: {:12.10E}\n오차: {:12.10E}'.format(val, approx_error, exact_error))
   ....: 
합: -16.811850137411582295
근사오차: 1.9572941063E+01
오차: 1.6267829027E+01

\(x=100\)에서는 더욱 더 많이 나는 것을 알 수 있습니다.

In [22]: x = 100
   ....: val, approx_error, exact_error = sinSum2(x, 10)
   ....: print('합: {:20.18f}\n근사오차: {:12.10E}\n오차: {:12.10E}'.format(val, approx_error, exact_error))
   ....: 
합: -794697857233433001984.000000000000000000
근사오차: 1.9572941063E+22
오차: 7.9469785723E+20

이것은 테일러 다항식을 \(x=0\)에서 전개했기 때문에 0으로부터 멀리 있는 값은 발산할 수 밖에 없습니다. 이럴 때는 항의 갯수를 더 늘려서 근사값을 사용해야 합니다.

얼마나 많은 차이가 나는지를 알아보기 위해서 함수 자체에서 출력을 하도록 프로그램을 해보도록 합니다.

return 문이 없는 함수

앞에서 정의한 sinSum2 함수를 이용해 함수 안에서 출력을 하도록 새로운 함수 table를 작성하도록 하겠습니다.

In [23]: def table(x):
   ....:   import math
   ....:   print('x={}, sin(x)={}'.format(x, math.sin(x)))
   ....:   for n in [1, 2, 10, 100, 200]:
   ....:     val, approx, error = sinSum2(x, n)
   ....:     print('n={:5d} 합: {:< 15.4e} 근사오차: {:< 15.4e} 오차: {:< 15.4e}'.format(n, val, approx, error))
   ....: 

출력 포맷 {:< 15.4e}에서 <는 좌측 정렬을 의미하고 다음에 나오는 스페이스(빈 칸)는 숫자의 부호가 양이면 한 칸 띄워서 음일 때와 자리를 마추는 역할을 합니다.

다음은 \(x=10\)일 때 각각의 오차를 나열한 것입니다. 오차가 항의 갯수가 증가함에 따라 감소하는 것을 볼 수 있습니다.

In [24]: table(10)
x=10, sin(x)=-0.5440211108893698
n=    1 합:  1.0000e+01     근사오차: -1.6667e+02     오차: -1.0544e+01    
n=    2 합: -1.5667e+02     근사오차:  8.3333e+02     오차:  1.5612e+02    
n=   10 합: -1.6812e+01     근사오차:  1.9573e+01     오차:  1.6268e+01    
n=  100 합: -5.4402e-01     근사오차:  6.3083e-177    오차: -9.9809e-14    
n=  200 합: -5.4402e-01     근사오차:  0.0000e+00     오차: -9.9809e-14    

n=100 이상일 때 근사오차가 실제 오차보다 작은 것으로 나오는 것은 파이썬 실수 표현 방식에서 유효숫자를 나타내는 자릿수가 정해져 있기 때문입니다.

table() 함수는 return 문이 존재하지 않는 함수입니다. return 문이 존재하지 않는 함수는 None이라는 파이썬 객체를 반환합니다.

In [25]: res = table(10)
   ....: res == None
   ....: 
x=10, sin(x)=-0.5440211108893698
n=    1 합:  1.0000e+01     근사오차: -1.6667e+02     오차: -1.0544e+01    
n=    2 합: -1.5667e+02     근사오차:  8.3333e+02     오차:  1.5612e+02    
n=   10 합: -1.6812e+01     근사오차:  1.9573e+01     오차:  1.6268e+01    
n=  100 합: -5.4402e-01     근사오차:  6.3083e-177    오차: -9.9809e-14    
n=  200 합: -5.4402e-01     근사오차:  0.0000e+00     오차: -9.9809e-14    
Out[25]: True

매개변수 기본값

함수를 정의할 때 매개변수의 기본값을 지정하면 편리할 때가 많습니다. 기본값을 지정하면 해당 인수를 입력하지 않아도 자동으로 기본값이 지정이 됩니다.

In [26]: def somefunc(arg1, arg2, kwarg1=True, kwarg2=0):
   ....:   print(arg1, arg2, kwarg1, kwarg2)
   ....: 

다음과 같이 호출할 수 있습니다. 기본값이 지정된 인수들은 kwarg1, kwarg2 입니다.

함수를 호출할 때 기본값이 지정된 매개변수에 대응되는 인수를 생략하면 기본값들이 설정되어 계산됩니다.

다음은 인수를 넣지 않은 경우입니다.

In [27]: somefunc('Hello', [1, 2])
Hello [1, 2] True 0

인수를 하나만 입력한 경우에는 입력하지 않은 다른 인수는 기본값이 설정됩니다.

In [28]: somefunc('Hello', [1, 2], kwarg1='Hi')
Hello [1, 2] Hi 0

인수에 기본값과 다른 객체가 대입되도 상관없는 것을 알 수 있습니다.

In [29]: somefunc('Hello', [1, 2], kwarg2='Hi')
Hello [1, 2] True Hi

순서를 바꿔 입력해도 상관없습니다.

In [30]: somefunc('Hello', [1, 2], kwarg2='Hi', kwarg1=65)
Hello [1, 2] 65 Hi

다음과 같은 함수를 매개변수 기본값을 설정하여 정의할 수 있습니다.

\[f(t; A, a, \omega) = Ae^{-at} \sin (\omega t)\]
In [31]: import math
   ....: def f(t, A=1, a=1, omega=2 * math.pi):
   ....:   return A * math.exp(-a * t) * math.sin(omega * t)
   ....: 

다음과 같이 불러서 사용합니다. 아래는 \(e^{-0.2}\sin(2\pi \cdot 0.2)\)를 구하는 것이됩니다.

In [32]: v1 = f(0.2)

만일 \(e^{-1}\sin(\pi)\)와 같은 식을 구하려면 다음과 같이 입력하면 됩니다.

In [33]: v2 = f(1, omega=math.pi)

합을 구할 때 기본 임계값을 설정하면 편리합니다.

In [34]: def sinSum3(x, epsilon=1.0E-6):
   ....:   import math
   ....:   sign = 1
   ....:   n = 1
   ....:   term = x ** (2 * n - 1) / math.factorial(2 * n -1)
   ....:   sum = term
   ....:   while term > epsilon:
   ....:     n += 1
   ....:     sign *= -1
   ....:     term = x ** (2 * n - 1) / math.factorial(2 * n - 1)
   ....:     sum += sign * term
   ....:   return sum, n
   ....: 

위 함수는 주어진 임계값보다 근사오차가 작을 때까지 더하기를 계속 합니다.

In [35]: def table2(x):
   ....:   for k in range(4, 14, 2):
   ....:     epsilon = 10 ** (-k)
   ....:     approx, n = sinSum3(x, epsilon)
   ....:     exact = math.sin(x)
   ....:     error = exact - approx
   ....:     print('epsilon: {:.1e}, error: {:< .2e}, n={:3d}'.format(epsilon, error, n))
   ....: 

다음과 같이 출력됩니다.

In [36]: table2(10)
epsilon: 1.0e-04, error:  6.80e-07, n= 18
epsilon: 1.0e-06, error: -4.62e-08, n= 19
epsilon: 1.0e-08, error: -1.58e-10, n= 21
epsilon: 1.0e-10, error: -4.71e-13, n= 23
epsilon: 1.0e-12, error: -8.40e-14, n= 24

함수 객체 인수

함수도 파이썬 객체이므로 인수로 넘겨줄 수 있습니다. 미적분학 문제를 풀 때에 종종 함수를 인수로 넘겨주면 편리할 때가 많습니다.

예를 들면

  • 수치적으로 \(f(x) = 0\)의 해를 구할 때

  • 수치적으로 미분 \(f'(x)\)를 구할 때

  • 수치적으로 적분 \(\int_a^b f(x) dx\)를 구할 때

  • 수치적으로 미분방정식 \(\frac{df}{dt} = f(x)\)를 풀 때

등 함수 \(f(x)\)에 따라 원하는 답이 달라지기 때문에 함수를 인수로 넘겨줘야 합니다.

이차 미분을 예들 들어 봅니다. 이차 미분은 다음과 같이 수치적으로 미분할 수 있습니다.

\[f''(x) \approx \frac{f(x-h) - 2f(x) + f(x+h)}{h^2}\]

여기서 \(h\)는 작은 수입니다. 우변은 \(h \to 0\) 이면 \(f''(x)\)로 수렴하는 것을 알 수 있습니다.

위 식을 함수로 정의하면 다음과 같습니다.

In [37]: def diff2nd(f, x, h=1.E-6):
   ....:   r = (f(x-h) - 2 * f(x) + f(x+h)) / h ** 2
   ....:   return r
   ....: 

여기서 f는 미분할 함수이고 x\(f(x)\)x 입니다.

diff2ndf를 함수로 정의해서 넘겨주면 됩니다.

다음은 \(g(t) = t^{-6}\) 함수를 파이썬으로 정의한 것입니다.

In [38]: def g(t):
   ....:   return t ** (-6)
   ....: 

수치미분을 적용하면 다음과 같습니다.

In [39]: t = 1.2
   ....: d2g = diff2nd(g, t)
   ....: print('t={:.1f}, d2g={:.5f}'.format(t, d2g))
   ....: 
t=1.2, d2g=9.76780

\(t=1\)에서 \(h \to 0\)를 변해가며 미분값을 출력해 봅니다.

In [40]: for k in range(1, 15):
   ....:   h = 10 ** (-k)
   ....:   d2g = diff2nd(g, 1, h)
   ....:   print('h={:.1e}, {:.5f}'.format(h, d2g))
   ....: 
h=1.0e-01, 44.61504
h=1.0e-02, 42.02521
h=1.0e-03, 42.00025
h=1.0e-04, 42.00000
h=1.0e-05, 41.99999
h=1.0e-06, 42.00074
h=1.0e-07, 41.94423
h=1.0e-08, 47.73959
h=1.0e-09, -666.13381
h=1.0e-10, 0.00000
h=1.0e-11, 0.00000
h=1.0e-12, -666133814.77509
h=1.0e-13, 66613381477.50939
h=1.0e-14, 0.00000

\(h < 10^{-8}\) 보다 작으면 계산된 미분값이 발산하는 것을 알 수 있습니다. 이것은 앞에서도 이야기했듯이 파이썬 실수형 자릿수를 정확히 표시할 수 있는 숫자는 \(10^{-16}\)이기 때문에 이것 보다 작은 수는 부정확한 계산이 되어 믿을 수 없는 계산이 됩니다. 그리고 분자가 값이 0에 가까워서 유효숫자가 상실되기 때문에 부정확한 계산이 됩니다.

따라서 이러한 것을 보완하기 위해서는 유효자릿수를 늘려 계산하는 수밖에 없습니다. 파이썬 decimal 모듈이 이러한 것을 해결할 수 있도록 원하는 자릿수에서 계산할 수 있도록 합니다.

lambda 함수

함수를 한 줄에 정의할 수 있는 파이썬 예약어가 lambda 입니다.

In [41]: f = lambda x: x ** 2 + 4

위 함수는 다음과 똑 같습니다.

In [42]: def f(x):
   ....:   return x ** 2 + 4
   ....: 

인수가 여러 개일 때는 다음과 같이 정의합니다.

g = lambda arg1, arg2, ...: 표현식

\(g(x, y) = x^y\)은 다음과 같이 정의할 수 있습니다.

In [43]: g2 = lambda x, y: x ** y
   ....: 
   ....: g2(2, 3)
   ....: 
Out[43]: 8

따라서 람다 함수를 이용해서 diff2nd를 이용할 수 있습니다.

In [44]: g = lambda x: x ** (-6)
   ....: diff2nd(g, 1)
   ....: 
Out[44]: 42.000736222291835

생성자(generator)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

In [51]: 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 [52]: g = (x*x for x in range(1, 11))
In [53]: print(type(g))
   ....: 
<class 'generator'>

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

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

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

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

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

In [56]: 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 [57]: h = (x * x for x in range(11))
   ....: sum(h)
   ....: 
Out[57]: 385

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

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

조건문

다음과 같이 조건에 따라 함수를 정의하는 경우에 대해서 알아봅니다.

\[\begin{split}f(x) = \begin{cases} \sin(x), & 0 \le x \le \pi \\ 0, & \text{otherwise} \end{cases}\end{split}\]

if-else 문

일반적인 if-else 문은 다음과 같습니다.

if 조건:
  조건이 True일  실행되는 구역
else:
  조건이 False일  실행되는 구역
In [59]: import math
   ....: def f(x):
   ....:   if 0 <= x <= math.pi:
   ....:     val = math.sin(x)
   ....:   else:
   ....:     val = 0
   ....:   return val
   ....: 

else ifelif로 사용해야 합니다.

if condition1:
  <block of statements>
elif condition2:
  <block of statements>
elif condition3:
  <block of statements>
else:
  <block of statements>
<next statement>

다음과 같은 hat 함수

\[\begin{split}N(x) = \begin{cases} 0, & x < 0 \\ x, & 0 \le x < 1 \\ 2 - x, & 1 \le x < 2 \\ 0, & x \ge 2 \end{cases}\end{split}\]
In [60]: def N(x):
   ....:   if x < 0:
   ....:     return 0.0
   ....:   elif 0 <= x < 1:
   ....:     return x
   ....:   elif 1 <= x < 2:
   ....:     return 2 - x
   ....:   elif x >= 2:
   ....:     return 0.0
   ....: 

더 짧은 형태로 코들 작성할 수 있습니다.

In [61]: def N(x):
   ....:   if x <= 0 < 1:
   ....:     return x
   ....:   elif 1 <= x < 2:
   ....:     return 2 - x
   ....:   else:
   ....:     return 0.0
   ....: 

if 한 줄 표현식

다음과 같은 if-else 문장을 한 줄 표현식으로 바꿀 수 있습니다.

if condition:
  a = value1
else:
  a = value2

조건 condition이 참이면 value1을 거짓이면 value2를 반환합니다.

a = (value1 if condition else value2)

여기서 소괄호는 선택사항으로 있어도되고 없어도 되지만 권장사항입니다.

예를 들면,

def f(x):
  return (math.sin(x) if 0 <= x <= 2 * math.pi else 0)

이것을 lambda 함수를 이용하면 한 줄에 표현할 수 있습니다.

f = lambda x: math.sin(x) if 0 <= x <= 2 * math.pi else 0

lambda 함수 안에 일반적인 if-else 문을 사용할 수 없습니다. 람다함수는 한 개의 표현식만 가질 수 있습니다.

연습문제

  1. 다음 수학 함수를 파이썬 함수 g(t) 로 구현하고 g(0), g(1)을 구하세요.

    \[g(t) = e^{-t} \sin(\pi t)\]
  2. 다음 함수를 파이썬 함수 h(t, a)로 작성하시오. a를 매개변수 기본값을 갖도록 작성하시오. a가 기본값 10 일 때 \(h(0), h(1)\)의 값을 구하시오.

    \[h(t) = e^{-at} \sin(\pi t)\]
  3. 다음 프로그램이 작동하는 순서를 자세히 설명하시오.

    def add(A, B):
      C = A + B
      return C
    
    a = 3
    b = 2
    print(add(a, b))
    print(add(2 * a, b + 1) * 3)
    
  4. 화씨를 섭씨로 변환하는 함수

    \[C = \frac{5}{9}(F - 32)\]

    를 파이썬 함수 C(F)로 작성하고 이것의 역함수 F(C)도 파이썬 함수로 작성하세요. 그리고 작성한 함수들이 역함수가 맞는지 F(C(f)), C(F(c))의 값을 확인하여 보세요.

  5. \(s = \sum_{k=1}^M \frac{1}{k}\)의 합 \(s\)를 구하는 파이썬 함수 sum1(M)을 작성하고 M=3 일 때 값을 손으로 구한 값과 비교해보세요.

  6. 이차방정식 \(ax^2 + bx + c =0\)의 해를 구하는 파이썬 함수 roots(a, b, c)를 작성하세요. 실수근을 가지면 float 형을, 복소수근을 가지면 complex 형을 반환하도록 작성하세요.

  7. \(n+1\)개의 해 \(r_0, r_1, \ldots, r_n\)을 갖는 \(n+1\) 차 다항식은 다음과 같이 구할 수 있습니다.

    \[p(x) = \prod_{i=0}^n (x - r_i) = (x-r_0)(x-r_1) \cdots (x -r_n).\]

    이것에 대한 파이썬 함수 poly(x, roots)를 작성하시오. roots는 해 리스트입니다.

  8. 계단함수 또는 Heaviside 함수라고 알려진 다음 함수

    \[\begin{split}H(x) = \begin{cases} 0, & x < 0 \\ 1, & x \ge 0 \end{cases}\end{split}\]

    를 파이썬 함수 H(x)로 작성하고 \(H(-10), H(-10^{-15}), H(0), H(10^{-15}), H(10)\)의 값을 구하세요.

  9. 위에서 정의한 Heaviside 함수는 0에서 불연속인 것을 알 수 있습니다. 이러한 것을 극복하기 위해 수정된 미분가능한 Heaviside를 다음과 같이 정의해서 사용하기도 합니다.

    \[\begin{split}H_{\epsilon}(x) = \begin{cases} 0, & x < -\epsilon \\ \frac{1}{2} + \frac{x}{2\epsilon} + \frac{1}{2\pi}\sin\left( \frac{\pi x}{\epsilon} \right), & -\epsilon \le x \le \epsilon \\ 1, & x > \epsilon \end{cases}\end{split}\]

    위의 수정된 함수 \(H_\epsilon(x)\)를 파이썬 함수 H_eps(x, eps=0.01)으로 구현하고 \(x < -\epsilon\), \(x = -\epsilon\), \(x = 0\), \(x = \epsilon\), \(x > \epsilon\) 각각의 경우에 대해서 값을 확인해보세요.

  10. 많은 응용 문제에서 다음과 같은 indicator 함수를 사용합니다. indicator 함수란 주어진 구간 \([L, R]\)에서는 1이고 나머지에서는 0을 갖는 함수를 뜻합니다.

    \[\begin{split}I(x; L, R) = \begin{cases} 1, & x \in [L, R] \\ 0, & \text{otherwise} \end{cases}\end{split}\]

    indicator 함수를 파이썬 함수 indicator(x, L=-1, R=1)로 직접 작성해보세요. 그리고 indicator 함수는 다음과 같이 Heaviside 함수들을 이용해서도 구할 수 있습니다.

    \[I(x; L, R) = H(x - L) H(R - x)\]

    Heaviside 함수들의 곱으로 정의한 것을 파이썬 함수 indicator2(x, L=-1, R=1)로 작성하고 두 함수를 비교해 보세요. \(x < L\), \(x = L\), \(x = (L + R)/2\), \(x = R\), \(x > R\) 일 때에 대해서 값을 확인해 보세요.

연습문제 풀이