python에는 composition이라는 개념이 있다. composition은 functional programming 관점과 객체지향 관점에서 의미하는 것이 다르다.
합성함수는 의미하는 바가 명확하니 넘어가고, 객체지향에서 말하는 composition에 대해 코드로 알아보자
# 객체지향에서의 composition
class A:
x = 1
def __init__(self, a):
self.a = a
class AA:
x = 1
def __init__(self, a):
self.a = a
class B:
def __init__(self):
self.b = A(3) # composition 기법!!!
self.bb = AA(5) # composition 기법!!!
def cool(self):
return self.b + self.bb
class B에서는 class A와 AA에 대한 객체를 각각 만들어 __init__에서 초기화 한 후 사용한다. 이렇게 객체지향에서는 한 클래스에 대한 인스턴스를 다른 클래스에서 사용하는 경우를 composition 기법이라고 이야기 한다.
composition 기법을 사용하면 상속을 직접적으로 사용하지 않아도 상속을 받는 것처럼 사용할 수 있다. 아래는 일반적으로 상속을 받는 경우이다.
class A:
x = 1
def __init__(self, a):
self.a = a
class C(A):
def __init__(self, a, b):
super().__init__(a)
self.b = b
이 때의 문제점은 A와 C가 지나치게 밀접한 연관을 갖게 된다. 상속을 했으니 당연한 것이지만, 지나치게 밀접한 관계는 그리 좋지 않다. 만약 A의 구조를 업데이트 한다면 그 영향이 C에 고스란히 미치기 때문이다. 따라서 유지보수를 쉽게 하기 위해서는 연관된 정도를 줄일 필요가 있고, 이는 composition으로 구현할 수 있다. 아래 코드를 보자.
# composiion 방식으로 상속 기능 대신하기
class D:
def __init__(self, a, b):
self.a = A(a)
self.b = b
두 코드 모두 동일한 동작을 함을 확인해보자.
>>> c = C(1, 2)
>>> c.a, c.b
(1, 2)
>>> d = D(1, 2)
>>> d.a.a, d.b
(1, 2)
이 때 눈여겨 볼 점은, d.a.a
이다. 왜 이렇게 써줬을까? d.a
만 해주면, class A의 객체를 호출하는 것이기 떄문이다. 따라서 class A 객체의 변수 a를 불러오기 위해 d.a.a
로 호출한 것이다.
functional programming에서 composition은 합성함수를 의미한다. 합성함수의 기본적인 구조는 함수에 x를 넣고, 그 결과를 다음 함수의 인자로 넣어주는 것이다. 아래 구조를 살펴보자.
def f(x):
x = sum(x)
x = bool(x)
return x
함수 f에는 인자로 x를 받는다. 인자로 받은 x는 sum()
연산을 거쳐 값이 새롭게 된다. 그 후 그 값을 다시 bool
class에 넣어 그 결과로 다시 x로 업데이트 하고 반환한다. 이처럼 합성함수의 기본은, 입력받은 인자를 연산을 거쳐 업데이트 하고 그 값을 또 다른 함수에 넣는 것이다.
funcionla programming의 첫 번째 특징인 'first class'에 의해 클래스는 함수로 사용할 수 있다. 그리고 class는 상속을 통해 이미 만들어진 구조를 내가 원하는 대로 바꿀 수 있었다. 그렇다면 함수도 이미 존재하는 것을 내가 임의로 일부분을 변경할 수 있지 않을까? 이런 배경에서 나온 것이 decorator이다.
decorator를 알아보기 전에 AOP 개념에 대해 먼저 알아보자.
AOP는 Aspect Oriented Programming의 줄임말이다. AOP가 필요한 이유는 아래 예시와 같다.
[ AOP가 필요한 이유 ]
아래 표와 같이 각 class마다 필요로하는 기능이 중복된다고 가정해보자.
class 필요한 기능 A x, y, z B x, z C x, y 이 때 각 class를 정의할 때마다 동일한 코드를 중복해서 넣는 것은 파이썬스럽지 않은 방법이다. 이럴 떄는 관점을 조금 바꿔보자. x, y, z를 각각 독립적인 기능으로 정의하고, class를 정의할 때 필요한 기능만 가져다 쓰면 되지 않을까? (이 떄 중요한 점은 기능 간 중복이 없어야 한다는 것)
이렇게 필요한 기능만 먼저 구현해놓고, class를 구현할 때 가져다 쓰기만 하는 것을 AOP 개념이라고 한다. 그런데 AOP는 class에만 적용 가능한 방법이 아니라 함수에도 적용이 가능하다.
이렇게 class나 함수의 기능을 쉽고 효율적으로 확장시켜주는 것을 AOP라고 하고, 그 중 AOP가 함수에 적용된 decorator를 정리해보고자 한다.
decorator는 위에서 이야기했듯 AOP가 함수에 적용된 것이다. 아래 예시를 보자.
def good():
print('good!')
def bar(fn): # 함수를 인자로 받음
fn() # calable의 개념에 의해 함수에 괄호 연산자를 붙일 수 있음
bar(good)
이 두 함수를 decorator로 표현해 볼 것인데, 그 전에 decorator의 조건에 대해 이야기 해보자.
위 조건을 살펴보면 closure 구조와 비슷하다는 것을 알 수 있다.(closure 구조가 기억 안나면 아래 코드를 참고할 것) 그래서 decorator는 function closure라고도 부른다고 한다.
# closure 구조
def x(m) :
def y(n) :
return m + n
return y
x(2)(3)
어쨌든, 위 두 조건을 만족하면 decorator로 인정받고, @로 decorator임을 표현한다. 위 조건을 만족하는 함수는 아래와 같다.
# decorator 구조
def good(fn): # 함수를 인자로 받음
def inner(): # 함수가 중첩 되어있어야함
print('function call')
fn() # 인자로 받은 함수가 중첩된 함수 내부에서 실행 되어야함
print('function finished')
return inner # 중첩된 함수가 return으로 제공 되어야함
위 함수는 decorator의 조건을 모두 만족하기 때문에 @good
으로 사용이 가능하다. @good
을 사용해보자.
# 위에서 만든 decorator 사용해보기
@good
def bar():
print('decorator')
위 코드를 해석하면 다음과 같다. bar라는 함수가 good이라는 함수에 들어가서 새로운 함수를 만드는 것이다. decorator의 해석 관점에서 보면 아래와 같다.
# decorator 해석 관점에서 보면 아래와 같다.
bar = good(bar) # bar를 good에 넣어서 새로운 bar를 정의
함수의 기능을 확장해 새로운 함수로 만들어주는 decorator는 여러개를 붙여 사용할 수 있다.
# decorator 만들기 1
def decorator1(fn):
print("decorator1")
def inner1(*args, **kwargs): # 파라미터를 어떻게 받던 간에 모두 처리할 수있게 만드는 방법
print("inner1")
return fn(*args, **kwargs)
return inner1
# decorator 만들기 2
def decorator2(fn):
print("decorator2")
def inner2(*args, **kwargs):
print("inner2")
return fn(*args, **kwargs)
return inner2
# decorator의 사용
>>> @decorator1
def xx():
print('a')
decorator1
위 함수 xx는 선언하자마자 'decorator1'가 출력되었을까? decorator의 해석을 생각해보면, xx를 decorator1에 넣어 새로운 함수로 만드는 것이다. 그런데 decorator1에는 중첩함수(inner1)를 선언하기 전에 'decorator1'울 출력하기 때문에 이를 decorator로 사용하는 xx를 정의할 때 print('decorator1')
가 작동한 것이기 때문이다.
decorator가 두 개 붙으면 어떠헥 될까?
>>> @decorator2
@decorator1
def xx():
print('a')
decorator1
decorator2
xx를 선언하자마자 'decorator1'과 'decorator2' 순서대로 찍혔다. 이를통해 decorator가 두 개 이상 붙을 때 동작을 생각해보면, 함수 정의와 가까운 순서대로 함수를 확장함을 알 수 있다.(위의 경우 decorator1 → decorator2 순)