ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] 디스크립터(Discriptor)
    Python/Python 2024. 7. 3. 14:09
    반응형

    디스크립터란 무엇인가?

    파이썬에서는 다른언어와 다르게 디스크립터 라는 개념이 있다.
    이게 무엇이냐면  객체 속성을 참조했을 때 어떻게 되어야 하는지 커스터마이즈 할 수 있다.
    다시 말해 클래스는 한 속성의 관리를 다른 클래스에게 위임(delegate) 할 수 있다.

    디스크립터 클래스들은 세 개의 특별한 메서드에 기반하며, 이 클래스들은 디스크립터 프로토콜을 형성한다.

    • __set__(self, obj, vaule): 흔히 아는 세터 (Setter)다.
    • __get__(self, obj, owner=None): Getter
    • __detete__(self, obj): 속성에서 del이 불렸을 때 호출

    __get__, __set__ 을 구현한 디스크립터를 데이터 디스크립터라고 부른다, 
    __get__ 만 구현한 디스크립터를 비데이터 디스크립터라 부른다,

    디스크립터의 메서드 속성을 확인하기 위해 __getatrribute__() 메서드로 호출되며 이것이 암묵적으로 호출되고 이것으로 디스크립터인지 확인한다.

    실습

    class RevealAccess(object):
        """데이터 데커레이터로 일반적인 값을 설정하고 반환하며, 접근에대한 로깅메시지를 표기한다."""
    
        def __init__(self, initval=None, name="var"):
            self.val = initval
            self.name = name
    
        def __get__(self, obj, objtype):
            print("Retrieving", self.name)
            return self.val
    
        def __set__(self, obj, val):
            print("Updating", self.name)
            self.val = val
    
        def __delete__(self, obj):
            print("Deleting", self.name)
    
    
    class MyClass(object):
        x = RevealAccess(10, 'var "x"')
        y = 5
    
    
    m = MyClass()
    m.x  # get이 호출되면서 Retrieving var"x"가 출력된다.
    m.x = 20  #  set이 호출되면서 Updating var"x"가 출력된다.
    
    del m.x  # delete가 호출되면서 Delete var "x"가 출력된당

    해당 코드를 통해 디스크립터(get,set,delete) print로 출력한다. 

     

    그래서 이게 getter/setter라고 하면 되지 왜 굳이 디스크립터로 나누지? 다른게 있나?

    다음과 같은 차이점이 있다.

    정의 위치:

    • Getter/Setter: 인스턴스 메서드로 정의되며 인스턴스 속성 접근을 제어함
    • Descriptor: 클래스 레벨에서 정의되며 인스턴스 및 클래스 속성 접근을 모두 제어함

    유연성:

    • Getter/Setter: 특정 속성에 대해 제한된 동작만을 정의합니다.
    • Descriptor: __get__, __set__, __delete__ 메서드를 통해 다양한 속성 접근 동작을 정의할 수 있으며, 여러 클래스에서 재사용할 수 있습니다.

    사용 편의성:

    • Getter/Setter: 단순한 속성 접근 제어에 적합하며, 코드가 더 직관적이고 간단함
    • Descriptor: 더 복잡한 속성 접근 로직을 구현해야 할 때 유용하며, 보다 정교한 제어가 가능함

    재사용성:

    • Getter/Setter: 특정 클래스의 특정 속성에 국한됨
    • Descriptor: 여러 클래스에 걸쳐 재사용될 수 있는 독립적인 구성 요소

    파이썬은 디스크립터 프로토콜을 이용해 클래스 함수를 인스턴스 메서드로 바인드 한다.

    디스크립터는 classmethod와 staticmethod 데커레이터 메커니즘의 기반이 된다. 실제로 해당 함수 객체가 비데이터 객체이기 때문이다.

    print(hasattr(function, "__get__"))  # True
    print(hasattr(function, "__set__"))  # False

    위 코드처럼 hasattr 메서드로 결과를 출력하면 get은 True, set은 False로 나뉜다. 따라서 이것은 비데이터 디스크립터이다.

    그래서 __dict__메서드가 비 데이터 디스크립터 보다 우선하지 않는다면, 이미 생성된 인스턴스으 특정 메서드를 런타임에 동적으로 오버라이딩 할 수 없다. 

    그래서 멍키패칭이라는 기법을 이용해서 서브 클래싱 하지 않고도 어떤 애드혹으로 동작하도록 변경한다.

    멍키패칭;  대충 아래와 같은 코드로 함수를 바꿔치기하여 오버라이딩함.

    class MyClass:
        def greet(self):
            return "Hello"

    def new_greet(self):
        return "Hello, Monkey Patch!"
    MyClass.greet = new_greet
    obj = MyClass()
    print(obj.greet())

    속성평가지연;사용 예시) 클래스가 임포트 되는 시점에 아직 사용할 수 없는 콘텍스트에 따라 이 속성들을 초기화 해야하는 경우가 있따. 이럴때 컴퓨팅 자원들이 필요하지만, 클래스가 임포트 되는 시점에 그 사용 여부를 알 수 있는 경우도 있다.

    class InitOnAccess:
        def __init__(self, init_func, *args, **kwargs):
            self.klass = init_func
            self.args = args
            self.kwargs = kwargs
            self._initialized = None
    
        def __get__(self, instance, owner):
            if self._initialized is None:
                print("initialized")
                self._initialized = self.klass(*self.args, **self.kwargs)
            else:
                print("cached!")
            return self._initialized

    해당 (디스크립터)클래스는 몇몇 print() 호출을 포함하고 잇으며 이를 이용해 값에 접근 시 초기화되는지, 캐시에 해당 값에 접근하는지 확인 할 수 있다.

    그리고, 다른 클래스가 있다고 가정해보자.
    이 클래스는  정렬된 무작위 값으로 이루어진 공용 리스트에 접근할 수 있다, 또한 리스트의 길이는 자유롭게 변할 수 있고, 모든 인스턴스는 하나의 리스트를 재사용한다.  매우 긴 리스트를 정렬하는데는 많은 시간 이 걸릴 것 이다.

    import random
    
    
    class InitOnAccess:
        def __init__(self, init_func, *args, **kwargs):
            self.klass = init_func
            self.args = args
            self.kwargs = kwargs
            self._initialized = None
    
        def __get__(self, instance, owner):
            if self._initialized is None:
                print("initialized")
                self._initialized = self.klass(*self.args, **self.kwargs)
            else:
                print("cached!")
            return self._initialized
    
    
    class WithSortedRandoms:
        lazily_initialized = InitOnAccess(sorted, [random.random() for _ in range(5)])
    
    
    m = WithSortedRandoms()
    print(m.lazily_initialized)  # initialized
    # [0.09225735667653667, 0.4460156144933808, 0.4802389291835204, 0.7955825518506061, 0.826629276623519]
    print(m.lazily_initialized)  # cached!
    # [0.09225735667653667, 0.4460156144933808, 0.4802389291835204, 0.7955825518506061, 0.826629276623519]

    디스크립터로 정의된 클래스(InitOnAccess)에  WithSortedRandom클래스를 만들어서 호출 하므로 처음에 생성하고 그 후 캐시가 된다. 이것을 지연적 방법으로 디스크립터를 활용해서 호출한 예씨이다.

     

    class lazy_proerty(object):
        def __init__(self, function):
            self.fget = function
    
        def __get__(self, obj, cls):
            value = self.fget(obj)
            setattr(obj, self.fget.__name__, value)
            return value
    
    
    class WithSortedRandoms:
        @lazy_proerty
        def lazily_initialized(self):
            return sorted([[random.random() for _ in range(5)]])

    setattr() 함수로 위치 전달한 위치 인수의 속성을 이용하여 객체 인스턴스의 속성을 설정할 수 있다.
    이때의 형태는 self.fget.__name__이다.  이런 형태로 구성 된 이유는 lazy_property가 디스크립터가 해당 메서드의 데커레이터이며, 프러퍼티 데커레이터로 사용하기 위해서이다.

    인스턴스 속성은 디스크립터 보다 우선하므로, 해당 클래스 인스턴스에 대해서는 더 이상 초기화가 수행 되지 않는다.

    이 기법을 사용하려면 두 가지 요구사항을 동시에 만족해야 한다.

    • 하나의 객체 인스턴스가 클래스 속성으로 저장되어, 해당 클래스의 인스턴스 사이에서 공유된다( 리소스 절약 목적)
    • 이 객체는 임포트 시점에 초기화 되어서는 안된다. 생성 프로세스는 몇몇 글로벌 애플리케이션 상태 콘택스트에 의존하기 때문이다.

    OpenGL을 이용해 작성한 애플리케이션에서는 이런 상황을 자주 만날 수 있다. OpenGL에 셰어더를 생성하는 비용은 상당히 크다.  최초 한번은 코드를 작성하고 동시에 이를 요구하는 클래스들에 가까이 정의해야 하는 것은 정의해야 하는 것은 충분히 합리적이다. 한편 셰이더 컴파일은 OpenGL 콘텍스트 초기화를 해야만 이를 수행할 수 있기 때문에 임포트 시점에 글로벌 모듈 네임스페이스에서 신뢰성 있게 이들을 정의하고 컴파일 하기는 어렵다.

     

    import glfw
    from OpenGL.GL import *
    from OpenGL.GL.shaders import compileProgram, compileShader
    import numpy
    
    
    # lazy_class_attribute 정의
    class lazy_class_attribute:
        def __init__(self, func):
            self.func = func
            self.attr_name = f"_{func.__name__}"
    
        def __get__(self, instance, owner):
            if not hasattr(instance, self.attr_name):
                setattr(instance, self.attr_name, self.func(instance))
            return getattr(instance, self.attr_name)
    
    
    # OpenGL 프로그램 클래스
    class OpenGLApp:
        def __init__(self):
            # GLFW 초기화
            if not glfw.init():
                raise Exception("GLFW can't be initialized")
    
            # 윈도우 생성
            self.window = glfw.create_window(800, 600, "OpenGL Window", None, None)
            if not self.window:
                glfw.terminate()
                raise Exception("GLFW window can't be created")
    
            glfw.make_context_current(self.window)
    
            # 셰이더 프로그램 초기화
            self.shader = self.init_shader_program
    
        @lazy_class_attribute
        def init_shader_program(self):
            vertex_src = """
            #version 330 core
            layout(location = 0) in vec3 position;
            void main()
            {
                gl_Position = vec4(position, 1.0);
            }
            """
    
            fragment_src = """
            #version 330 core
            out vec4 FragColor;
            void main()
            {
                FragColor = vec4(1.0, 1.0, 1.0, 1.0);
            }
            """
    
            vertex_shader = compileShader(vertex_src, GL_VERTEX_SHADER)
            fragment_shader = compileShader(fragment_src, GL_FRAGMENT_SHADER)
            shader = compileProgram(vertex_shader, fragment_shader)
            return shader
    
        def run(self):
            # 삼각형 데이터
            vertices = [-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0]
            vertices = numpy.array(vertices, dtype=numpy.float32)
    
            # VAO와 VBO 생성
            VAO = glGenVertexArrays(1)
            VBO = glGenBuffers(1)
    
            glBindVertexArray(VAO)
    
            glBindBuffer(GL_ARRAY_BUFFER, VBO)
            glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
    
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
            glEnableVertexAttribArray(0)
    
            glBindBuffer(GL_ARRAY_BUFFER, 0)
            glBindVertexArray(0)
    
            # 메인 루프
            while not glfw.window_should_close(self.window):
                glfw.poll_events()
    
                glClear(GL_COLOR_BUFFER_BIT)
    
                glUseProgram(self.shader)
                glBindVertexArray(VAO)
                glDrawArrays(GL_TRIANGLES, 0, 3)
                glBindVertexArray(0)
                glUseProgram(0)
    
                glfw.swap_buffers(self.window)
    
            # 정리
            glDeleteVertexArrays(1, [VAO])
            glDeleteBuffers(1, [VBO])
            glfw.terminate()
    
    
    # 실행
    if __name__ == "__main__":
        app = OpenGLApp()
        app.run()

    PyOpenGL을 활용하여 작성한 lazy_property 데커레이터를 활용한 예시이다.

    디스크립터는 클래스의 기본적인 동작에 영향을 미친다.

    GUI프로그램 같이 클래스에 임폴트 할 때 말고 불러올때 해야 콘택스트에 적합하다

     

    일반적으로 pyqt나 다른 GUI라이브러리에서는 직접 lazy_property를 구현하지 않고 내장메서드로 존재하긴한다

     

     

    반응형

    댓글

Designed by Tistory.