1 사용자 정의함수

사용자 정의함수(user-defined function)는 매개변수(parameter)를 갖는 함수와 매개변수를 갖는 함수로 크게 나눌 수 있고 함수결과를 반환하는 함수와 함수결과를 반환하지 않는 함수로 크게 나눌 수 있다.

1.1 매개변수를 받지 않는 함수

hello_world() 함수는 인자가 없고 출력결과만 화면에 출력하는 가장 간단한 함수다.

헬로월드!!!

1.2 매개변수를 받는 함수

hello_world() 함수는 인자가 없고 출력결과만 화면에 출력하는 가장 간단한 함수다. f를 문자열 앞에 추가하여 문자열임을 알리고 {name}과 같이 매개변수를 결합시킬 수도 있다.

안녕 정훈.

1.3 결과를 반환하는 함수

add_number() 함수는 매개변수 두개를 인자로 받아 + 연산을 한 후에 결과값(sum_number)을 반환시킨다. 그래서, add_number(10, 20)이 수행된 결과는 result 객체에 저장되고 이를 다시 재사용해서 또다른 연산작업을 수행한 결과를 출력시킨다.

두 숫자 합: 30.
3.0

1.4 다수 결과를 반환하는 함수

매개변수를 2개 이상 전달받을 수 있다. 하지만 반환되는 결과는 하나만 가능했다. 매개변수 2개를 전달받아 사칙연산 작업을 수행하는 결과를 반환하고자 하는 경우 새로운 자료구조가 필요하다. 우선 리스트로 사칙연산 수행결과를 반환하는 함수를 작성해보자. 파이썬에서 리스트는 []으로 생성한다. 사칙연산 작업 결과 각각을 계산 한 후에 리스트로 result 변수에 저장한 후에 print() 함수로 결과를 확인한다.

[15, 5, 50, 2.0]
  • 함수를 구성하는 기본요소
    • 함수 머리(header): def로 함수임을 선언하고, 함수명과 함수인자를 기술, 마지막을 :으로 마무리.
    • 함수 설명: docstring으로 """""" 으로 함수에 대한 도움말을 기술한다. 함수가 하는 역할, 매개변수, 반환되는 값, 예제 등을 넣어 개발자가 봤을 때 피로도가 없도록 작성한다.
    • 함수 몸통(body): 앞서 사칙연산처럼 함수가 수행해야 되는 작업을 기술한다.
    • 반환(return): return 예약어로 함수작업결과 반환되는 값을 지정한다.

1.5 변동되는 인자수를 갖는 함수

만약, 함수의 위치(positional argument) 혹은 인수명(keyword argument)이 인자값이 고정되지 않는 경우는 어떻게 처리할 것인가? 이런 문제에 도입되는 새로운 문법이 *을 함수 인자로 넣는 것이다. 이렇게 정의된 함수는 고정길이를 갖는 인자를 갖지 않더라도 함수 결과를 출력하는 것이 가능하다.

sum_them_all() 함수에 콤마로 구분되는 숫자를 인자값으로 차례로 넣게 되면 인자수가 고정되지 않더라도 처리할 수 있다.

15

두번째로 인수명(keyword argment)를 갖는 경우 ** 두개를 사용해서 함수를 정의하면 되고, 키값(key-value) 짝을 이루는 것이라 이를 풀어서 함수 몸통에서 처리하여 결과를 도출할 수 있다.

sales 딕셔너리를 만들고, 이를 **kwargs 인자로 넣어 주면 sum_up() 함수는 결과값을 반환시켜준다.

70

2 람다 함수

2.1 리스트 원소를 제곱

[1,2,3,4,5]이 담겨있는 리스트 각 원소를 제곱하는 프로그램을 먼저 작성해 보자. 리스트에 담겨있기 때문에 원소 각각을 꺼내서 제곱한 후에 이를 data 리스트 각 원소 위치에 맞춰 갱신해 주면 된다.

[1, 4, 9, 16, 25]

2.2 리스트 제곱 함수

두번째 사례로 함수(square_num())를 만들어서 리스트를 매개변수로 던져 각 원소를 제곱하는 함수로 제작하여 결과를 출력시킨다.

[1, 4, 9, 16, 25]

2.3 람다 함수

먼저 data 리스트를 선언하고 lambda 함수를 사용해서 data 리스트 원소 하나만 제곱하도록 lamdba 함수를 정의힌다.

4

map() 함수는 내부에 인자를 두개 갖는데, 하나는 함수, 다른 하나는 데이터가 된다. 즉 map(function, data) 형태로 요약되는데 lambda num: num * num 무명함수를 정의하게 되면 data 리스트 각 원소를 꺼내 원하는 연산작업을 각 원소별로 수행할 수 있게 된다.

[1, 4, 9, 16, 25]

2.4 람다 함수와 비교

람다 함수는 꼭 필요한 경우 for 루프를 대체하여 사용하는데 대표적인 사용례는 다음과 같다.

  • 콜백(callback)
  • 작은 작업을 함수로 구현하여 처리

2.5 콜백(callback) 함수

콜백(callback)함수는 call-after 함수라고 불리기도 하며 여기에 의미하는 바와 같이 함수의 인자로 함께 불려 전달이 되지만 바로 실행되지 않고 실제 수행은 나중에 되는 함수를 의미한다. caller() 함수가 함수와 함께 인자를 함께 전달받아 가져가지만 실행은 나중에 된다.

이런 간단한 함수의 경우 람다 함수로 구현하게 되면 코드도 단순해지고 직관적으로 바뀐다.

3 오류 처리

함수가 매번 올바르게 처리되는 경우는 없다. 경우에 따라서는 오류가 명백한 경우도 많아 이에 대한 예외처리가 필수적이다. 다음 사례를 살펴보면 log_number() 함수는 숫자를 매개변수로 넘겨받아 상용로그로 변환하는 역할 수행하는데 문자열 Korea가 넘어오게 되면 처리를 할 수 없는데 상식적으로 Korea에 상용로그를 취하게 되는 의미는 무엇을까? 이런 경우 오류가 나오는 것이 당연하다.

1.0
Error in py_call_impl(callable, dots$args, dots$keywords): TypeError: must be real number, not str

Detailed traceback: 
  File "<string>", line 1, in <module>
  File "<string>", line 2, in log_number

먼저 입력받은 매개변수 number가 제대로 된 값인지를 점검하여 오류가 있는 경우 ValueError() 오류처리를 하고, 정상적인 처리가 불가능할 경우 TypeError()를 내서 처리를 한다.

Error in py_call_impl(callable, dots$args, dots$keywords): ValueError: math domain error

Detailed traceback: 
  File "<string>", line 1, in <module>
  File "<string>", line 5, in log_number
Error in py_call_impl(callable, dots$args, dots$keywords): TypeError: '<' not supported between instances of 'str' and 'int'

Detailed traceback: 
  File "<string>", line 1, in <module>
  File "<string>", line 2, in log_number

4 사례 연구

4.1 자료형별로 정렬

문자와 숫자가 뒤섞인 입력값을 받아 문자는 문자대로 숫자는 숫자대로 구분하여 출력시키는 함수를 작성한다.

  1. 임의 입력값이 들어가기 때문에 *args 인자를 함수 인자로 지정한다.
  2. 문자와 숫자 결과를 저장시키는 텅빈 리스트를 각각 생성한다.
  3. 자료형을 확인하는 일반함수 isinstance()를 활용하여 문자인지 숫자인지 판정하고 해당 인자를 문자리스트와 숫자리스트에 각각 저장한다.
  4. 튜플로 결과값을 반환시킨다.
([100, 7, 53], ['한국', '독일', '사랑'], [2.7, 0.9, 3.14])

5 map, filter, reduce

5.1 map() 함수

map()iterable에 대해서 함수를 각 원소별로 적용시킨다고 볼 수 있다.

  • map: [1,2,3,4,5] → 함수(\(x^2\)) → [1,4,9,16,25]

map() 연산을 취하게 되면 결과값이 iterable이라 이를 list() 혹은 for루프로 풀어줘야 한다.

1
4
9
16
25

이를 확장하여 람다 함수로 square_num() 함수를 대체할 수 있다.

[1, 4, 9, 16, 25]

이를 확장하여 벡터 내적을 구하는 사례를 살펴보자. 먼저 수식으로 나타내면 \(\vec{x} = \begin{bmatrix}1&2&3\end{bmatrix}\) 이고 \(\vec{y} = \begin{bmatrix}1\\2\\3\end{bmatrix}\)으로 둘을 곱하게 되면 다음과 같이 표현할 수 있다.

\(\vec{x} \cdot \vec{y} = \begin{bmatrix}1&2&3\end{bmatrix} \times \begin{bmatrix}1\\2\\3\end{bmatrix} = \begin{bmatrix}1&4&9\end{bmatrix}\)

이를 map() 함수로 두 벡터의 내적을 계산하면 다음과 같이 간결하게 표현할 수 있다.

[1, 4, 9]

5.2 filter() 함수

map()iterable에 대해서 함수를 각 원소별로 적용시킨다는 점에서는 동일하지만 조건에 맞는 것을 추려낸다.

  • filter: [-3,-2,-1,0,1,2,3] → 함수(양수) → [1,2,3]

map() 연산과 마찬가지로 결과값이 iterable이라 이를 list() 혹은 for루프로 풀어줘야 한다.

[1, 2, 3]

이를 람다 함수로 작성하면 다음과 같이 간략히 코드를 작성할 수 있다.

[1, 2, 3]

5.3 reduce() 함수

map(), filter()iterable에 대해서 함수를 적용시켜 iterable과 동일한 자료형을 갖는 상수를 만들어 낸다. 유의할 점은 reduce()는 뭔가 줄이기 위해서 두개 인자를 필요로 한다는 점에서 차이가 난다.

  • reduce: [-3,-2,-1,0,1,2,3] → 함수(최대값) → 3
3

마찬가지로 람다 함수로 표현하면 다음과 같다.

3

6 재귀(recursion)

재귀는 반복(for, while) 루프를 제거하여 코드를 더 간결하게 작성할 수 있다. 근본적으로 의사결정나무도 내부적으로 보면 재귀 로직을 취하고 있어 이를 이해하는 것은 데이터 사이언스 코딩에 중요한 역할을 한다. 재귀 프로그래밍은 크게 두가지로 나뉜다. 큰 문제를 유사한 작은 문제로 나누는 것과 함께 무한반복을 막아 탈출할 수 있게 되는 기본 사례로 구성된다.

countdown() 함수 내부에 for루프를 돌려 숫자를 내려서 발사시키는 함수 대신 재귀기능을 넣어 직관적으로 코드를 작성할 수 있다.

발사(Blast off) !!!