composition, decorator

jwKim·2023년 7월 9일
0

1. composition

1.1. composition 개요

python에는 composition이라는 개념이 있다. composition은 functional programming 관점과 객체지향 관점에서 의미하는 것이 다르다.

  • 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 기법이라고 이야기 한다.

1.2. 상속을 대신하는 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로 호출한 것이다.

1.3. functional programming에서의 composition

functional programming에서 composition은 합성함수를 의미한다. 합성함수의 기본적인 구조는 함수에 x를 넣고, 그 결과를 다음 함수의 인자로 넣어주는 것이다. 아래 구조를 살펴보자.

def f(x):
  x = sum(x)
  x = bool(x)
  return x

함수 f에는 인자로 x를 받는다. 인자로 받은 x는 sum() 연산을 거쳐 값이 새롭게 된다. 그 후 그 값을 다시 bool class에 넣어 그 결과로 다시 x로 업데이트 하고 반환한다. 이처럼 합성함수의 기본은, 입력받은 인자를 연산을 거쳐 업데이트 하고 그 값을 또 다른 함수에 넣는 것이다.



2. decorator

2.1. decorator가 나온 배경

2.1.1. 함수의 일부분 바꾸기

funcionla programming의 첫 번째 특징인 'first class'에 의해 클래스는 함수로 사용할 수 있다. 그리고 class는 상속을 통해 이미 만들어진 구조를 내가 원하는 대로 바꿀 수 있었다. 그렇다면 함수도 이미 존재하는 것을 내가 임의로 일부분을 변경할 수 있지 않을까? 이런 배경에서 나온 것이 decorator이다.

decorator를 알아보기 전에 AOP 개념에 대해 먼저 알아보자.

2.1.2. AOP

AOP는 Aspect Oriented Programming의 줄임말이다. AOP가 필요한 이유는 아래 예시와 같다.

[ AOP가 필요한 이유 ]
아래 표와 같이 각 class마다 필요로하는 기능이 중복된다고 가정해보자.

class필요한 기능
Ax, y, z
Bx, z
Cx, y

이 때 각 class를 정의할 때마다 동일한 코드를 중복해서 넣는 것은 파이썬스럽지 않은 방법이다. 이럴 떄는 관점을 조금 바꿔보자. x, y, z를 각각 독립적인 기능으로 정의하고, class를 정의할 때 필요한 기능만 가져다 쓰면 되지 않을까? (이 떄 중요한 점은 기능 간 중복이 없어야 한다는 것)

이렇게 필요한 기능만 먼저 구현해놓고, class를 구현할 때 가져다 쓰기만 하는 것을 AOP 개념이라고 한다. 그런데 AOP는 class에만 적용 가능한 방법이 아니라 함수에도 적용이 가능하다.

  • AOP가 class에 적용되면 MixIn
  • AOP가 함수에 적용되면 decorator

이렇게 class나 함수의 기능을 쉽고 효율적으로 확장시켜주는 것을 AOP라고 하고, 그 중 AOP가 함수에 적용된 decorator를 정리해보고자 한다.

2.2. decorator

2.2.1. decorator 기본 구조

decorator는 위에서 이야기했듯 AOP가 함수에 적용된 것이다. 아래 예시를 보자.

def good():
  print('good!')
  
def bar(fn): # 함수를 인자로 받음
  fn() # calable의 개념에 의해 함수에 괄호 연산자를 붙일 수 있음
  
bar(good)

이 두 함수를 decorator로 표현해 볼 것인데, 그 전에 decorator의 조건에 대해 이야기 해보자.

  • 함수 정의가 중첩되어 있어야함
  • 입력받는 첫 번째 인자가 함수여야함
  • 인자로 받은 함수가 중첩된 함수 내부에서 실행 되어야함
  • return으로 중첩된 함수가 와야함

위 조건을 살펴보면 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를 정의

2.2.2. decorator의 동작

함수의 기능을 확장해 새로운 함수로 만들어주는 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 순)

0개의 댓글

관련 채용 정보