파이썬 클린코드 발표 정리 디스크립터 분석

HHHHH·2021년 8월 8일
0

개념정리

목록 보기
6/6

다시 복습하면 좋은 파트

p 49 프로퍼티
p 59 callable 객체
p159 데코레이터를 디스크립터로 구현하기

좋은 디스크립터의 기준은? 우리는 적절히 쓰는 것일까 오버 엔지니어링 하는 것일까?

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

디스크립터를 자세히 분석하기 위해 파이썬 내부의 활용부터 살펴보자.
파이썬 자체의 디스크립터를 보면 좋은 디스크립터 활용을 알 수 있다.

함수와 메서드

함수는 __get__ 메서드를 구현했기 떄문에 클래스 안에서 메서드 처럼 동작할 수 있음.

메서드는 추가 파라미터를 가진 함수일 뿐임. 관습적으로 메서드의 첫 번째 파라미터는 "self"라는 이름을 사용하여 메서드를 소유하고 있는 클래스의 인스턴스를 나타냄.


class MyClass:
	def method(self, ...):
    	self.x = 1
        

가 실제로는


class MyClass: pass

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

로 정의하는 것과 같음
따라서 메서드는 객체를 수정하는 또 다른 함수일 뿐이며, 객체 안에서 정의 되었기 떄문에 객체에 바인딩 되었다고 말함.


instacne = MyClass()
instance.method()

instance = MyClass()
MyClass.method(instance, ...)

로 처리함 단 이들은 디스크립터의 도움을 받아 내부적으로 처리되는 구문 변환일뿐임.

함수는 디스크립터 프로토콜을 구현했으므로 메서드를 호출하기 전에 __get__() 메서드가 먼저 호출 되고 필요한 변환을 함.

>>> def function(): pass
...
>> function.__get__
<method-wrapper '__get__' of function object at 0x00000245D769D3A8>

methond는 클래스 속성으로 정의된 객체이고 __get__ 메서드가 있기 떄문에 __get__ 메서드가 호출되고
__get__ 가 함수를 메서드로 변환하는 것임. 즉 함수를 작업하려는 객체의 인스턴스에 바인딩함.

예제

함수 또는 메서드를 클래스 내에 호출 가능한 객체로 정의할 것임.
METHOD 클래스의 인서턴스는 함수나 메서드 형태로 다른 클래스에서 사용 될 것임.
이 함수는 단지 전달 받은 3개의 인자를 그대로 출력함.

첫번째 파라미터는 instance로 클래스에 정의될 경우 self 가 됨.

__call__() 메서드에서느 self는 MyClass의 인스턴스가 아니라 Method의 인스턴ㅅ흐를 나타내는 것에 주의.
파라미터로 전달된 instance가 MyClass 타입의 객체임.

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")
External call: <__main__.MyClass object at 0x00000245D765D908> 호출됨. 인자는 first와 second입니다.

>>>instance.method("first", "second")
Traceback (most recent call last)
<ipython-input-132-c31942aa7798> in <module>
----> 1 instance.method("first", "second")

TypeError: __call__() missing 1 required positional argument: 'arg2'

파라미터의 위치가 한 칸씩 밀려서 Method.__call__ 기준으로 self 자리에 instance가 전달되고,
instance 자리에 "first"가 전달되고, arg1 자리에 "second"가 전달되는데, arg2 자리에는 아무 것도 전달되지 않았기 떄문임.

해결을 위해서는 메서드를 디스크립터로 변경하면 됨.
그렇게하면 instance.method 호출 시 Method.__get__ 메서드를 먼저 호출 할것이고,
여기에서 첫 번째 패러미터로 Method의 인스턴스를 전달함으로써 객체에 바인딩 할 수 있음.


from type 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)

types 모듈의 MethodType을 사용하여 함수(※ 예제에서는 호출 가능한 객체 Method를 만들어 설명)
를 메서드로 변환하는 것임. 이 클래스의 첫 번째 파라미터는 호출 가능한 것이여야 함.
(여기서는 self인데 self는 Method의 인스턴스로 __call__ 메서드를 구현했으므로 호출 가능한 형태)

두번째 객체는 이 함수에 바인딩할 객체임.

파이썬 함수 객체도 이것과 비슷하게 동작함. 따라서 클래스 내부에 함수를 정의할 경우 메서드처럼 사용할 수 있는 것임.

매우 우아한 처리 방법으로 사용자 정의 객체를 만들 떄도 이러한 파이썬 스러운 접근 방식을 염두에 두는 것이 좋음.

예를 들어 사용자 정의 호출 가능한 객체를 정의할때 지금처럼 디스크립터로 만들어서 클래스 속성으로도 사용할 수 있도록 하는 것이 좋음.

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

@property, @classmethod와 @staticmethod 데코레이터는 디스크립터임

메서드를 인스턴스가 아닌 클래스에서 호출 할때는 관습적으로 디스크립터 자체를 반환함.
프로퍼티를 클래스에서 직접 호출하면 계산할 속성이 없으므로 일종의 디스크립터인 프로퍼티 객체 자체를 반환함.

@classmethod를 사용하면 디스크립터의 __get__ 함수가 메서드를 인스턴스에서 호출하든 클래스에서 직접호출하든
상관없이 데코레이팅 함수에 첫 번째 파라미터로 메서드를 소유한 클래스를 넘겨줌.

@staticmethod를 사용하면 정의한 파라미터 이외의 파라미터를 넘기지 않도록함. 즉 , __get__메서드에서 함수의 첫 번째 파라미터에 self를 바인딩하는 작업을 취소함.

예를 들어 @property 데코레이터 처럼 동작하지만 클래스를 대상으로 한다느 ㄴ것만 다른
@classproperty 데코레이터를 만들었다고 가정해보자. 이 데코레이터를 잘 만들었다면 다음과 같은 코드가 잘 동작행해야함

class TableEvent:
    schema = "public"
    table = "user"
    
    
    @classproperty
    def topic(cls):
        prefix = read_prefix_from_config()
        return f"{prefix}{cls.schema}.{cls.table}"

>>> TableEvent.topic
'public.user'

>>> TableEvent().topic
'public.user'

슬롯(slot)

클래스에 __slot__ 을 정의하면 클래스가 기대하는 특정 속성만 정의하고나머지는 제한 가능.

__slot에 정의되지 않은 속성을 동적으로 추가하려할 경우 AttributeError 가 발생함.
이 속성ㅇ르 정의하면 클래스는 정적이 되고 __dict
속성을 가지지 않음.

객체에 사전이 없지만 속성을 가져올 수 있는 이유는 디스크립터를 사용하기 때문
__slot__에 정의된 이름마다 디스크립터를 만들어서 값을 저장하고 있으므로 나중에 검색도 가능함.

class Coordinate2D:
    __slots__ = ("lat", "long")

    def __init__(self, lat, long):
        self.lat = lat
        self.long = long

    def __repr__(self):
        return f"{self.__class__.__name__}({self.lat}, {self.long})"
"""
>>> coord = Coordinate2D(1, 2)
>>> repr(coord)
'Coordinate2D(1, 2)'
"""

파이썬의 동적인 특성을 없애기 때문에 조심해서 사용해야함.
정적인 객체에만 사용하고, 다른 부분에서 절대로 동적인 속성 추가할일이 없다는 것을 확신할 때에만 사용.

사전 형태가 아니라 고정된 필드의 값만 저장하면 되기 때문에 메모리를 덜 사용함.

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

5장 p159쪽 복습

요약

디스크립터는 파이썬의 경계를 보다 메타프로그래밍1에 가깝게 해주는 고급 기능임.

파이썬의 클래스는 일반 객체일뿐임으로 속성을 갖고 속성과 상호 교류할 수 있다는 점을 명확하게 해줌.
이를 통해 디스크립터 프로토콜은 보다 진보된 형태의 객체 지향 기능을 활용하도록 촉진.

디스크립터를 활용함으로써 강력한 추상화를 통해 깔끔하고 컴팩트한 클래스를 만들 수 있음.

불필요한 오버 엔지니어링에 사용되지 않도록 주의해야함. 내부 API, 라이브러리, 프레임 워크 디자인과 같은 일반적인 경우에 대해서만 사용해야함.

1: 메타프로그래밍(meaprogramming) 프로그램을 데이터 처럼 다룰 수 있게 하는 프로그래밍 기법을 뜻함. 다른 프로그램을 읽고, 생성하고, 분석하고, 변형하고 심지어 런타임 중에도 수정할 수 있게 함.

profile
공부중

0개의 댓글