python이나 django framework를 사용하여 개발을 하다보면 가끔 이게 어떻게 되는거지? 싶은 클래스들이 있다. 대표적으로 Serializer와 Model 클래스다.
특히 Model 클래스가 그러한데 모델을 사용할때 우리는
class AModel(models.Model):
tm = models.ForeignKey(TestModel, on_delete=models.SET_NULL)
위 처럼 정의해서 사용한다.
a = AModel.objects.get(pk=1)
print(a.tm) 위처럼 사용하면 우리가 평소에 사용하던 클래스처럼
models.ForeignKey(TestModel, null=True, on_delete=models.SET_NULL)
의 반환값이 아닌 TestModel의 객체를 전달해준다 어떻게 가능한걸까?
이는 디스크립터이기 때문이다.
디스크립터가 어떤 기능을 하길래 이렇게 동작할 수 있는걸까?
간단한 디스크립터를 구현해보자
class PositiveInteger:
def __set_name__(self, owner, name):
self.private_name = "_" + name
def __get__(self, instance, owner=None):
if instance is None:
return self
return getattr(instance, self.private_name, None)
def __set__(self, instance, value):
if not isinstance(value, int) or value <= 0:
raise ValueError("양의 정수만 허용됩니다.")
setattr(instance, self.private_name, value)
class Person:
age = PositiveInteger()
def __init__(self, age):
self.age = age
p = Person(10)
print(p.age)
p.age = -10
10
Traceback (most recent call last):
File "/Users/~~.py", line 22, in <module>
p.age = -10
^^^^^
File "/Users/~~.py", line 12, in __set__
raise ValueError("양의 정수만 허용됩니다.")
ValueError: 양의 정수만 허용됩니다.
디스크립터는 get, set, delete 이 세가지 디스크립터 프로토콜 중 하나를 구현한 클래스로 구현한 객체를 말한다.
객체를 호출하면 기존 로직과는 다르게 get함수가 호출된다.
값을 수정하려고할시에는 set함수가 호출된다.
객체를 삭제하려고 할시에는 delete함수가 호출된다.
위 3가지 프로토콜을 통해 위와 같이 객체의 속성에 값을 가져오거나 수정하거나 삭제하거나 할때 특정 동작을 구현할 수 있다.
위의 개념을 이용하여 django model에서도 특정 필드에 접근할 때 DB 쿼리가 필요한 경우 DB에 쿼리를 보내 값을 가져와서 처리하는 로직으로
우리가 사용하는 장고 모델이 구현된다.
그러면 장고 모델에 필드를 선언하면 바로 디스크립터가 되는걸까?
models.CharField(blank=True, max_length=31) 필드가 디스크립터 프로토콜 함수를 가지고있는지 확인해보면
>>> models.CharField(blank=True, max_length=31).__get__
Traceback (most recent call last):
File "/usr/local/lib/python3.10/code.py", line 90, in runcode
exec(code, self.locals)
File "<console>", line 1, in <module>
AttributeError: 'CharField' object has no attribute '__get__'. Did you mean: '__ge__'?
다음과 같은 결과를 볼 수 있다. 그러면 모델의 각 필드는 디스크립터가 아니란걸까?
models.CharField(blank=True, max_length=31)를 출력해보면 다음과 같이 나온다.
<django.db.models.fields.CharField>
그런데 클래스의 필드를 확인해보면
>>> AModel.title
<django.db.models.query_utils.DeferredAttribute object at 0xffff915be2c0>
>>> AModel.title.__get__
<bound method DeferredAttribute.__get__ of <django.db.models.query_utils.DeferredAttribute object at 0xffff915be2c0>>
위처럼 get이 정의되어있는 논데이터 디스크립터라는 것을 확인할 수 있다.(set, delete 는 정의되어 있지않다)
장고 모델에 필드는 아무런 값을 넣을 수 있게 설계되어있다. 그래서 set이 구현되지않은 논데이터 디스크립터이고 필드값을 변경하더라도 save()가 호출되기 전까지 DB에 반영하지 않는 정책을 가지고있어서 위의 구조가 가능하다.
디스크립터의 get을 통해서 DB 상에서 데이터를 가져오는 지연로딩(Lazy Loading) 동작이 가능하다. 그래서 클래스명이 지연된 속성이라는 뜻을 가지고있다.