파이썬 클린코드를 읽으며 정리한 내용입니다.
__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)
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
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
__set__
에서 막아줘야 한다.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__
메서드로 읽고 쓸때 사용된다.__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.__dict__
에서 descriptor라는 이름의 키를 찾지 못하고 클래스에서 디스크립터를 찾게 된다. 이때문에 __get__
메서드가 반환되게 된다.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
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__()
메서드가 먼저 호출되고 필요한 변환을 한다.instance.method(...)
구문에서는 괄호 안의 인자를 처리하기 전에 instance.method
부분을 먼저 평가한다.__get__
메서드가 있기 때문에 __get__
메서드가 호출된다. (디스크립터 프로토콜)__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')
정리가 잘 되어있네요 잘봤습니다!