# 함수 파이썬과 같은 프로그래밍 언어에서 함수란 수학에서의 함수 의미와 약간 다릅니다. 함수란 프로그램에서 언제든지 어디서든지 실행할 수 있는 문장들을 모아 놓은 집합입니다. ## 수학 함수 표현 섭씨를 화씨로 변환하는 수학 함수는 다음과 같습니다. $$ F(C) = \frac{9}{5} C + 32 $$ 이것을 파이썬 함수로 표현하면 다음과 같습니다. ```{eval-rst} .. ipython:: In [1]: def F(c): ...: return 9 / 5 * c + 32 ``` 섭씨 `c`를 입력받아 화씨값 `F(c)`를 반환하는 프로그램입니다. ### 함수 정의 ```py def funtion_name(param1, param2): body ``` 모든 파이썬 함수는 `def`로 시작하고 함수 이름 소괄호 `()`를 하고 콜론 `:` 으로 머리부분 {sup}`header` 을 정의합니다. 소괄호 안에는 입력 매개변수를 쉼표를 이용하여 나열할 수 있습니다. 다음은 다음 줄에 들여쓰기를 하여 몸통부분 {sup}`body` 을 정의합니다. 몸통 부분에 `return` 문을 이용하여 반환하고자 하는 값을 적습니다. `return` 문을 만나면 함수는 종료하게 됩니다. `return` 뒤에 아무것도 없거나 `return` 문이 없는 함수의 반환값은 `None` 입니다. 몸통 부분을 끝내려면 들여쓰기를 멈춥니다. ### 함수 호출 함수를 사용하려면 정의된 함수를 호출해야합니다. 호출하는 방법은 함수이름과 소괄호를 이용합니다. ```{eval-rst} .. ipython:: In [1]: a = F(10) ...: print(a) ...: print(a + F(30)) ``` 함수를 호출할 때 입력 되는 값을 **인수(argument)**라고 합니다. 반복문을 이용해서 다음과 같이 여러 번 호출하여 리스트를 만들수도 있습니다. ```{eval-rst} .. ipython:: In [1]: Cdegrees = [-15, 0, 10, 20, 30] ...: Fdegrees = [F(c) for c in Cdegrees] ...: Fdegrees ``` 반환값을 문자열로 하는 함수를 다음과 같이 정의할 수 있습니다. ```{eval-rst} .. ipython:: In [1]: def F1(c): ...: Fval = 9 / 5 * c + 32 ...: return f'섭씨: {c}, 화씨: {Fval}' ...: ...: print(F1(20)) ``` ## 프로그램 흐름 이해 프로그램 실행 순서를 이해하는 것이 중요합니다. ```{eval-rst} .. ipython:: In [1]: 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 ``` ## 지역/전역 변수 함수 안에서 정의된 변수를 **지역변수** {sup}`local variable` 이라고 합니다. 지역변수는 함수 밖에서는 접근할 수 없습니다. 함수 `F()` 에서 정의된 변수 `Fval`은 지역변수이고 이것을 함수밖에서 호출하면 에러가 발생합니다. ```{eval-rst} .. ipython:: :verbatim: In [1]: F(30) ...: Fval NameError: name 'Fval' is not defined ``` 반면에 함수 밖에서 정의된 변수를 **전역변수** {sup}`global variable` 이라고 합니다. ```{eval-rst} .. ipython:: In [1]: 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) ``` 위에서 변수 C는 함수 안에서는 지역변수이고 밖에서는 60을 갖는 전역변수입니다. 함수 안에 있는 C는 지역변수로서 전역변수 C를 가리는 {sup}`hide` 역할을 합니다. 함수 안에서 C값을 변경해도 전역변수 C는 영향을 받지 않습니다. 변수 검색 규칙은 먼저 지역변수를 찾고, 없으면 전역변수를 찾습니다. 그래도 못찾으면 내장변수가 있는지를 찾고 없으면 에러를 발생시킵니다. ``` 1. 지역변수 2. 전역변수 3. 내장된 이름 ``` 다음 예제를 살펴봅니다. ```{eval-rst} .. ipython:: In [1]: 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()으로 작동 ``` `global` 키워드를 사용해서 함수 안에서 전역변수의 값을 변경할 수도 있습니다. 하지만 어쩔수없는 경우를 제외하고는 함수안에서 전역변수 값을 변경하는 것을 피해야합니다. ```{eval-rst} .. ipython:: In [1]: 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 출력 ``` ## 여러 개 인수들 함수는 여러 개의 인수들을 취할 수 있습니다. 예를 들어 다음과 같은 함수를 생각해봅니다. $$ y(t) = v_0 t - \frac{1}{2} g t^2 $$ `g`는 상수, $v_0$는 변하는 수라고 가정합니다. $y(t)$는 수학적으로는 `t`에 대한 함수이지만 $v_0$가 변함에 따라 `y` 값이 변하는 것을 알 수 있습니다. 파이썬에서는 이러한 함수를 2개의 인수를 입력받아 정의할 수 있습니다. ```{eval-rst} .. ipython:: In [1]: def yfunc(t, v0): ...: g = 9.8 ...: return v0 * t - 0.5 * g * t ** 2 ``` 함수 호출은 다음과 같이 합니다. ```{eval-rst} .. ipython:: In [1]: yfunc(0.1, 6) ...: yfunc(0.1, v0=6) ...: yfunc(t=0.1, v0=6) ...: yfunc(v0=6, t=0.1) ``` `인수=값` 형태로 인수를 넘겨줄 수도 있습니다. 이러한 인수를 **키워드인수**라고 부릅니다. 이럴 때는 순서가 바뀌어도 상관없습니다. 하지만 일반 인수와 키워드인수가 동시에 포함된 인수들은 키워드인수가 일반 인수 뒤에 위치해 있어야 합니다. 다음은 키워드인수가 일반 인수보다 앞에 있기때문에 에러를 발생합니다. ```{eval-rst} .. ipython:: :okexcept: In [1]: yfunc(v0=6, 0.1) # 에러 ``` ## 수학 함수를 넘어 앞에서는 수학 함수를 파이썬 함수로 표현하는 것을 보았습니다. 파이썬 함수는 수학함수를 넘어 일련의 문장을 반복해서 실행할 필요가 있을 때 사용됩니다. 예를 들어 파이썬 내장 함수 `range`와 비슷한 기능을 하는 함수를 만들어 봅니다. 즉, 시작, 끝, 간격을 입력받아 리스트를 반환하는 함수를 작성해봅니다. ```{eval-rst} .. ipython:: In [1]: 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) ``` `range` 함수는 정수형 리스트만 반환하는 반면 `makelist`는 실수형 리스트도 반환할 수 있습니다. ## 여러 개의 반환값 다음과 같이 함수값 및 그것의 미분값도 동시에 얻고 싶다고 해봅니다. $$ y(t) = v_0 t - \frac{1}{2} g t^2 \\ y'(t) = v_0 - gt $$ $y, y'$ 값을 동시에 반환하려면 튜플 형태로 반환하면 됩니다. ```{eval-rst} .. ipython:: In [1]: def yfunc2(t, v0): ...: g = 9.8 ...: y = v0 * t - 0.5 * g * t ** 2 ...: dydt = v0 - g * t ...: return y, dydt ``` 함수값을 받을 때도 순서대로 쉼표를 이용하여 받습니다. ```{eval-rst} .. ipython:: In [1]: pos, vel = yfunc2(0.6, 3) ...: print(pos, vel) ``` 또는 다음과 같이 튜플 변수에 저장하여 사용할 수도 있습니다. ```{eval-rst} .. ipython:: In [1]: tup = yfunc2(0.6, 3) ...: print(tup) ``` $t$, $y(t)$, $y'(t)$에 대한 표를 for 문을 이용하여 얻을 수 있습니다. ```{eval-rst} .. ipython:: In [1]: 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)) ``` `{:<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} $$ ```{eval-rst} .. ipython:: In [1]: 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 ``` 약간 바꿔서 정확한 오차와 근사 오차를 함께 반환하도록 하겠습니다. ```{eval-rst} .. ipython:: In [1]: 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$에서 오차가 거의 없는 것을 알 수 있습니다. ```{eval-rst} .. ipython:: In [1]: 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)) ``` $x=10$에서 값을 보면 오차가 많아서 값으로 사용할 수 없는 것을 알 수 있습니다. ```{eval-rst} .. ipython:: In [1]: 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)) ``` $x=100$에서는 더욱 더 많이 나는 것을 알 수 있습니다. ```{eval-rst} .. ipython:: In [1]: 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)) ``` 이것은 테일러 다항식을 $x=0$에서 전개했기 때문에 0으로부터 멀리 있는 값은 발산할 수 밖에 없습니다. 이럴 때는 항의 갯수를 더 늘려서 근사값을 사용해야 합니다. 얼마나 많은 차이가 나는지를 알아보기 위해서 함수 자체에서 출력을 하도록 프로그램을 해보도록 합니다. ## `return` 문이 없는 함수 앞에서 정의한 sinSum2 함수를 이용해 함수 안에서 출력을 하도록 새로운 함수 table를 작성하도록 하겠습니다. ```{eval-rst} .. ipython:: In [1]: 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$일 때 각각의 오차를 나열한 것입니다. 오차가 항의 갯수가 증가함에 따라 감소하는 것을 볼 수 있습니다. ```{eval-rst} .. ipython:: In [1]: table(10) ``` `n=100` 이상일 때 근사오차가 실제 오차보다 작은 것으로 나오는 것은 파이썬 실수 표현 방식에서 유효숫자를 나타내는 자릿수가 정해져 있기 때문입니다. `table()` 함수는 `return` 문이 존재하지 않는 함수입니다. `return` 문이 존재하지 않는 함수는 `None`이라는 파이썬 객체를 반환합니다. ```{eval-rst} .. ipython:: In [1]: res = table(10) ...: res == None ``` ## 매개변수 기본값 함수를 정의할 때 매개변수의 기본값을 지정하면 편리할 때가 많습니다. 기본값을 지정하면 해당 인수를 입력하지 않아도 자동으로 기본값이 지정이 됩니다. ```{eval-rst} .. ipython:: In [1]: def somefunc(arg1, arg2, kwarg1=True, kwarg2=0): ...: print(arg1, arg2, kwarg1, kwarg2) ``` 다음과 같이 호출할 수 있습니다. 기본값이 지정된 인수들은 `kwarg1, kwarg2` 입니다. 함수를 호출할 때 기본값이 지정된 매개변수에 대응되는 인수를 생략하면 기본값들이 설정되어 계산됩니다. 다음은 인수를 넣지 않은 경우입니다. ```{eval-rst} .. ipython:: In [1]: somefunc('Hello', [1, 2]) ``` 인수를 하나만 입력한 경우에는 입력하지 않은 다른 인수는 기본값이 설정됩니다. ```{eval-rst} .. ipython:: In [1]: somefunc('Hello', [1, 2], kwarg1='Hi') ``` 인수에 기본값과 다른 객체가 대입되도 상관없는 것을 알 수 있습니다. ```{eval-rst} .. ipython:: In [1]: somefunc('Hello', [1, 2], kwarg2='Hi') ``` 순서를 바꿔 입력해도 상관없습니다. ```{eval-rst} .. ipython:: In [1]: somefunc('Hello', [1, 2], kwarg2='Hi', kwarg1=65) ``` 다음과 같은 함수를 매개변수 기본값을 설정하여 정의할 수 있습니다. $$ f(t; A, a, \omega) = Ae^{-at} \sin (\omega t) $$ ```{eval-rst} .. ipython:: In [1]: 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)$를 구하는 것이됩니다. ```{eval-rst} .. ipython:: In [1]: v1 = f(0.2) ``` 만일 $e^{-1}\sin(\pi)$와 같은 식을 구하려면 다음과 같이 입력하면 됩니다. ```{eval-rst} .. ipython:: In [1]: v2 = f(1, omega=math.pi) ``` 합을 구할 때 기본 임계값을 설정하면 편리합니다. ```{eval-rst} .. ipython:: In [1]: 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 ``` 위 함수는 주어진 임계값보다 근사오차가 작을 때까지 더하기를 계속 합니다. ```{eval-rst} .. ipython:: In [1]: 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)) ``` 다음과 같이 출력됩니다. ```{eval-rst} .. ipython:: In [1]: table2(10) ``` ## 함수 객체 인수 함수도 파이썬 객체이므로 인수로 넘겨줄 수 있습니다. 미적분학 문제를 풀 때에 종종 함수를 인수로 넘겨주면 편리할 때가 많습니다. 예를 들면 - 수치적으로 $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)$로 수렴하는 것을 알 수 있습니다. 위 식을 함수로 정의하면 다음과 같습니다. ```{eval-rst} .. ipython:: In [1]: 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` 입니다. `diff2nd`의 `f`를 함수로 정의해서 넘겨주면 됩니다. 다음은 $g(t) = t^{-6}$ 함수를 파이썬으로 정의한 것입니다. ```{eval-rst} .. ipython:: In [1]: def g(t): ...: return t ** (-6) ``` 수치미분을 적용하면 다음과 같습니다. ```{eval-rst} .. ipython:: In [1]: t = 1.2 ...: d2g = diff2nd(g, t) ...: print('t={:.1f}, d2g={:.5f}'.format(t, d2g)) ``` $t=1$에서 $h \to 0$를 변해가며 미분값을 출력해 봅니다. ```{eval-rst} .. ipython:: In [1]: for k in range(1, 15): ...: h = 10 ** (-k) ...: d2g = diff2nd(g, 1, h) ...: print('h={:.1e}, {:.5f}'.format(h, d2g)) ``` $h < 10^{-8}$ 보다 작으면 계산된 미분값이 발산하는 것을 알 수 있습니다. 이것은 앞에서도 이야기했듯이 파이썬 실수형 자릿수를 정확히 표시할 수 있는 숫자는 $10^{-16}$이기 때문에 이것 보다 작은 수는 부정확한 계산이 되어 믿을 수 없는 계산이 됩니다. 그리고 분자가 값이 0에 가까워서 유효숫자가 상실되기 때문에 부정확한 계산이 됩니다. 따라서 이러한 것을 보완하기 위해서는 유효자릿수를 늘려 계산하는 수밖에 없습니다. 파이썬 `decimal` 모듈이 이러한 것을 해결할 수 있도록 원하는 자릿수에서 계산할 수 있도록 합니다. ## lambda 함수 함수를 한 줄에 정의할 수 있는 파이썬 예약어가 `lambda` 입니다. ```{eval-rst} .. ipython:: :verbatim: In [1]: f = lambda x: x ** 2 + 4 ``` 위 함수는 다음과 똑 같습니다. ```{eval-rst} .. ipython:: :verbatim: In [1]: def f(x): ...: return x ** 2 + 4 ``` 인수가 여러 개일 때는 다음과 같이 정의합니다. ```python3 g = lambda arg1, arg2, ...: 표현식 ``` $g(x, y) = x^y$은 다음과 같이 정의할 수 있습니다. ```{eval-rst} .. ipython:: In [1]: g2 = lambda x, y: x ** y ...: ...: g2(2, 3) ``` 따라서 람다 함수를 이용해서 diff2nd를 이용할 수 있습니다. ```{eval-rst} .. ipython:: In [1]: g = lambda x: x ** (-6) ...: diff2nd(g, 1) ``` ## 생성자(generator) [생성자](https://docs.python.org/3/library/stdtypes.html#generator-types)는 반복자 {sup}`iterator` 의 특수한 한 형태입니다. **생성자 함수**{sup}`generator function` 는 함수 안에 `yield`를 사용하여 값을 반환하는 함수입니다. 생성자 함수가 처음 호출되면, 그 함수 실행 중 처음으로 만나는 `yield` 에서 값을 반환하고 함수 실행을 멈춥니다. 생성자 함수가 다시 호출되면, 직전에 실행되었던 `yield` 문 다음부터 다음 `yield` 문을 만날 때까지 문장들을 실행하고 `yield`에서 실행을 멈추고 값을 반환합니다. 더 이상 실행할 `yield`가 없을 때가지 이러한 과정을 반복합니다. 이러한 생성자 함수를 호출하여 **생성자** {sup}`generator` 객체를 만듭니다. 다음은 간단한 생성자 함수와 그 호출 예를 보인 것입니다. 여기서 `생성함수()` 함수는 **생성자 함수**로서 3개의 `yield` 문을 가지고 있습니다. 따라서 한번 호출시마다 각 `yield` 문에서 실행을 중지하고 값을 리턴하게 됩니다. ```{eval-rst} .. ipython:: In [1]: def 생성함수(): ...: yield 1 ...: yield 2 ...: yield 3 ``` 생성자 함수를 호출하여 **생성자**를 만들어 아래와 같이 변수 `g`에 할당합니다. 그러면 `g`는 생성자 객체가 됩니다. ```{eval-rst} .. ipython:: In [1]: g = 생성함수() ...: type(g) ``` 생성자 호출은 내장함수 `next(생성자)`를 이용할 수 있습니다. `next`를 처음 호출하면 처음으로 만나는 `yield 1`에서 실행을 멈추고 `yield` 뒤에 있는 값 `1`을 반환합니다. ```{eval-rst} .. ipython:: In [1]: print(next(g)) ``` 다시 `next`를 실행하면 `yield 1` 다음 문장 즉, `yield 2`에서 실행을 멈추고 `2`를 반환합니다. ```{eval-rst} .. ipython:: In [1]: print(next(g)) ``` 또 `next`를 실행하면 `yield 2` 다음 문장, `yield 3`에서 실행을 멈추고 `3`을 반환합니다. ```{eval-rst} .. ipython:: In [1]: print(next(g)) ``` 생성자가 더이상 반환할 값이 없으면 `StopIteration` 예외를 발생시킵니다. ```{eval-rst} .. ipython:: :verbatim: In [1]: print(next(g)) ...: --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) in () ----> 1 print(next(g)) StopIteration: ``` `for` 문을 이용해서 자동으로 반복자를 가져오고 `next` 함수를 실행시켜 값을 가져옵니다. ```{eval-rst} .. ipython:: In [1]: for x in 생성함수(): ...: print(x) ``` 리스트나 집합 {sup}`set` 과 같은 자료형에 대한 반복자는 해당 객체가 이미 모든 값을 가지고 있는 경우이나, 생성자는 모든 값을 갖지 않은 상태에서 `yield`에 의해 하나씩만 값을 만들어 가져온다는 차이점이 있습니다. 이러한 생성자는 자료가 매우 커서 모든 자료를 한꺼번에 반환할 수 없는 경우나, 모든 자료를 미리 계산하면 속도가 느려서 필요한 자료 요청시 처리하는 것이 좋은 경우 등에 사용됩니다. ```{eval-rst} .. doit:: #. 구구단 출력하는 프로그램을 생성자 함수 ``구구단()``\ 으로 만들고 ``for``\ 문을 이용해 호출해 보세요. 즉 다음을 실행하면 구구단이 출력되어야 합니다. .. code:: bash 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 ``` ### 생성자 축약 [생성자 식](https://docs.python.org/3/reference/expressions.html#generator-expressions)은 생성자 축약(generator comprehension)으로도 불리우는데, 리스트 축약 {sup}`list comprehension` 과 외관상 유사합니다. 리스트 축약은 앞뒤를 대괄호 `[...]`로 표현한다면, 생성자 축약은 소괄호 `(...)`를 사용합니다. 하지만 생성자 축약은 리스트 축약과 달리 실제 값 전체를 반환하지 않고, 그 표현식만을 갖는 생성자 객체만 반환합니다. 즉 실제 실행은 하지 않고, 표현식만 가지며 위의 `yield` 방식으로 늦은 연산(Lazy Operation)을 수행하는 것입니다. 아래 예제는 1부터 10까지의 숫자에 대한 제곱값을 생성자 축약으로 표현한 것으로 여기서 생성자 축약을 할당받은 변수 `g`는 생성자 타입 객체입니다. ```{eval-rst} .. ipython:: In [1]: g = (x*x for x in range(1, 11)) ``` ```{eval-rst} .. ipython:: In [1]: print(type(g)) ...: ``` `for` 문을 사용하여 10개의 `next()` 문을 실행하여 처음 10개에 대한 제곱값을 출력한 것입니다. ```{eval-rst} .. ipython:: In [1]: for i in range(10): ...: print(next(g)) ``` 또는 다음과 같이 `for`문에 직접 생성자를 넣어서 실행할 수 있습니다. ```{eval-rst} .. ipython:: In [1]: for x in g: ...: print(x) ``` 그런데 위와 같이 실행하면 아무런 결과가 나오질 않는 것을 알 수 있습니다. 이것은 생성자 `g`는 위에서 `next` 문을 통해 이미 끝까지 값을 반환했기 때문에 더이상 반환할 것이 없어 `for` 문이 실행이 되질 않는 것입니다. 이때는 다시 생성자를 만들어 실행해야 합니다. ```{eval-rst} .. ipython:: In [1]: g = (x*x for x in range(1, 11)) ...: for x in g: ...: print(x) ``` 내장함수 `sum`과 같이 반복가능 객체를 인수로 받는 함수에서 생성자 축약을 인수로 넘겨줄 수 있습니다. ```{eval-rst} .. ipython:: In [1]: h = (x * x for x in range(11)) ...: sum(h) ``` 또는 직접 인수로 생성자 축약을 대입해도 됩니다. 이때 소괄호는 생략할 수 있습니다. ```{eval-rst} .. ipython:: In [1]: sum( x * x for x in range(11)) ``` ## 조건문 다음과 같이 조건에 따라 함수를 정의하는 경우에 대해서 알아봅니다. $$ f(x) = \begin{cases} \sin(x), & 0 \le x \le \pi \\ 0, & \text{otherwise} \end{cases} $$ ### if-else 문 일반적인 `if-else` 문은 다음과 같습니다. ```python3 if 조건: 조건이 True일 때 실행되는 구역 else: 조건이 False일 때 실행되는 구역 ``` ```{eval-rst} .. ipython:: In [1]: import math ...: def f(x): ...: if 0 <= x <= math.pi: ...: val = math.sin(x) ...: else: ...: val = 0 ...: return val ``` `else if`는 `elif`로 사용해야 합니다. ```python3 if condition1: elif condition2: elif condition3: else: ``` 다음과 같은 hat 함수 $$ N(x) = \begin{cases} 0, & x < 0 \\ x, & 0 \le x < 1 \\ 2 - x, & 1 \le x < 2 \\ 0, & x \ge 2 \end{cases} $$ ```{eval-rst} .. ipython:: In [1]: 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 ``` 더 짧은 형태로 코들 작성할 수 있습니다. ```{eval-rst} .. ipython:: In [1]: def N(x): ...: if x <= 0 < 1: ...: return x ...: elif 1 <= x < 2: ...: return 2 - x ...: else: ...: return 0.0 ``` ### if 한 줄 표현식 다음과 같은 `if-else` 문장을 한 줄 표현식으로 바꿀 수 있습니다. ```python3 if condition: a = value1 else: a = value2 ``` 조건 {sup}`condition`이 참이면 `value1`을 거짓이면 `value2`를 반환합니다. ```python3 a = (value1 if condition else value2) ``` 여기서 소괄호는 선택사항으로 있어도되고 없어도 되지만 권장사항입니다. 예를 들면, ```python3 def f(x): return (math.sin(x) if 0 <= x <= 2 * math.pi else 0) ``` 이것을 `lambda` 함수를 이용하면 한 줄에 표현할 수 있습니다. ```python3 f = lambda x: math.sin(x) if 0 <= x <= 2 * math.pi else 0 ``` `lambda` 함수 안에 일반적인 `if-else` 문을 사용할 수 없습니다. 람다함수는 한 개의 표현식만 가질 수 있습니다. ## 연습문제 01. 다음 수학 함수를 파이썬 함수 `g(t)` 로 구현하고 `g(0)`, `g(1)`을 구하세요. $$ g(t) = e^{-t} \sin(\pi t) $$ 02. 다음 함수를 파이썬 함수 `h(t, a)`로 작성하시오. `a`를 매개변수 기본값을 갖도록 작성하시오. `a`가 기본값 `10` 일 때 $h(0), h(1)$의 값을 구하시오. $$ h(t) = e^{-at} \sin(\pi t) $$ 03. 다음 프로그램이 작동하는 순서를 자세히 설명하시오. ```python3 def add(A, B): C = A + B return C a = 3 b = 2 print(add(a, b)) print(add(2 * a, b + 1) * 3) ``` 04. 화씨를 섭씨로 변환하는 함수 $$ C = \frac{5}{9}(F - 32) $$ 를 파이썬 함수 `C(F)`로 작성하고 이것의 역함수 `F(C)`도 파이썬 함수로 작성하세요. 그리고 작성한 함수들이 역함수가 맞는지 `F(C(f))`, `C(F(c))`의 값을 확인하여 보세요. 05. $s = \sum_{k=1}^M \frac{1}{k}$의 합 $s$를 구하는 파이썬 함수 `sum1(M)`을 작성하고 `M=3` 일 때 값을 손으로 구한 값과 비교해보세요. 06. 이차방정식 $ax^2 + bx + c =0$의 해를 구하는 파이썬 함수 `roots(a, b, c)`를 작성하세요. 실수근을 가지면 `float` 형을, 복소수근을 가지면 `complex` 형을 반환하도록 작성하세요. 07. $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`는 해 리스트입니다. 08. 계단함수 또는 Heaviside 함수라고 알려진 다음 함수 $$ H(x) = \begin{cases} 0, & x < 0 \\ 1, & x \ge 0 \end{cases} $$ 를 파이썬 함수 `H(x)`로 작성하고 $H(-10), H(-10^{-15}), H(0), H(10^{-15}), H(10)$의 값을 구하세요. 09. 위에서 정의한 Heaviside 함수는 0에서 불연속인 것을 알 수 있습니다. 이러한 것을 극복하기 위해 수정된 미분가능한 Heaviside를 다음과 같이 정의해서 사용하기도 합니다. $$ 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} $$ 위의 수정된 함수 $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`을 갖는 함수를 뜻합니다. $$ I(x; L, R) = \begin{cases} 1, & x \in [L, R] \\ 0, & \text{otherwise} \end{cases} $$ 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$ 일 때에 대해서 값을 확인해 보세요. ## 연습문제 풀이 % .. include:: src/Exercise5.rst