Python/ETC / / 2026. 1. 31. 16:22

[파이썬 기본 다지기] 파이썬에서의 Descriptor(디스크립터)의 역할 및 Non-Data Descriptor(비 데이터 디스크립터), Data Descriptor(데이터 디스크립터)와 Django(장고)에서의 model Field에서 살펴보기

반응형

 

1. 디스크립터란?

 

파이썬 공식 문서에서는 다음과 같이 디스크립터에서 간략하게 설명하고 있다.

 

A descriptor is what we call any object that defines __get__(), __set__(), or __delete__().

Optionally, descriptors can have a __set_name__() method. This is only used in cases where a descriptor needs to know either the class where it was created or the name of class variable it was assigned to. (This method, if present, is called even if the class is not a descriptor.)

디스크립터는 __get__(), __set__(), 혹은 __delete__()를 정의한 아무 객체를 지칭한다.

추가적으로 디스크립터는 __set_name__()를 가질 수 있다. 이는 이 디스크립터가 생성된 클래스나 클래스의 변수에 할당된 디스크립터에서 구별할 필요가 있을 때 사용되어진다. (이 메서드는 클래스가 디스크립터가 아니여도 호출될 수 있다.)

https://docs.python.org/ko/3.13/howto/descriptor.html

 

즉 가장 기본적인 Magic Method(매직 메소드 혹은 Special Method/스페셜 메소드)인 __get__(), __set__(), 혹은 __delete__()이 정의된 클래스의 경우 디스크립터라고 볼 수 있다. 디스크립터의 활용은 다른 클래스의 변수로 사용될 때만 작동하고, 클래스 인스턴스에 넣으면 작동하지 않는다. 그렇다면 이런 __get__(), __set__(), 혹은 __delete__()에는 어떤 파라메터가 들어갈까?

 

class MyDescriptor:
	
    def __get__(self, instance, owner):
        print("Get a value of attribute")
    
    def __set__(self, instance, value):
    	print("Set a value of attribute")
    
    def __delete__(self, instance):
    	print("Delete a value of attribute")
        
class MyClass:
	attribute = MyDescriptor()
    
>>> my_class = MyClass()
>>> my_class.attribute
>>> MyClass.attribute

 

  • __get__(self, instance, owner)
    • 여기서 self는 디스크립터 인스턴스 자체, instance는 디스크립터를 호출한 객체, owner는 디스크립터가 속해 있는 클래스를 의미한다. 
    • 위의 코드에서 self는 MyDescriptor가 되며, instance와 owner는 "my_class.attribute"로 호출했을 경우 instance my_class가 되며 owner의 경우 MyClass이며, "MyClass.attribute"로 호출했을 경우 None이 되고 owner는 역시 MyClass가 된다. 
  • __set__(self, instance, value)
    • 여기서 self는 디스크립터 인스턴스 자체, instance는 디스크립터를 호출한 객체, value는 할당된 값을 의미한다. 
  • __delete__(self, instance)
    • 여기서 self는 디스크립터 인스턴스 자체, instance는 디스크립터를 호출한 객체가 된다.

간단하게 말해서 위의 매직 메소드가 들어가 있는 Class라면 디스크립터가 될 수 있다고 할 수 있다. 하지만 이 디스크립터도 어떤 매직 메소드가 들어가는지에 따라 두가지의 종류로 나뉜다. 바로 "비 데이터 디스크립터"와 "데이터 디스크립터"이다.

 

2. Non-Data Descriptor(비 데이터 디스크립터)와 Data Descriptor(데이터 디스크립터)

  • 비 데이터 디스크립터 : __set__()과 __delete__()가 정의되어 있지 않고 __get__()만 정의 되어있는 경우. 객체를 통한 디스크립터 접근과 클래스를 통한 디스크립터 접근을 모두 지원한다.
  • 데이터 디스크립터 :  __set__()이나 __delete__() 혹은 둘 다 정의되어 있는 경우. 객체를 통한 디스크립터 접근만 된다.

간단하게 정의가 가능하다. 즉 __set__()이나 __delete__() 중 하나라도 정의되어 있으면 데이터 디스크립터이며 그렇지 않으면 비 데이터 디스크립터라고 볼 수 있다. 근데 이 데이터 디스크립터와 비 데이터 디스크립터의 분류로 인해 호출에도 우선순위가 생긴다. 만약 데이터 디스크립터, 인스턴스 변수, 비 데이터 디스크립터가 모두 있는 상황이라면 "데이터 스크립터 > 인스턴스 변수 > 비 데이터 디스크립터"순으로 우선순위가 나뉜다.

 

2-1. 데이터 디스크립터 - __set__()의 경우

 

class MyDescriptor:
	
    def __get__(self, instance, owner):
        print("Get a value of attribute")
        return 10
    
    def __set__(self, instance, value):
    	print("Set a value of attribute")
    
class MyClass:
	
    attribute = MyDescriptor()
    
    def __init__(self):
    	self.x = 20

>>> MyClass.attribute
>>> MyClass.attribute = 30
>>> MyClass.attribute

위의 코드를 한 번 봐보겠다. 그냥 단순하게 생각해서 MyClass.attribute를 처음 호출 했을 때는 __get__()으로 가서 "Get a value of attribute"를 출력한 뒤 10을 반환할 것이고, 30을 할당하면 아마 __set__()으로 가서 할당과 동시에 "Set a value of agttribute"가 출력될 것이라고 예상할 수 있다. 하지만 결과는 다르다.

 

 

결과는 처음에는 예상대로 나왔지만 30을 할당하니, attribute가 30으로 치환되어 버린다. 이는 위에서 설명한 데이터 디스크립터의 특징 때문이다. 즉 데이터 디스크립터는 객체(Instance)로만 접근이 가능한데 위의 코드는 class로 접근했으니 __set__()이 호출되지 않고 attribute에 있던 MyDescriptor()가 30으로 치환되어 버린 것이다. 그렇다면 객체 형태로 접근하면 어떻게 될까?

 

 

정상적으로 "Set a value of attribute"가 출력되는 것을 알 수 있다.

 

3. 디스크립터의 실행 예시

 

class MyDescriptor:
	
    def __init__(self, value):
        self.value = value

    def __get__(self, instance, owner):
        return self.value
    
    def __set__(self, instance, value):
        self.value = value
    
class MyClass:
    
    attribute = MyDescriptor(20)
        
>>> my_class = MyClass()
>>> my_class.attribute
>>> my_class.attribute = 30
>>> my_class.attribute

 

 

위의 코드를 실행하면 어떻게 나올지 이제 알 것이다. 아마 예상대로라면 "20, 30"이 차례차례로 출력될 것이다.

 

 

예상대로 20과 30이 제대로 출력되었다. 그러면 여기서 드는 생각이 있다. 만약 __set__()이 없는 상황에서 할당을 하려고 하면 어떻게 될 것인가 하는 의문이다. 데이터 디스크립터의 경우 가장 우선순위가 높기 때문에 __set__()이나 __get__()을 할 때 가장 먼저 고려되어 속성에 접근하여 값을 가져오고 수정할 수 있다. 하지만 우선순위가 가장 낮은 비 데이터 디스크립터의 경우라면 어떨까?

 

class MyDescriptor:
	
    def __init__(self, value):
        self.value = value

    def __get__(self, instance, owner):
        return self.value
    
class MyClass:
    
    attribute = MyDescriptor(20)
        
>>> my_class = MyClass()
>>> my_class.attribute

 

위의 경우처럼 단순하게 값을 가져온다면 20이 나올 것이다. 하지만 다음과 같은 경우는 어떨까?

 

>>> my_class.attribute
>>> my_class.attribute = 30
>>> my_class.attribute

 

즉 값을 수정하는 경우에는 어떻게 나올까? 그대로 30으로 수정될까? 아니면 다른 사이드 이펙트가 있을까?

 

 

기존 my_class에는 아무 attribute(속성)도 들어있지 않지만, "my_class.attribute = 30"을 해주자, {'attribute': 30}이라는 콘솔 로그처럼 새로운 속성이 생긴 것을 알 수 있다. 만약 이 상태에서 수정 전에는 어떤 값을 가져오고 값을 넣어주는 수정 후에는 어떤 값을 가져오게 될까? 

 

 

위의 경우처럼 처음에는 __get__()을 통해 20을 가져오지만, 그 후에는 30을 가져온다. 왜냐하면 위에서 언급한 대로 현재 디스크립터는 비 데이터 디스크립터이고 "인스턴스 변수 > 비 데이터 디스크립터"순으로 우선순위가 있기 때문에 인스턴스에 속성(변수)가 생긴 뒤로는 인스턴스 변수가 우선순위가 더 높아 변수에 있는 값인 30을 가져오는 것이다.

 

4. Django(장고) ORM, 모델에서 Field(필드)를 지정하는 경우

 

이런 패턴을 장고를 자주 사용해 보신 분이라면 비슷한 패턴이라고 경험해 봤을 것이다. 위의 코드 패턴이 장고에서 모델을 정의할 때 사용하는 패턴과 유사하기 때문이다. 실제 장고 코드를 보면 어떤 말인지 감이 올 것이다.

 

from django.db import models

class Book(models.Model):

    title = models.CharField(max_length=255)
    published_date = models.DateField(null=True, blank=True)

    def __str__(self):
        return self.title

 

여기서 눈치 빠르신 분이라면 알아 챘겠지만, "models.CharField(), models.DateField()"이 둘은 디스크립터이며, 실제로도 그렇게 작동한다. 물론 위에서 설명한 부분처럼 __get__()이나 __set__()이 정확하게 구현되어 있는 것은 아니다. 실제로는 Field 클래스가 CharField class나 DateField등에 디스크립터를 심어주는 역할을 한다. 실제로 이 역할을 하는 함수가 Field 클래스에 있는 contribute_to_class 메소드이다. 이 구현을 살짝 봐보면 다음과 같다.

 

def contribute_to_class(self, cls, name, private_only=False):

	"""
    Register the field with the model class it belongs to.

    If private_only is True, create a separate instance of this field
    for every subclass of cls, even if cls is not an abstract model.
    """
        
    self.set_attributes_from_name(name)
    self.model = cls
    cls._meta.add_field(self, private=private_only)
        
    if self.column:
        setattr(cls, self.attname, self.descriptor_class(self))
            
    if self.choices is not None:
        # Don't override a get_FOO_display() method defined explicitly on
        # this class, but don't check methods derived from inheritance, to
        # allow overriding inherited choices. For more complex inheritance
        # structures users should override contribute_to_class().
            
        if "get_%s_display" % self.name not in cls.__dict__:
            setattr(
                cls,
                "get_%s_display" % self.name,
                partialmethod(cls._get_FIELD_display, field=self),
            )

 

위의 메소드가 모델 정의 시점에 호출되어서 Field가 모델 클래스에 자신을 등록하고 실제 작동할 디스크립터를 심어준다. 여기서 위의 파라메터 중 cls는 지금 생성되고 있는 모델 클래스로 위의 코드 예시에서는 Book이 되며 name은 변수 명으로 지정한 필드 명으로 위의 코드에서는 title, published_date가 된다. 위의 코드에서 가장 중요한 부분이 바로 디스크립터를 주입하는 부분인 "cls._meta.add_field(self, private=private_only)"부분과 "setattr(cls, self.attname, self.descriptor_class(self))" 부분이다. add_field를 통해 메타 데이터에 필드 객체를 등록하고 setattr(cls, self.attname, self.descriptor_class(self))를 통해 기존 Field객체를 descriptor_class(self) 즉 디스크립터로 교체한다. 

 

※ 참고로 이 descriptor_class는 DeferredAttribute 구현체를 받는데, 이 DeferredAttribute 클래스의 경우 장고에서 자주 언급되는 LazyLoading을 구현하는 디스크립터이다.

 

즉 이런 식으로 장고는 디스크립터를 활용하여 장고의 데이터베이스 모델 정의를 활용하고 있다.

 

5. 번외 __set_name__

 

디스크립터를 만들 때 디스크립터가 처리하려 하는 attribute의 이름을 알아야 하는데, 이를 간편하게 해주는 것이 __set_name__ 매직 메소드이다. __set_name__  메소드는 파라메터로 디스크립터를 소유한 클래스와 이름을 받으며 이를 지정해 두면 필요한 이름을 지정할 수 있다. 코드로 보면 다음과 같다. 

 

class MyDescriptor:
	
    def __get__(self, instance, owner):
        print("Get a value of attribute")
        return 10
    
    def __set__(self, instance, value):
    	print("Set a value of attribute")
    
    def __set_name__(self, owner, name):
    	self.name = name
    
class MyClass:
	
    attribute = MyDescriptor()
    
    def __init__(self):
    	self.x = 20
728x90
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유