[python cleancode] 6. 디스크립터로 더 멋진 객체 만들기

햄도·2021년 4월 11일
2

Python Cleancode

목록 보기
6/9

출처

파이썬 클린코드를 읽으며 정리한 내용입니다.

  • 디스크립터는 파이썬의 객체지향 수준을 한 단계 더 끌어올려주는 혁신적인 기능
  • 잘 활용하면 견고하고 재사용성이 높은 추상화를 할 수 있다.
  • 이번 장에서 학습할 내용
    • 디스크립터의 의미와 동작 원리, 효율적인 구현 방법
    • 디스크립터의 유형
    • 디스크립터를 활용한 코드 재사용 방법
    • 디스크립터의 좋은 사용 예시

디스크립터 개요

디스크립터 메커니즘

  • 최소 두 개의 클래스로 구성
    • 클라이언트 클래스: 디스크립터 구현의 기능을 활용할 도메인 모델
    • 디스크립터 클래스: 디스크립터 로직의 구현체
  • 디스크립터는 단지 디스크립터 프로토콜을 구현한 클래스의 인스턴스이다.
  • 디스크립터 프로토콜
    • __get__
    • __set__
    • __delete__
    • __set_name__
    • 디스크립터 클래스는 위 매직 메서드 중 한 개 이상을 포함해야 한다.
  • 이 프로토콜이 제대로 동작하려면 디스크립터 객체는 인스턴스 속성이 아닌 클래스 속성으로 정의되어야 한다.
  • 클라이언트 클래스 내부에 클래스 속성을 객체로 선언하면 디스크립터로 인식되고, 인스턴스에서 디스크립터 속성을 호출하면 __get__ 매직 메서드의 결과를 반환한다.
class DescriptorClass:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        print("Call: %s.__get__(%r, %r)", self.__class__.__name__, instance, owner)
        return instance
    
class ClientClass:
    descriptor = DescriptorClass()
client = ClientClass()
client.descriptor
>>> 
Call: %s.__get__(%r, %r) DescriptorClass <__main__.ClientClass object at 0x7f649fa469a0> <class '__main__.ClientClass'>

<__main__.ClientClass at 0x7f649fa469a0>
# 클라이언트 인스턴스 자체를 반환하므로 결과는 같다.
client.descriptor is client
>>> 
Call: %s.__get__(%r, %r) DescriptorClass <__main__.ClientClass object at 0x7f649fa469a0> <class '__main__.ClientClass'>

True

디스크립터 프로토콜의 메서드 탐색

  • 지금까지는 맛보기였다.
  • 디스크립터 프로토콜의 각 메서드에 사용되는 파라미터와 사용 방법을 더 자세히 알아보자.

__get__(self, instance, owner)

  • 두번째 파라미터 instance는 디스크립터를 호출한 객체를 의미한다.
  • owner는 해당 객체의 클래스를 의미한다. 즉 클라이언트 클래스이다.
  • 클라이언트 인스턴스가 아니라 클라이언트 클래스에서 디스크립터를 호출하는 경우에는 instance가 None이다. 이 경우를 대비해 클라이언트 클래스는 owner로 따로 받는다.
class DescriptorClass:
    def __get__(self, instance, owner):
        if instance is None:
            return f"{self.__class__.__name__}.{owner.__name__}"
        return f"value for {instance}"
    
class ClientClass:
    descriptor = DescriptorClass()
ClientClass.descriptor
>>> 'DescriptorClass.ClientClass'
ClientClass().descriptor
>>> 'value for <__main__.ClientClass object at 0x7f649fa466d0>'

__set__(self, instance, value)

  • 디스크립터에 값을 할당하려고 할 때 호출된다. 이 특성을 이용해 속성의 유효성을 검사하는 객체를 만들 수 있다.
  • 이 메서드를 구현한 디스크립터에서만 활성화된다. 구현하지 않은 디스크립터에 값을 할당하려고 하는 경우 디스크립터 자체를 덮어쓴다.
class Validation:
    def __init__(self, validation_function, error_msg: str):
        self.validation_function = validation_function
        self.error_msg = error_msg
    
    def __call__(self, value):
        if not self.validation_function(value):
            raise ValueError(f"{value!r} {self.error_msg}")
            
class Field:
    def __init__(self, *validations):
        self._name = None
        self.validations = validations
    
    def __set_name__(self, owner, name):
        self._name = name
        
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self._name]
    
    def validate(self, value):
        for validation in self.validations:
            validation(value)
            
    def __set__(self, instance, value):
        # validate에서 raise하지 않으면 자기자신을 이름으로 한 속성의 값을 바꿔준다.
        self.validate(value)
        instance.__dict__[self._name] = value
        
class ClientClass:
    descriptor = Field(
        Validation(lambda x: isinstance(x, (int, float)), "는 숫자가 아님"),
        Validation(lambda x: x >= 0, "는 0보다 작음")
    )
client = ClientClass()
client.descriptor = 42
client.descriptor
>>> 42
client.descriptor = -42
>>> 
ValueErrorTraceback (most recent call last)

<ipython-input-29-c7448f081032> in <module>
----> 1 client.descriptor = -42

<ipython-input-27-35255fb5a0b0> in __set__(self, instance, value)
     26 
     27     def __set__(self, instance, value):
---> 28         self.validate(value)
     29         instance.__dict__[self._name] = value
     30 

<ipython-input-27-35255fb5a0b0> in validate(self, value)
     23     def validate(self, value):
     24         for validation in self.validations:
---> 25             validation(value)
     26 
     27     def __set__(self, instance, value):

<ipython-input-27-35255fb5a0b0> in __call__(self, value)
      6     def __call__(self, value):
      7         if not self.validation_function(value):
----> 8             raise ValueError(f"{value!r} {self.error_msg}")
      9 
     10 class Field:

ValueError: -42 는 0보다 작음
client.descriptor = "invalid value"
>>> 
ValueErrorTraceback (most recent call last)

<ipython-input-30-f259c4b5e588> in <module>
----> 1 client.descriptor = "invalid value"

<ipython-input-27-35255fb5a0b0> in __set__(self, instance, value)
     26 
     27     def __set__(self, instance, value):
---> 28         self.validate(value)
     29         instance.__dict__[self._name] = value
     30 

<ipython-input-27-35255fb5a0b0> in validate(self, value)
     23     def validate(self, value):
     24         for validation in self.validations:
---> 25             validation(value)
     26 
     27     def __set__(self, instance, value):

<ipython-input-27-35255fb5a0b0> in __call__(self, value)
      6     def __call__(self, value):
      7         if not self.validation_function(value):
----> 8             raise ValueError(f"{value!r} {self.error_msg}")
      9 
     10 class Field:

ValueError: 'invalid value' 는 숫자가 아님
  • 프로퍼티 자리에 놓일 수 있는 것은 무엇이든 디스크립터로 추상화할 수 있으며 여러 번 재사용할 수 있다. 여기에서는 __set__() 메서드가 @property.setter가 하던 일을 대신한다.
  • __set_name__의 동작도 궁금하지만 다음에 나오니까 일단 넘어가자.

__delete__(self, instance)

  • 이 메서드는 다음과 같은 형태로 호출된다.
del client.descriptor
  • self는 descriptor 속성을 나타내고, instance는 client를 나타낸다.
class ProtectedAttribute:
    def __init__(self, requires_role=None) -> None:
        self.permission_required = requires_role
        self._name = None
        
    def __set_name__(self, owner, name):
        self._name = name
    
    def __set__(self, user, value):
        if value is None:
            raise ValueError(f"{self._name}를 None으로 설정할 수 없음")
        user.__dict__[self._name] = value
    
    def __delete__(self, user):
        if self.permission_required in user.permissions:
            user.__dict__[self._name] = None
        else:
            raise ValueError(
                f"{user!s} 사용자는 {self.permission_required} 권한이 없음"
            )
            
class User:
    """admin 권한을 가진 사용자만 이메일 주소를 삭제할 수 있음"""
    
    email = ProtectedAttribute(requires_role="admin")
    
    def __init__(self, username: str, email: str, permission_list: list = None) -> None:
        self.username = username
        self.email = email
        self.permissions = permission_list or []
        
    def __str__(self):
        return self.username
  • User 클래스는 username과 email을 필수로 받고, email 속성을 지우게 되면 User 클래스에서 정의한 인터페이스와 맞지 않는 상태가 된다.
  • 이런 이유 때문에 email을 삭제하는 작업은 email을 None으로 설정하는 것으로 대체한다. 이를 위해 email 값에 None을 할당하는 것은 __set__에서 막아줘야 한다.
  • admin 권한을 가진 사용자만 이메일 주소를 제거할 수 있다고 가정하면, 다음과 같이 동작한다.
admin = User("root", "root@d.com", ["admin"])
user = User("user", "user1@d.com", ["email", "helpdesk"])
admin.email
>>> 'root@d.com'
del admin.email
admin.email is None
>>> True
user.email
>>> 'user1@d.com'
user.email = None
>>> 
ValueErrorTraceback (most recent call last)

<ipython-input-35-5a2ef792c3b9> in <module>
----> 1 user.email = None

<ipython-input-31-a74365bea70b> in __set__(self, user, value)
      9     def __set__(self, user, value):
     10         if value is None:
---> 11             raise ValueError(f"{self._name}를 None으로 설정할 수 없음")
     12         user.__dict__[self._name] = value
     13 

ValueError: email를 None으로 설정할 수 없음
del user.email
>>> 
ValueErrorTraceback (most recent call last)

<ipython-input-36-c33f7427572a> in <module>
----> 1 del user.email

<ipython-input-31-a74365bea70b> in __delete__(self, user)
     16             user.__dict__[self._name] = None
     17         else:
---> 18             raise ValueError(
     19                 f"{user!s} 사용자는 {self.permission_required} 권한이 없음"
     20             )

ValueError: user 사용자는 admin 권한이 없음
  • __delete__ 메서드는 앞의 두 메서드에 비해 자주 사용되지는 않지만 이해의 완결성을 위해 살펴보는 것이 좋다.

__set_name__(self, owner, name)

  • 클래스에 디스크립터 객체를 만들 때는 디스크립터가 처리하려는 속성의 이름을 알아야 한다. 즉 디스크립터 객체 자신이 클라이언트 객체 안에서 어떤 이름으로 사용되는지 알아야 한다.
  • 앞에서 보았듯이, 속성의 이름은 __dict__에서 __get____set__ 메서드로 읽고 쓸때 사용된다.
  • 파이썬 3.6 이전에는 디스크립터 객체 초기화 시 생성자에 이름을 전달하는 방식으로 사용했다. 3.6부터 __set_name__ 메서드가 추가되어 파라미터로 디스크립터를 소유한 클래스와 디스크립터의 이름을 받는다.
  • 하위 호환을 위해서 __init__ 메서드에 기본 값을 지정하고 __set_name__을 함께 사용하는 것이 좋다.
class DescriptorWithName:
    def __init__(self, name=None):
        self.name = name
    
    def __set_name__(self, owner, name):
        self.name = name

class ClientClass:
    descriptor = DescriptorWithName()

디스크립터의 유형

  • 메서드를 사용하는 디스크립터의 작동방식에 따라 디스크립터를 구분할 수 있다.
  • 이 구분을 이해해야 디스크립터를 효과적으로 사용할 수 있고, 런타임 경고나 오류를 피하는 데에 도움이 된다.
  • 데이터 디스크립터: __set__이나 __delete__ 메서드를 구현함
  • 비데이터 디스크립터: __get__만을 구현함
  • __set_name__은 이 분류에 전혀 영향을 미치지 않는다.
  • 데이터 디스크립터는 객체의 사전에 디스크립터와 동일한 이름을 갖는 키가 존재하더라도 디스크립터가 먼저 호출된다. 반면 비데이터 디스크립터는 객체의 사전에 동일한 이름의 키가 있으면 객체의 사전 값이 적용된다.

비데이터 디스크립터

class NonDataDescriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return 42

class ClientClass:
    descriptor = NonDataDescriptor()
client = ClientClass()
client.descriptor
>>> 42
# 속성을 다른 값으로 바꾸면 이전의 값을 잃는다.
client.descriptor = 43
client.descriptor
>>> 43
del client.descriptor
client.descriptor
>>> 42
vars(client)
>>> {}
  • 처음 client 객체를 만들었을 때 descriptor 속성은 인스턴스가 아니라 클래스 안에 있다. 따라서 client 객체의 사전은 비어있다.
  • 이 때 descriptor 속성을 조회하면 client.__dict__에서 descriptor라는 이름의 키를 찾지 못하고 클래스에서 디스크립터를 찾게 된다. 이때문에 __get__ 메서드가 반환되게 된다.
  • 그러나 descriptor 속성에 다른 값을 설정하면 인스턴스의 사전에 descriptor가 추가되고, 클래스 속성인 디스크립터보다 먼저 반환된다.
  • 다시 del로 인스턴스 속성을 삭제하면, 디스크립터를 반환할 수 있게 된다.

데이터 디스크립터

class DataDescriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return 42
    
    def __set__(self, instance, value):
        print("%s.descriptor를 $s 값으로 설정", instance, value)
        instance.__dict__["descriptor"] = value

class ClientClass:
    descriptor = DataDescriptor()
client = ClientClass()
client.descriptor
>>> 42
# 속성을 다른 값으로 바꾸어도 반환 값이 변경되지 않는다.
client.descriptor = 99
client.descriptor
>>> 
%s.descriptor를 $s 값으로 설정 <__main__.ClientClass object at 0x7f649ff6fd90> 99

42
# 하지만 __dict__의 값은 업데이트된다.
vars(client)
>>> {'descriptor': 99}
del client.descriptor
>>> 
AttributeErrorTraceback (most recent call last)

<ipython-input-59-322a6dd19fa3> in <module>
----> 1 del client.descriptor

AttributeError: __delete__
  • __set__메서드가 호출되면 객체의 사전에 값을 설정해주고, 데이터 디스크립터에서 속성을 조회하면 객체의 __dict__ 대신 클래스의 descriptor를 먼저 조회한다.
    • 데이터 디스크립터는 인스턴스의 __dict__를 오버라이드하여 인스턴스 사전보다 높은 우선순위를 가지지만, 비데이터 디스크립터는 인스턴스 사전보다 낮은 우선순위를 가지기 때문이다.
    • __set__ 메서드가 구현된 디스크립터는 __dict__에 들어있는 속성과 유사한 기능을 한다고 생각한걸까??
  • __set__ 메서드가 비어있어도 value는 흔적없이 증발하고 descriptor는 여전히 42를 반환한다.
  • 속성 삭제는 더이상 동작하지 않는다. del을 호출하면 인스턴스 사전 대신 디스크립터에서 __delete__를 호출하기 때문이다.
  • 디스크립터의 __set__ 메서드에서 setattr()이나 instance.descriptor = value 등 할당 표현식을 직접 사용하면 무한루프가 발생한다. 아래 코드와 같이 인스턴스 사전에 직접 접근해야 한다.
instance.__dict__["descriptor"] = value
  • 클라이언트 클래스는 디스크립터의 참조를 가지고 있고, 디스크립터가 다시 클라이언트 객체를 참조하면 순환 종속성이 생겨 가비지 컬렉션이 되지 않기 때문에 디스크립터는 모든 인스턴스의 프로퍼티 값을 보관할 수 없다.
    • 파이썬은 레퍼런스 카운트라는 가비지 컬렉션 전략을 가지고 있다. 즉 해당 객체에 대한 참조가 모두 사라져야 가비지 컬렉션될 수 있다. 참고
  • 이를 해결하기 위해 weakref 모듈의 약한 참조를 사용하여 약한 참조 키 사전을 만들 수 있다. (To be continued)
from functools import wraps
from types import MethodType

class DBDriver:
    def __init__(self, dbstring):
        self.dbstring = dbstring
    
    def execute(self, query):
        return f"{self.dbstring}에서 쿼리 {query} 실행"
    

class inject_db_driver:
    """데이터베이스 dns 문자열을 받아 DBDriver 인스턴스를 생성하는 데코레이터
    """
    
    def __init__(self, function):
        self.function = function
        wraps(self.function)(self)
    
    def __call__(self, dbstring):
        return self.function(DBDriver(dbstring))

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.__class__(MethodType(self.function, instance))
# inject_db_driver로 한번 감싸주지만 받는 인자는 유지하는 것 같다..
inject_db_driver(MethodType(DataHandler.run_query, dh)).function
>>> <function __main__.DataHandler.run_query(self, driver)>
class DataHandler:
    # 클래스 안에서 참조
    # run_query = inject_db_driver(run_query)(self, driver)
    @inject_db_driver # 여기에서 init 실행, self.function에 run_query 들어감
    def run_query(self, driver):
        return driver.execute(self.__class__.__name__)
dh = DataHandler()
dh.run_query('driver')
>>> 'driver에서 쿼리 DataHandler 실행'
@inject_db_driver
def run_query2(driver):
    return driver.execute('function')
run_query2('driver2')
>>> 'driver2에서 쿼리 function 실행'

디스크립터 분석

  • 디스크립터를 이용하면 반복 로직과 구현을 추상화하여 명확한 코드를 작성할 수 있다는 것은 알겠다.
  • 그러면 좋은 디스크립터의 기준은 무엇일까?
  • 디스크립터를 더 자세히 분석해보자.

파이썬 내부에서의 디스크립터 활용

  • 어떤 것이 좋은 디스크립터인지 확인하기 위해 파이썬 자체의 디스크립터를 확인해보자.

함수와 메서드

  • 디스크립터 객체 중 가장 멋진 예는 함수이다.
  • 함수는 __get__ 메서드를 구현했기 때문에 클래스 안에서 메서드처럼 동작할 수 있다.
  • 메서드는 추가 파라미터를 가진 함수일 뿐이며, 메서드에서 self를 사용하는 것은 객체를 받아서 수정을 하는 함수를 사용하는 것과 동일하다.
  • 메서드는 객체 안에서 정의되었기 때문에 객체에 바인딩되어 있다고 말한다.
class MyClass:
    def method(self, ...):
        self.x = 1

위 코드는 아래 코드와 같다.

class MyClass: pass

def method(myclass_instance, ...):
    myclass_instance.x = 1
    
method(MyClass())
instance = MyClass()
instance.method(...)

파이썬은 위 구문을 다음과 같이 처리한다.

instance = MyClass()
MyClass.method(instance, ...)
  • 함수는 디스크립터 프로토콜을 구현했기 때문에 메서드를 호출하기 전에 __get__() 메서드가 먼저 호출되고 필요한 변환을 한다.
  • 변환 과정은 아래와 같다.
    1. instance.method(...) 구문에서는 괄호 안의 인자를 처리하기 전에 instance.method 부분을 먼저 평가한다.
    2. method는 클래스 속성으로 정의된 객체이고 __get__ 메서드가 있기 때문에 __get__ 메서드가 호출된다. (디스크립터 프로토콜)
    3. __get__ 메서드는 함수를 메서드로 변환한다. 즉 함수를 작업하려는 객체의 인스턴스에 바인딩한다.
class Method: 
    # 호출 가능한 메소드 객체
    def __init__(self, name):
        self.name = name
    
    def __call__(self, instance, arg1, arg2):
        print(f"{self.name}: {instance} 호출됨. 인자는 {arg1}{arg2}입니다.")
        

class MyClass:
    # 함수가 클래스 속성으로 들어가있는 경우
    method = Method("Internal call")    
# 아래 호출은 동일한 역할을 해야 하지만 두 번째 호출은 오류가 발생한다.
instance = MyClass()
Method("External call")(instance, "first", "second")
instance.method("first", "second")
>>> 
External call: <__main__.MyClass object at 0x7f649fc6f820> 호출됨. 인자는 first와 second입니다.

TypeErrorTraceback (most recent call last)

<ipython-input-88-679fb8aa1fc8> in <module>
      2 instance = MyClass()
      3 Method("External call")(instance, "first", "second")
----> 4 instance.method("first", "second")

TypeError: __call__() missing 1 required positional argument: 'arg2'
  • 두 번째 호출에서 오류가 나는 이유는, Method.__call__의 self 자리에는 instance가 들어가면서 인자가 하나씩 당겨졌기 때문이다. arg2에는 아무것도 할당되지 않게 된다.
  • 이런 문제를 해결하기 위해 메서드를 디스크립터로 변경할 수 있다. 그렇게 하면 intance.method 호출 시 Method.__get__이 먼저 호출되고, __get__에서 첫번째 파라미터로 Method의 인스턴스를 전달하며 객체에 바인딩하면 된다.
from types import MethodType

class Method:
    def __init__(self, name):
        self.name = name
    
    def __call__(self, instance, arg1, arg2):
        print(f"{self.name}: {instance} 호출됨. 인자는 {arg1}{arg2}입니다.")
        
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return MethodType(self, instance)
    
class MyClass:
    method = Method("Internal call")    
instance = MyClass()
Method("External call")(instance, "first", "second")
instance.method("first", "second")
>>> 
External call: <__main__.MyClass object at 0x7f649faf4eb0> 호출됨. 인자는 first와 second입니다.
Internal call: <__main__.MyClass object at 0x7f649faf4eb0> 호출됨. 인자는 first와 second입니다.
  • __get__ 메소드는 types 모듈의 MethodType을 사용하여 호출 가능한 객체를 메서드로 변환한다. MethodType의 첫번째 파라미터는 호출 가능한 객체로 self(Method의 인스턴스)가 들어가고, 두번째 파라미터는 이 함수를 바인딩할 객체이다.
instance.method.__call__()
>>> 
TypeErrorTraceback (most recent call last)

<ipython-input-100-4a9a51cfb689> in <module>
----> 1 instance.method.__call__()

TypeError: __call__() missing 2 required positional arguments: 'arg1' and 'arg2'

메서드를 위한 빌트인 데코레이터

  • 공식 문서에 설명된 것처럼 @property, @classmethod, @staticmethod 데코레이터는 디스크립터이다.
  • 메서드를 인스턴스가 아닌 클래스에서 직접 호출할 때는 관습적으로 디스크립터 자체를 반환한다. 프로퍼티를 클래스에서 직접 호출하면 계산할 속성이 없으므로 일종의 디스크립터인 프로퍼티 객체 자체를 반환한다.
  • @classmethod를 사용하면 디스크립터의 __get__ 함수가 메서드를 인스턴스에서 호출하든 클래스에서 호출하든 상관없이 데코레이팅 함수에 첫번째 파라미터로 메서드를 소유한 클래스를 넘겨준다.
  • @staticmethod를 사용하면 정의한 파라미터 이외의 파라미터를 넘기지 않도록 한다. 즉 __get__메서드에서 첫번째 파라미터에 self를 바인딩하는 작업을 취소한다.

슬롯

  • 클래스에 __slot__ 속성을 정의하면 클래스가 기대하는 특정 속성만 정의하고 다른 것은 제한할 수 있다.
  • __slot__에 정의되지 않은 속성을 동적으로 추가하려고 할 경우 AttributeError가 발생한다. 이 경우 클래스는 __dict__ 속성을 갖지 않는다.
  • 이 때 객체의 사전이 없어도 속성을 가져올 수 있도록 디스크립터를 사용할 수 있다.
  • 흥미로운 기능이지만, 파이썬의 동적인 특성을 없애기 때문에 신중히 사용하자.
  • 메모리를 덜 사용한다는 점이 장점이다.

데코레이터를 디스크립터로 구현하기

  • 지금까지 살펴본 것처럼, 데코레이터 클래스에 __get__ 메서드를 구현하고 types.MethodType을 이용해 데코레이터 자체를 객체에 바인딩된 메서드로 만들면 메소드와 함수 모두에 사용할 수 있는 데코레이터를 만들 수 있다.
from functools import wraps
from types import MethodType

class DBDriver:
    def __init__(self, dbstring):
        self.dbstring = dbstring
    
    def execute(self, query):
        return f"{self.dbstring}에서 쿼리 {query} 실행"
    

class inject_db_driver:
    """데이터베이스 dns 문자열을 받아 DBDriver 인스턴스를 생성하는 데코레이터
    """
    
    def __init__(self, function):
        self.function = function
        wraps(self.function)(self)
    
    def __call__(self, dbstring):
        return self.function(DBDriver(dbstring))

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.__class__(MethodType(self.function, instance))
# inject_db_driver로 한번 감싸주지만 받는 인자는 유지하는 것 같다..
inject_db_driver(MethodType(DataHandler.run_query, dh)).function
>>> <function __main__.DataHandler.run_query(self, driver)>
class DataHandler:
    # 클래스 안에서 참조
    # run_query = inject_db_driver(run_query)(self, driver)
    @inject_db_driver # 여기에서 init 실행, self.function에 run_query 들어감
    def run_query(self, driver):
        return driver.execute(self.__class__.__name__)
dh = DataHandler()
dh.run_query('driver')
>>> 'driver에서 쿼리 DataHandler 실행'
@inject_db_driver
def run_query2(driver):
    return driver.execute('function')
run_query2('driver2')

요약

  • 디스크립터는 파이썬의 클래스가 일반 객체라서 속성을 갖고 속성과 상호 교류할 수 있다는 점을 명확하게 해준다.
  • 디스크립터의 장점은 많지만 오버 엔지니어링에 사용되지 않도록 주의하자.
  • 디스크립터에는 기술적인 기능 구현만을 포함하고 비즈니스 로직 자체를 포함하지 않도록 하자.
profile
developer hamdoe

2개의 댓글

comment-user-thumbnail
2021년 7월 10일

정리가 잘 되어있네요 잘봤습니다!

1개의 답글