2.12. 내용정리: 12일차
- 파이썬은 객체지향 패러다임만을 제공하는 것이 아니라 다양한 프로그래밍 패러다임을 제공한다.
- 파이썬에서는 수많은 패러다임 중 함수형 패러다임 역시 제공한다.
- 함수형 프로그래밍(functional programming) 은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다.
- 객체지향 프로그래밍은 객체를 중심적으로 사고하는 기법이라면, 함수형 프로그래밍은 거의 모든 것을 순수 함수로 나누어 문제를 해결하는 기법 이다.
- 작은 문제를 해결하기 위한 함수를 작성하여 가독성을 높이고 유지 보수를 용이하게 해준다.
- 파이썬에는 여러 가지 형태의 함수들이 존재한다.
- 앞서 파이썬에는 함수형 패러다임을 제공한다고 서술했었다.
- 함수형 패러다임에서 사용되는 몇 가지 함수 패턴들이 존재한다.
- 람다 함수는 이전에도 배웠던 개념이다.
- 함수의 명칭을 지정하지 않아도 된다는 점에서 익명 함수(Anonymous Function) 라고도 한다.
- 함수를 간단히 한줄만으로 만들게 해준다는 점에서 람다식(Lambda Expression) 이라고도 한다.
- 주로 함수를 사용할 수 없는 경우나 간단한 함수를 인자값으로 넘길 때 사용된다.
f = lambda x: x + 1
f(10)
재귀 함수란, 정의한 함수에 같은 함수를 호출하여 반복하는 결과를 만들어내는 함수를 의미한다.
아래는 팩토리얼(factorial) 연산을 수행하는 함수 factorial
을 정의한 것이다.
def factorial(n):
# 'n'이 1보다 클 경우에는 n과 factorial 함수를 다시 호출한 결과값을 곱셈한다.
if n > 1:
return n * factorial(n - 1)
# 'n'이 1과 같거나 작다면 1을 반환시킨다.
else:
return 1
x = factorial(5)
print(x)
위의 코드가 수행되는 과정은 다음과 같다.
- 함수
factorial
이 호출된다. - 인자값은
5
이므로,n
은5
가 된다. -
5
는1
보다 크기 때문에5 * factorial(5 - 1)
이return
키워드에 의해 반환된다. - 반환됨과 동시에
factorial(5 - 1)
이 호출되므로,factorial(5 - 1)
을 수행한다. -
n
은4
이므로, 다시4 * factorial(4 - 1)
이 수행된다. - 위와 같은 과정이 계속 반복이 되다보면 결국
n
은1
이 된다. -
n
이1
이므로,1
이 반환된다. - 최종적으로
5 * 4 * 3 * 2 * 1
을 반환하게 된다. - 따라서
x
는120
이 된다.
- 재귀 함수의 성능은 좋지 않은 편이다.
- 위의 코드가 수행되는 과정 중 스택 이라는 메모리 영역에 계속 쌓이는 개념이다.
- 스택(Stack) 이란, 마지막에 들어온 요소가 가장 먼저 빠져 나오는 구조(LIFO; Last-In First-Out)를 지닌 자료 구조를 의미한다.
- 컴퓨터 메모리 구조는 스택이라는 영역이 존재하는데, 이 스택이라는 영역에 호출된 함수를 차곡차곡 쌓아 올리면서 기억한다.
- 쌓아올린 스택을 다시 회귀하면서 연산하는 과정이 지나치게 많아지게 되면 속도와 성능이 저하되는데, 이를 스택 오버헤드(Stack Overhead) 라고 부른다.
- 또한, 스택이 계속 쌓여서 더 이상 쌓을 공간이 부족해 메모리 공간을 초과하게 되면, 스택 오버플로우(Stack Overflow) 에러가 발생한다.
즉, 위의 재귀 함수가 호출되는 과정을 좀 더 상세하게 나타내면 다음과 같다.
- 함수
factorial
이 호출된다. - 메모리의 스택 영역에 호출된 함수를 기억하기 위해 호출된 함수를 쌓아 올린다.
- 인자값은
5
이므로,n
은5
가 된다. -
5
는1
보다 크기 때문에5 * factorial(5 - 1)
이return
키워드에 의해 반환된다. - 반환됨과 동시에
factorial(5 - 1)
이 호출되므로,factorial(5 - 1)
을 수행한다. - 위의 과정을 계속 반복한다.
- 메모리의 스택 영역에 새롭게 호출된
factorial(5 - 1)
를 쌓아 올린다. - 이 과정이 계속 반복되면 총 5번 쌓이게 된다. (
n
이5
이므로 5번 쌓인다. 만약n
이10
이면 총 10번 호출하게 되므로, 10번 쌓인다.) - 가장 마지막에 호출된 함수
factorial(2 - 1)
이 스택에서 먼저 빠져 나온다. (LIFO) - 위와 같은 과정을 반복하면서 곰셈 연산을 위해 자신이 호출되었던 위치를 찾기 위해 회귀하는 과정을 거친다.
- 최종적으로
1 * 2 * 3 * 4 * 5
를 연산한 값이 반환된다. - 따라서
x
는120
이 된다.
- 만약
n
값이100
이었다면,100
번 호출하게 되고, 스택 영역에서 다시100
번 빠져나오면서 곱셈 연산까지 수행해야 한다. - 따라서 이렇게 단순한 재귀 함수는 성능이 좋지 못하다는 한계점이 있다.
- 위에서 살펴본 재귀 함수의 한계점을 보완하기 위한 일종의 기법이다.
- 꼬래 재귀 함수의 원리는 일반적인 재귀 함수처럼 추가적인 연산 과정을 하지 않는다.
- 대신, 연산해야 하는 값을 자체적으로 기억하는 방식이다.
- 단, 컴파일러(Compiler) 차원에서 꼬리 재귀의 최적화를 지원해야 한다는 조건이 있다.
- 컴파일러(Compiler) 란, 프로그래밍 언어를 기계어로 바꿔주는 일종의 통역사 역할을 해주는 것이다.
- 파이썬같은 스크립트 언어같은 경우, 컴파일러라는 말 대신에 인터프리터(Interpreter), 즉 해석기라고 표현한다.
-
C
나C++
, 그리고Python
역시 컴파일러 차원에서 최적화를 지원한다. -
JAVA
같은 경우, 컴파일러 차원에서 자체적인 지원은 하지 않는다고 한다.
def factorial(n, i=1):
if n > 1:
return factorial(n - 1, n * i)
else:
return i
- 일반 재귀 함수와의 차이점은 매개 변수가 두 개로 늘어났다는 점이다.
- 요점은 재귀 호출 이후 추가적인 연산을 요구하지 않도록 구현하는 것이다.
- 즉, 스택 영역에 쌓아 올린 후, 추가적인 곱셈 연산을 다시 한번 하지 않는다는 것에 있다.
- 함수 호출 이후에 쌓아 올려진 스택을 빠져 나와 회귀하는 과정에서 추가적인 연산을 하지 않음으로, 오버헤드를 줄일 수 있다.
- 퍼스트 클래스 함수란, 프로그래밍 언어가 함수를 일급 객체(first-class object 혹은 일등 시민; first-class citizen 라고도 함) 으로 취급하는 것을 의미한다.
- 함수 자체를 인자(argument)로써 다른 함수에 전달하거나 다른 함수의 결과값으로 반환할 수도 있다.
- 혹은 함수를 변수에 할당하거나 자료 구조안에 저장할 수 있는 함수를 의미한다.
- 즉, 변수에 담을 수 있고, 함수의 인자로 전달하고 함수의 반환값(return value)으로 전달할 수 있는 함수를 의미한다.
- 일급 객체란, 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.
- 보통 함수에 인자로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 한다.
- 변수나 자료 구조 안에 담을 수 있다.
- 파라미터로 전달 할 수 있다.
- 반환값으로 사용할 수 있다.
- 할당에 사용된 이름과 무관하게 고유한 구별이 가능하다.
함수를 인자값으로 받는 함수 f
를 선언한다.
# 퍼스트 클래스 함수 'f' 선언
def f(x):
# 여기서 파라미터 'x'는 함수이다.
# 함수를 호출할 때에는 괄호를 사용한다.
print("함수 'f' 호출")
x()
그 다음, 함수 'test' 호출
이라는 메시지를 호출하는 함수 test
를 선언한다.
def test():
print("함수 'test' 호출")
함수 f
에 인자값으로 함수 test
를 넣어주고 호출한다.
f(test)
-
콜백 함수란, 다른 함수의 인자로써 넘겨진 후(퍼스트 클래스 함수) 특정 이벤트에 의해 호출되는 함수를 의미한다.
- 이벤트(event) 는 말 그대로 어떤 사건을 의미한다.
- 간단한 예시로, 키보드의 특정한 키를 입력하거나, 마우스 클릭과 같은 이벤트 등이 있다.
- 주로 넘겨받는 인자값의 함수는 람다식으로 전달된다.
- 콜백 함수를 실행하는 함수를 이벤트를 조율하는 함수라는 의미의 이벤트 핸들러(Event Handler) 라고 한다.
- 아래의 예시에서 이벤트 핸들러 함수
f
를 정의하였다. - 이때, 함수
f
의 인자값으로 받는x
는 콜백 함수이며,y
는 정수를 받는다. -
y
가0
보다 크면 함수x
를 호출한 결과값을 반환한다. -
y
가0
보다 작으면'y'의 값이 0보다 작습니다.
라는 문자열을 반환한다.
# 이벤트 핸들러 함수 'f' 정의
def f(x, y):
# 여기서 인자값으로 넘겨진 'x'는 함수이며, 이를 '콜백 함수'라고 한다.
if y > 0:
return x(y)
else:
return "'y'의 값이 0보다 작습니다."
그 뒤, 아래의 코드를 실행해본다.
n = input('수를 입력하세요:')
if n:
n = int(n)
y = f(lambda x: x * 2, n)
print(y)
- 특정한 이벤트가 발생했을 때(위의 코드같은 경우, 수를 입력받았을 때) 아래의 함수가 수행된다.
- 만약 사용자로부터 입력받은 결과가 존재하는 경우, 즉
None
이 아닐 경우, 이벤트 핸들러 함수f
를 호출한다. - 함수
f
는 퍼스트 클래스 함수이자, 이벤트 핸들러이다. - 인자값으로 넘겨받은 람다식
lambda x: x * 2
는 어떤 수에2
를 곱셈 연산을 하는 함수이다.
- 중첩 함수란, 말 그대로 함수 내에 또 다른 함수를 의미한다.
- 함수 내부에 선언된 함수이므로, 내부 함수(Inner function) 라고도 한다.
- 기존함수를 한번 감싸서 원래 동작에 약간의 처리를 추가하는 함수를 의미하는 래퍼 함수(Wrapper function) 라고도 한다.
아래는 outer
라는 함수 안에 inner
라는 내부 함수를 선언하는 예시이다.
def outer():
print("외부 함수 영역")
def inner():
print("내부 함수 영역")
# 내부 함수 'inner' 호출
inner()
# 함수 'outer' 호출
outer()
- 이전에 배웠던
global
이라는 키워드는 함수 외부에 선언되어있는 변수(즉, 전역 변수)를 사용하기 위한 키워드이다. -
nonlocal
키워드는global
키워드와는 다르게, 중첩 함수의 관계에서, 내부 함수가 외부 함수의 지역 변수의 값을 다시 할당하려고 할 때 쓰이는 키워드이다.
아래의 코드를 실행하면 UnboundLocalError
라는 에러가 발생한다.
def outer():
a = 10
def inner():
a += 10
print('a:', a)
inner()
# 함수 'outer'를 호출하면 아래와 같은 에러를 발생시킨다.
# UnboundLocalError: local variable 'a'
outer()
- 에러가 발생하는 이유는 할당하기 전에 지역 변수
a
가 이미 참조되었기 때문이다. - 즉, 변수
a
가 외부 함수의 지역 변수로써의a
인지, 내부 함수의 지역 변수로써의a
인지를 구별하지 못하고 있는 것이다.- 이전에 공부했었던 이름 공간이라는 개념을 생각해보면 된다.
- 이러한 문제를 해결하기 위해서 변수
a
를 아래와 같이nonlocal
로 선언하면 된다.
def outer():
print("외부 함수 영역")
a = 10
# 내부 함수 'inner' 호출
def inner():
print("내부 함수 영역")
# nonlocal 키워드를 사용하여 함수 'outer' 영역에 선언된 변수 'a'를 사용하겠다는 의미이다.
nonlocal a
a += 10
print('a:', a)
# 내부 함수 'inner' 호출
inner()
# 함수 'outer' 호출
outer()
- 클로저 함수란, 어떤 함수를 함수 자신이 가지고 있는 환경과 함께 저장하는 함수이다.
- 또한 함수가 가진 프리 변수(free variable) 를 클로저 함수가 만들어지는 당시의 값과 참조된 값들을 맵핑(mapping)해주는 역할을 한다.
- 파이썬에서 프리 변수란, 코드 블럭안에서 사용은 되었지만, 그 코드 블럭안에서 정의되지 않은 변수를 의미한다.
- 클로저 함수는 일반 함수와는 다르게, 자신의 영역 밖에서 호출된 함수의 변수값과 참조된 값들을 복사하고 저장한 뒤, 이 값들에 접근할 수 있게 도와준다.
- 즉, 간단히 말해서 클로저 함수란, 자신이 가지고 있는 환경에 맞춰진 형태로 반환해주는 함수이다.
아래는 클로저 함수 outer
와 내부 함수인 inner
를 실행시킨 결과값을 반환하는 예시이다.
# 클로저 함수 'outer' 정의
def outer():
# 함수 'outer' 영역의 변수 'msg'
msg = 'Hi'
# 내부 함수 'inner' 정의
def inner():
# 내부 함수 'inner' 역시 함수 'outer' 영역에 있다.
# 재선언을 하는 것이 아니기 때문에 함수 'outer' 영역의 변수 'msg'임을 알 수 있다.
# 따라서 지역 변수 'msg'를 참조할 수 있다. (프리 변수)
print(msg)
# 함수 'inner'를 호출하면서 실행 결과를 반환시킨다.
# 반환되는 값이 없으므로, 결과값은 'None'이 반환된다.
return inner()
# 클로저 함수 'outer' 호출
outer()
다음과 같이 함수를 반환시키는 함수를 만들어서 응용할 수도 있다.
# 클로저 함수 'f' 정의
def f(x):
# 내부 함수 'g' 정의
def g(y):
# 클로저 함수 'f'의 파라미터 'x'의 값과 내부 함수 'g'의 파라미터 'y'의 값을 더해준다.
return x + y
# 내부 함수 'g'를 반환한다.
return g
# 함수 'f'는 함수를 반환시키는 함수이므로, 아래와 같이 호출할 수 있다.
# 아래와 같은 방식을 마치 사슬처럼 엮여있는 모양이라고 해서 체이닝(chaining) 기법이라고 한다.
f(1)(2)
위와 같은 방식을 마치 사슬처럼 엮여있는 모양이라고 해서 체이닝(chaining) 기법이라고 한다.
- 데코레이터란, 함수와 메서드를 장식(decorate)하는 문법적인 요소이다.
- 사용할 때에는
@
기호를 붙여서 사용한다.
이해하기 쉽게 정의하자면 다음과 같다.
- 이전에
lambda
함수같은 경우, 함수의 간단한 표현 방식이라고 서술하였다. - 데코레이터는 함수를 인자로 받는 함수, 즉 퍼스트 클래스 함수를 문법적인 요소로 간편하게 표현한 것이다.
- 함수를 인자로 받는 함수를 선언한다.
- 함수 안에 또다른 함수(내부 함수)를 정의한다.
- 새롭게 정의한 내부 함수를 반환한다.
- 이렇게 정의한 함수를
@
기호를 붙여@함수명
으로 새롭게 정의한 함수 윗줄에 추가한다.
먼저, 아래의 예시처럼 함수를 인자로 받는 클로저 함수 f
를 정의한다.
def f(x):
print("함수 'f' 호출")
# 내부 함수 'g'를 정의한다.
def g(y):
print("함수 'g' 호출")
# 함수를 인자로 받았기 때문에 'function' 타입이 출력된다.
print(x)
print(type(x))
# 인자값으로 받은 함수 'x'에 함수 'g'의 인자값 'y'를 넘긴다.
return x(y)
# 내부 함수 'g'를 반환시킨다.
return g
그 다음 새롭게 정의할 함수 윗줄에 @
기호를 붙여 다음과 같이 정의한다.
@f
def f2(x):
print("함수 'f2' 호출")
return x + 1
그 다음 아래의 코드를 실행시켜본다.
y = f2(10)
print(y)
위의 코드를 실행하면 결과는 다음과 같이 출력된다.
함수 'f' 호출
함수 'g' 호출
<function f2 at 0x.....>
<class 'function'>
함수 'f2' 호출
11
과정은 다음과 같다.
- 함수
f
에 인자값으로x
가 넘어가며,x
는 함수이다. - 내부 함수
g
에 인자값으로 받은 함수x
가 넘어간다. - 내부 함수
g
에서 인자값으로 넘긴 함수x
에 내부 함수g
의 인자값y
를 넘긴다. - 내부 함수
g
는 최종적으로 인자값으로 넘긴 함수x
를 실행시킨 결과값을 반환시킨다. - 결과적으로, 함수
f
는 위의 과정을 거쳐 만들어진 내부 함수g
를 반환한다. - 함수
f
가 호출된다. - 함수
f
의 인자값x
로f2
가 넘어간다. - 내부 함수
g
에서 인자값으로 받은 함수f2
에 인자값y
를 넘기면서 함수f2
를 호출시킨 후, 그 결과값을 반환되도록 만들어진다. - 이렇게 만들어진 내부 함수
g
를 반환한다. - 내부 함수
g
가 호출된다. - 최종적으로, 내부 함수
g
는 함수f2
를 호출한다.
과정을 전부 풀어서 보면 매우 복잡해 보이지만, 알고 보면 사실 굉장히 단순하다.
매개 변수와 반환값을 처리해주는 데코레이터 역시 정의하는 게 가능하다.
# 호출할 함수를 매개 변수로 받는다.
def trace(func):
# 호출할 함수 'add(a, b)'의 매개 변수와 똑같이 지정한다.
def wrapper(a, b):
# func에 매개 변수 'a'와 'b'를 넣어서 호출하고 반환값을 변수에 저장한다.
result = func(a, b)
# 매개 변수와 반환값 출력
print(f'{func.__name__}(a={a}, b={b}) -> {result}'
return result
# 위에서 정의한 내부 함수 'wrapper'를 반환한다.
return wrapper
# @데코레이터
@trace
# 매개 변수는 'a'와 'b' 총 두 개이다.
def add(a, b):
return a + b
result = add(10, 20)
print(result)
결과
add(a=10, b=20) -> 30
30
- 함수
add
함수를 호출했을 때 데코레이터를 통해서 매개 변수와 반환값이 출력되었다. - 매개 변수와 반환값을 처리하는 데코레이터를 만들 때는 먼저 내부에 정의한 함수
wrapper
의 매개 변수를 호출할 함수add(a, b)
의 매개 변수와 똑같이 만들어준다. - 함수
wrapper
안에서는func
를 호출하고, 반환값을 변수에 저장한다. - 그 다음,
print
로 매개 변수와 반환값을 출력한다. - 이때
func
에는 매개 변수a
와b
를 그대로 넣어준다. - 또한, 함수
add
는 두 수를 더해서 반환해야 하므로func
의 반환값을return
키워드로 반환해준다. - 데코레이터를 사용할 때는
@
기호로 함수 위에 지정해주면 된다. - 또한,
@
기호로 데코레이터를 사용했으므로, 함수add
는 그대로 호출해준다.
인자값의 개수가 정해지지 않는 가변 인수를 받는 함수 역시 데코레이터로 받는 것이 가능하다.
# 호출할 함수를 매개 변수로 받는다.
def trace(func):
# 가변 인수 함수를 정의한다.
def wrapper(*args, **kwargs):
# 'func'에 가변 인자 'args'와, 키워드 가변 인자 'kwargs'를 언패킹하여 넣어준다.
result = func(*args, **kwargs)
print(f'{func.__name__}(args={args}, kwargs={kwargs}) -> {result}'
return wrapper
@trace
def get_max(*args):
return max(args)
@trace
def get_min(**kwargs):
return min(kwargs.values())
print(get_max(10, 20))
print(get_min(x=10, y=20, z=30))
결과
get_max(args=(10, 20), kwargs={}) -> 20
20
get_min(args=(), kwargs={'x': 10, 'y': 20, 'z': 30}) -> 10
10
- 함수에는 데코레이터를 여러 개 지정할 수 있다.
- 다음과 같이 함수 위에 데코레이터를 여러 줄로 지정해준다.
- 이때, 데코레이터가 실행되는 순서는
위 -> 아래
순이다.
@데코레이터1
@데코레이터2
def 함수명():
코드
def decorator1(func):
def wrapper():
print('decorator1')
func()
return wrapper
def decorator2(func):
def wrapper():
print('decorator2')
func()
return wrapper
# 데코레이터를 여러 개 지정
@decorator1
@decorator2
def hello():
print('hello')
hello()
결과
decorator1
decorator2
hello
@
기호를 사용하여 데코레이터로 정의하지 않았을 때는 다음 코드와 동작이 같다.
decorated_hello = decorator1(decorator2(hello))
decorated_hello()
- 클래스로 데코레이터를 직접 정의할 수 있다.
- 특히 클래스를 활용할 때는 인스턴스를 함수처럼 호출하게 해주는 매직 메서드
__call__
을 구현해야 한다.
아래는 클래스를 통해 데코레이터를 정의하는 예시이다.
class Trace:
# 호출할 함수를 인스턴스의 초기값으로 받는다.
def __init__(self, func):
# 호출할 함수를 속성 'func'에 할당한다.
self.func = func
def __call__(self):
# '__name__'으로 함수 이름을 출력한다.
print(self.func.__name__, '함수 시작')
# 속성 'func'에 저장된 함수를 호출한다.
self.func()
print(self.func.__name__, '함수 끝')
# 정의한 데코레이터를 사용한다.
@Trace
def hello():
print('hello')
# 함수를 그대로 호출한다.
hello()
결과
hello 함수 시작
hello
hello 함수 끝
클래스로 만든 데코레이터도 매개변수와 반환값을 처리할 수 있다.
- 아래는 함수의 매개 변수를 출력하는 데코레이터를 정의한 예시이다
- 여기서는 위치 인수와 키워드 인수를 모두 처리하는 가변 인수로 만들었다.
class Trace:
# 호출할 함수를 인스턴스의 초기값으로 받는다.
def __init__(self, func):
# 호출할 함수를 속성 'func'에 할당한다.
self.func = func
# 호출할 함수의 매개변수를 처리한다.
def __call__(self, *args, **kwargs):
# 'self.func'에 매개 변수를 받아서 호출하고 반환값을 변수에 할당한다.
result = self.func(*args, **kwargs)
print(f'{self.func.__name__}(args={args}, kwargs={kwargs}) -> {result}')
@Trace
def add(a, b):
return a + b
print(add(10, 20))
print(add(a=10, b=20))
결과
add(args=(10, 20), kwargs={}) -> 30
30
add(args=(), kwargs={'a': 10, 'b': 20}) -> 30
30
- 클래스로 매개변수와 반환값을 처리하는 데코레이터를 만들 때는 매직 메서드
__call__
에 매개 변수를 지정한다. - 그 뒤에
self.func
에 매개 변수를 받아서 호출한 뒤에 반환값을 반환해주면 된다. - 위의 예제에서는 매개 변수를
*args
,**kwargs
로 지정했으므로,self.func
으로 받을때는 언패킹하여 넣어준다. - 물론 가변 인수를 사용하지 않고, 고정된 매개 변수를 사용할 때는
def __call__(self, a, b):
처럼 정의해도 된다.
마찬가지로, 클래스로 매개 변수가 있는 데코레이터를 정의할 수 있다.
class IsMultiple:
# 데코레이터가 사용할 매개 변수를 초기값으로 받는다.
def __init__(self, x):
# 매개변수를 속성 x에 저장
self.x = x
# 호출할 함수를 매개변수로 받는다.
def __call__(self, func):
# 호출할 함수의 매개변수와 똑같이 지정한다.
# 물론, 가변 인수로 작성해도 가능하다.
def wrapper(a, b):
# 인자로 받은 함수 'func'를 호출하고 반환값을 변수에 할당한다.
result = func(a, b)
# 인자로 받은 함수 'func'의 반환값이 'self.x'의 배수인지 확인한다.
if result % self.x == 0:
print(f'{func.__name__}의 반환값은 {self.x}의 배수입니다.')
else:
print(f'{func.__name__}의 반환값은 {self.x}의 배수가 아닙니다.')
# 인자로 받은 함수 'func'의 반환값을 반환한다.
return result
# 내부 함수 'wrapper'를 반환한다.
return wrapper
# 데코레이터(인수)
@IsMultiple(3)
def add(a, b):
return a + b
print(add(10, 20))
print(add(2, 5))
결과
add의 반환값은 3의 배수입니다.
30
add의 반환값은 3의 배수가 아닙니다.
7
지금까지 생성자 __init__
에서 호출할 함수를 매개 변수로 받았는데, 여기서는 데코레이터가 사용할 매개변수를 받는다.
위에서 살펴본 퍼스트 클래스 함수와 클로저 함수 등을 활용한 다양한 기법들이 존재하며, 파이썬에서 이를 지원하는 대표적인 세 가지 함수가 존재한다.
-
map
함수는 순환 가능한(이터러블; iterable) 객체에 있는 모든 요소(element)에 인자값으로 넘겨받은 함수를 적용하여 그 결과를 반환한다.- 순환 가능한 객체, 즉 이터러블(iterable) 객체는 컨테이너 타입 중 인덱싱과 슬라이싱이 가능한 객체를 의미한다.
- 이때 함수는 여러 인자값을 받을 수 있어야 하고, 모든 이터러블 객체의 요소에 동시에 적용되도록 해야 한다.
- 즉, 정리하자면
map
함수는 어떤 순환 가능한 객체(대표적으로 리스트와 같은 컨테이너 타입)에 함수를 적용시킨 결과가 나오도록 해주는 함수이다. - 사용하는 방법은
map(순환 가능한 객체의 요소 하나를 인자로 받는 함수, 순환 가능한 객체)
이다.
아래는 map
함수를 사용해 순환 가능한 객체인 리스트 l
에 있는 요소들을 모두 1
씩 증가시키는 예시이다.
def f(x):
return x + 1
l = [1, 2, 3, 4, 5]
y = map(f, l)
# 출력하면 'map' 객체가 출력된다.
print(z)
map
함수를 통해 나온 결과인 map
타입의 객체는 자체적으로 사용이 불가능하기 때문에 타입을 변환시켜서 사용해야 한다.
z = list(y)
# 출력하면 모든 요소가 하나씩 더해진 '[2, 4, 6, 8, 10]'이 출력된다.
print(z)
위의 예시를 lambda
를 활용하면 훨씬 더 간단히 표현할 수 있다.
l = [1, 2, 3, 4, 5]
y = list(map(lambda x: x + 1, l))
print(y)
-
filter
함수는 이터러블 객체의 각 요소에 대해 함수의 결과값이True
를 반환하는 요소만을 추려내는 함수이다. - 즉, '필터'라는 이름에서 알 수 있듯, 어떤 조건(함수)에 만족하는 경우에 해당하는 요소들만을 추출해주는 함수이다.
- 사용하는 방법은
map
함수와 동일하게filter(함수, 순환 가능한 객체)
와 같이 사용한다.- 여기서 인자값으로 받는 함수의 결과값은 참과 거짓을 판단하는 함수가 들어간다.
아래는 filter
함수를 활용해서 리스트 l
안에 있는 요소들 중에서 짝수에 해당하는 요소들만 가져오는 예제이다.
def f(x):
return x % 2 == 0
l = [1, 2, 3, 4, 5]
y = filter(f, l)
# 출력하면 'filter' 객체가 나온다.
print(y)
# 따라서 'map' 함수처럼 타입을 변환시켜서 사용해야 한다.
z = list(y)
# 짝수에 해당하는 값인 '[2, 4]'가 출력된다.
print(z)
마찬가지로, lambda
키워드를 통해 좀 더 축약하여 표현할 수 있다.
l = [1, 2, 3, 4, 5]
y = list(filter(lambda x: x % 2 == 0, l))
print(y)
- 파이썬이 3.x 버전으로 업데이트가 진행되면서
reduce
함수가 파이썬의 기본적인 내장 함수에서 빠졌다고 한다. - 대신 파이썬에 내장된
functools
이라는 모듈(라이브러리)를import
해서 사용할 수 있다.-
reduce
함수를 사용하기 위해서는from functools import reduce
를 통해 사용할 수 있다.
-
-
reduce
함수는 순환 가능한 객체의 각 요소를 왼쪽부터 오른쪽 방향으로 함수를 적용시키며 하나의 값으로 합쳐진 결과를 반환시켜준다. - 사용하는 방법은
reduce(함수, 순환 가능한 객체, 초기값<생략 가능>)
이다.
아래는 reduce
함수를 활용해서 1
부터 100
까지 모두 더해 결과를 얻는 예시이다.
# reduce 함수를 사용하기 위해 아래의 코드를 추가한다.
from functools import reduce
def f(x, y):
return x + y
l = [i for i in range(1, 101)]
# 함수의 결과값이 나오므로, 타입을 변환시킬 필요는 없다.
y = reduce(f, l)
print(y)
마찬가지로, lambda
를 통해 축약된 표현이 가능하다.
from functools import reduce
l = [i for i in range(1, 101)]
y = reduce(lambda x, y: x + y, l)
print(y)
초기값을 지정해주면 순환 가능한 객체안에 요소가 아무것도 없는 경우, 그 초기값을 반환시켜준다.
from functools import reduce
# 요소가 하나도 없는 빈 리스트이다.
l = []
# 초기값을 1로 지정한다.
y = reduce(lambda x, y: x + y, l, 1)
# 인자값으로 받은 리스트가 비어있으므로, y의 값은 초기값으로 지정한 1이 된다.
print(y)
위의 예시에서도 알 수 있듯, reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
라고 한다면, ((((1+2)+3)+4)+5)
의 값을 반환하는 원리이다.
위에서 살펴본 개념들을 토대로 장점들을 정리해보자면 다음과 같이 정리할 수 있다.
- 프로그래밍 코드를 수학처럼 수식화시켜서 표현하는데 좋다.
- 표현이 간결해지므로 코드가 짧아진다.
- 따라서 가독성이 좋아지고 유지 보수하기가 보다 용이해진다.
- 작은 문제들(위의 예제들에서도 살펴보았듯, 단순 반복하는 작업을 통해 결과를 내놓는 문제들)을 해결하기에 적합하다.
- 위의 예제들에서도 살펴보았듯, 불필요한 반복문들이 많이 생략된다.
- 자료 구조(컨테이너 타입)를 제자리에서 수정하여 해결한다.
- 함수 자체가 독립적이므로 프로그램을 동작시키는데 안전성을 보장받을 수 있다.
- 이는 쓰레드(Thread)와 같이 동시성 프로그래밍이나 멀티 프로그래밍 환경에서 빛을 발한다.
- (이 개념에 대해 잘 모르겠지만, 한번에 여러 동작을 수행해야 하는 환경에서 유리하다고 이해했다.)