Computer Engineering/Fluent Python 정리

Fluent Python Chapter 21. 클래스 메타프로그래밍

jordan.bae 2022. 4. 24. 16:50

Introduction

21장은 클래스 메타프로그래밍입니다. 런타임에 클래스를 생성하고 변경하는 프로그래밍 방법을 의미합니다. 파이썬 Guru개발자들을 프레임워크를 만드는것이 아니면 해당 기능을 사용하는 것을 권장하지 않습니다. 해당 기능이 주는 편리함보다는 해치는 가독성이 더 크기 때무일 것 같습니다. 인터프리터단에서 동작하는 부분이 많을수록 파이썬으로 어플리케이션을 주로 개발하는 개발자들에게는 가독성이 좋지 않을 수 있습니다. 해당 장에서는 클래스를 생성하는 함수를 이용해서 클래스를 만드는 것을 시작으로 클래스 데커레이터, 임포트 타임과 런타임, 메타클래스에 대해서 살펴봅니다. 지난 장에서 descriptor 객체의 storage_name이 구분하기 어려웠던 것들을 클래스 데커레이터와 메타클래스를 사용해서 해결하는 과정도 포함되어 있습니다. 메타클래스는 제대로 사용하기 어려운 기능이기 때문에 해당 장의 내용만 읽었다고 충분히 이해하고 사용하기는 어려워 보이지만 해당 장을 이해하려고 노력한다면 클래스 메타프로그래밍을 이해하기 좋은 시작이고 프레임워크의 코드들을 이해하는데 도움이 될 것 같습니다.

 

책 정리를 시작한 이유

책 정리를 시작한 이유

Chapter1의 Introduction 부분에서 이야기 한 것처럼 지난 5년간 다양한 언어나 프레임워크 및 프로그램을 공부하고 이용하여 소프트웨어를 개발했는데 이것저것 하다 보니 자주 사용하는 언어임에도 불구하고 파이썬을 잘 활용하고 있느냐에 대한 답변을 자신 있게 하기 어렵다고 느껴서 Fluent Python이라는 책을 공부하며 정리하고 있습니다. 올해에는 새로운 기술들도 좋지만 기존에 활용하던 언어나 프레임워크 그리고 소프트웨어를 더 잘 사용할 수 있도록 깊게 공부할 수 있는 한 해를 만들고 싶은 소망을 가지고 있습니다. 21장까지 정리를 성공하고 맛있는 걸 먹으면서 스스로 축하할 날이 어서 왔으면 좋겠네요! 혹시, 제가 정리한 글이 괜찮으시면 블로그를 구독 하시면 다음편이 나오면 바로 알림을 받으실 수 있습니다. 어느 덧 21장을 정리하는 날이 왔네요. 처음에 호기롭게 2월 이내에 끝내야지라고 생각했는데 어느 덧 4월 말이네요. 처음 목표한 일정을 지키지는 못했지만 끝까지 포기하지 않은 것 하나만으로도 저에게는 큰 성과인 것 같습니다. 이 책에서는 읽을 거리라는 part에 정말 좋은 자료들을 소개하는데 그 부분까지는 살펴보지 못해서 책을 한 번 다시 읽으면서 그 부분도 같이 살펴보고 싶습니다.

 

지난 chapter 정리한 포스팅

Fluent Python Chapter 1. 파이썬 데이터 모델 (Feat. 일관성 있는 언어)

Fluent Python Chapter 2-1. Sequence (Sequence의 분류, listcomp, generator, tuple, slicing

Fluent Python Chapter 2-2. Sequence (bisect, deque, memoryview)

Fluent Python Chapter 3. Dictionary (feat. hashtable)

Fluent Python Chapter 4. 텍스트와 바이트 (feat. 깨지지마라..)

Fluent Python Chapter 5. 일급 함수

Fluent Python Chapter 6. 일급 함수 디자인 패턴

Fluent Python Chapter 7. 함수 데커레이터와 클로저 (feat. 메타프로그래밍)

Fluent Python Chapter 8. 객체 잠조, 가변성, 재활용

Fluent Python Chapter 9. 파이썬스러운 객체

Fluent Python Chapter 10. 시퀀스 해킹, 해시, 슬라이스

Fluent Python Chapter 11. 인터페이스: 프로토콜에서 ABC까지

Fluent Python Chapter 12. 내장 자료형 상속과 다중 상속

Fluent Python Chapter 13. 연산자 오버로딩(feat. 제대로 하기)

Fluent Python Chapter 14. 반복형, 반복자, 제너레이터

Fluent Python Chapter 15. Context manager와 else 블록

Fluent Python Chapter 16. 코루틴 (Coroutine)

Fluent Python Chapter 17. Future를 이용한 동시성

Fluent Python Chapter 18. asyncio를 이용한 동시성

Fluent Python Chapter 19. 동적 속성과 프로퍼티

Fluent Python Chapter 20. 속성 디스크립터

 

 

 

클래스 팩토리 (Class Factory)

클래스 팩토리는 문자 그대로 클래스를 생성하는 도구라고 할 수 있습니다.. 클래스도 일급 객체이기 때문에 파이썬에서는 함수로 클래스 팩토리를 만들 수 있습니다. 책에서는 예시 중 하나로 이 책에서 자주 사용한 표준 라이브러리인 collections.namedtuple()이라는 클래스 팩토리를 소개합니다. 

 

아래와 같은 클래스가 많이 필요하다고 할 때 클래스 팩토리를 사용해서 반복되는 코드를 줄일 수 있습니다.

 

# 이런 스타일의 class가 반복
class Dog:
	def __init__(self, name, weight, owner):
    	self.name = name
        self.weight = weight
        self.owner = owner
        
# 클래스 팩토리 함수
def recrod_factory(cls_name, field_names):
    try:
        field_names = field_names.replace(',', ' ').split()
    except AttributeError:
        pass
    field_names = tuple(field_names)
    
    def __init__(self, *args, **kwargs):
        attrs = dict(zip(self.__slots__, args))
        attrs.update(kwargs)
        for name, value in attrs.items():
            setattr(self, name, value)
    
    def __iter__(self):
        for name in self.__slots__:
            yield getattr(self, name)
    
    def __repr__(self):  # <5>
        values = ', '.join('{}={!r}'.format(*i) for i
                           in zip(self.__slots__, self))
        return '{}({})'.format(self.__class__.__name__, values)
    
    
    cls_attrs = dict(__slots__ = field_names,
                     __init__  = __init__,
                     __iter__  = __iter__,
                     __repr__  = __repr__)
    return type(cls_name, (object,), cls_attrs)


Dog = recrod_factory('Dog', 'name weight height')
dog = Dog('cogi', 20, 150)
dog.name
'cogi'

dog
Dog(name='cogi', weight=20, height=150)

Dog.__mro__
(__main__.Dog, object)

여기서 좀 유심하게 볼 부분은 type을 이요해서 새로운 클래스를 생성하는 부분입니다.

type()은 함수로 생각하기 쉽지만 클래스입니다. 위의 코드 처럼 type('class name', 'base classes', 'class attributes')을 전달하면 새로운 클래스를 생성하는 일종의 클래스처럼 작동합니다. 즉, type의 객체가 새로운 클래스라고 할 수 있습니다. 

 

# type signature
type(name, bases, dict)

__dict__를 사용하지 않고 __slots__클래스의 속성을 사용한 이유는 9장에서 소개한 dictionary가 아닌 tuple에 객체속성을 저장해서 메모리 공간을 절약할 수 있는 장점이 있습니다. (물론 단점도 있습니다. 조금 더 알아보고 싶은 신 분은 Fluent Python Chapter 9. 파이썬스러운 객체 여기를 참고하시면 됩니다.)

 

 

클래스 데커레이터

20장에서 LineItem.weight.storage_name 이 `_Quantity#0` 이렇게 표시되었었는데 이 부분을 21장에서 개선해보기로 했었습니다. 이렇게 표시되면 디버깅하기 어렵기 때문에 객체의 속성명의 표시되면 좋지만 디스크립터 클래스가 인스턴스가 초기화 될 때 대상 클래스의 속성명을 알 수 없었기 때문에 한계가 있었습니다. __new__() 생성자 매직 메서드를 수정하면 되지만 인스턴스 메서드이기 때문에 객체가 생성될 때 마다 동작함으로 컴퓨터 자원의 낭비입니다. 그렇기 때문에 클래스 단의 동작에서 수행해야 하는데 이를 위해서 클래스 데커레이터나 클래스 메타클래스를 사용할 수 있습니다. 코드를 보면 아래와 같습니다.

(그 전의 클래스들은 20장에 있기 때문에 생략했습니다. 코드를 돌려보실 분들은 reference에 적혀있는 github repo에 링크에 있는 코드를 확인해보시면 됩니다.)

 

# BEGIN MODEL_V6
def entity(cls):  # <1>
    for key, attr in cls.__dict__.items():  # <2>
        if isinstance(attr, Validated):  # <3>
            type_name = type(attr).__name__
            attr.storage_name = '_{}#{}'.format(type_name, key)  # <4>
    return cls  # <5>
    

@entity  # <1>
class LineItem:
    description = NonBlank()
    weight = Quantity()
    price = Quantity()

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

 

임포트 타임과 런타임

전에 함수 decorator를 살펴볼 때 가볍게 임포트 타임과 런타임에 대해서 살펴봤었습니다.

메타 프로그래밍을 이해하려면 각 코드 블록이 파이썬 인터프리터에 의해 언제 평가되는지 이해해야 합니다. 

 

책에서 소개 된 부분을 그대로 가져와 보면

임포트 타임에 인터프리터는 .py 모듈에 들어 있는 소스 코드를 위에서부터 한 번 파싱하고, 실행할 바이트 코드를 생성한다. 구문에러가 있으면 이때 발생한다. 만일 지역 __pycahe__ 디렉토리에 최신 .pyc 파일 있으면 바이트코드를 실행할 준비가 된 것이므로 이 과정을 생략한다.

위의 작업에서 런타임 작업과 모호한 부분이 있는데 클래스의 경우는 최상위 수준 코드가 실제로 실행되고  함수같은 경우는 객체를 전역 이름에 바인딩하지만 실행은 되지 않는다.  즉, 클래스는 최상 수준 코드라고 할 수 있다.

 

책에서는 예제 코드를 보여주면서 import time과 runtime에 실행되는 순서를 예측해보라고 하는데 책을 확인해서 해보시는것을 추천드립니다. import time만 확인 해보면 아래와 같습니다.

 

# evaltime.py
   
from evalsupport import deco_alpha

print('<[1]> evaltime module start')


class ClassOne():
    print('<[2]> ClassOne body')

    def __init__(self):
        print('<[3]> ClassOne.__init__')

    def __del__(self):
        print('<[4]> ClassOne.__del__')

    def method_x(self):
        print('<[5]> ClassOne.method_x')

    class ClassTwo(object):
        print('<[6]> ClassTwo body')


@deco_alpha
class ClassThree():
    print('<[7]> ClassThree body')

    def method_y(self):
        print('<[8]> ClassThree.method_y')


class ClassFour(ClassThree):
    print('<[9]> ClassFour body')

    def method_y(self):
        print('<[10]> ClassFour.method_y')


if __name__ == '__main__':
    print('<[11]> ClassOne tests', 30 * '.')
    one = ClassOne()
    one.method_x()
    print('<[12]> ClassThree tests', 30 * '.')
    three = ClassThree()
    three.method_y()
    print('<[13]> ClassFour tests', 30 * '.')
    four = ClassFour()
    four.method_y()


print('<[14]> evaltime module end')
# evalsupport.py

print('<[100]> evalsupport module start')

def deco_alpha(cls):
    print('<[200]> deco_alpha')

    def inner_1(self):
        print('<[300]> deco_alpha:inner_1')

    cls.method_y = inner_1
    return cls


class MetaAleph(type):
    print('<[400]> MetaAleph body')

    def __init__(cls, name, bases, dic):
        print('<[500]> MetaAleph.__init__')

        def inner_2(self):
            print('<[600]> MetaAleph.__init__:inner_2')

        cls.method_z = inner_2


print('<[700]> evalsupport module end')

 

위와 같이 두 파일이 있을 때

파이썬 콘솔에서 아래와 같이 실행될 때 찍히는 print문의 순서를 예측해 보시면됩니다.

>>> import evaltime

 

....

결과는 아래와 같습니다.

 

>>> import evaltime
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body
<[6]> ClassTwo body
<[7]> ClassThree body
<[200]> deco_alpha
<[9]> ClassFour body
<[14]> evaltime module end

가장 먼저 `from evalsupport import deco_alpha` 이 먼저 실행됩니다.  함수 내의 print문은 출력되지 않고, class 내의 print문은 실행된다는 것을 알 수 있습니다. 저는 예상하지 못한 점은 클래스 안의 클래스 까지 실행될거라는 점이었습니다.

 

책에서 이 부분을 통해서 보려고 했던 부분은 클래스 데커레이터는 상속되지 않는다는 부분입니다. ClassFour는 ClassThree의 deco_alpha가 적용되지 않아서 method_y가 그대로 출력됩니다.

jordan.bae@jordanui-MacBook-Pro 21-class-metaprog % python evaltime.py 
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body
<[6]> ClassTwo body
<[7]> ClassThree body
<[200]> deco_alpha
<[9]> ClassFour body
<[11]> ClassOne tests ..............................
<[3]> ClassOne.__init__
<[5]> ClassOne.method_x
<[12]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1
<[13]> ClassFour tests ..............................
<[10]> ClassFour.method_y
<[14]> evaltime module end
<[4]> ClassOne.__del__

이 문제는 메타클래스를 이용해서 해결할 수 있습니다.

 

 

메타클래스

메타클래스도 클래스 팩토리의 형태 중 하나로, 함수대신 클래스일 뿐입니다.

파이썬에서 클래스 또한 객체이기 때문에 즉, 클래스 또한 어떤 클래스의 객체라는 것입니다.

기본적으로 파이썬 클래스는 type의 객체입니다. 전에 함수 클래스 팩토리에서도 type을 이용해서 클래스를 생성했었습니다.

'span'.__class__
str

str.__class__
type

type외의 메타클래스(객체가 클래스인 클래스)는 ABCMeta. Enum등의 메타클래스가 있고, 이 클래스들은 type의 자식 클래스입니다.

import collections

collections.Iterable.__class__
abc.ABCMeta

import abc
abc.ABCMeta.__class__
type

abc.ABCMeta.__mro__
(abc.ABCMeta, type, object)

책에서 이야기하는 것처럼 모든 클래스는 type의 객체로, 메타클래스는 type의 서브클래스이기도 하므로 클래스 팩토리로(메타클래스) 동작할 수 있습니다.

 

결과적으로 LineItem은 아래와같은 메타클래스의 __init__ 메서드를 변경해서 처리할 수 있습니다

# BEGIN MODEL_V7
class EntityMeta(type):
    """Metaclass for business entities with validated fields"""

    def __init__(cls, name, bases, attr_dict):
        super().__init__(name, bases, attr_dict)  # <1>
        for key, attr in attr_dict.items():  # <2>
            if isinstance(attr, Validated):
                type_name = type(attr).__name__
                attr.storage_name = '_{}#{}'.format(type_name, key)

class Entity(metaclass=EntityMeta):  # <3>
    """Business entity with validated fields"""

class LineItem(Entity):  # <1>
    description = NonBlank()
    weight = Quantity()
    price = Quantity()

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price
        
item = LineItem('ipad', 100, 100)

item.description
'ipad'

LineItem.description.storage_name
'_NonBlank#description'

 

 

마무리

21장에서는 클래스 메타프로그래밍에 대한 내용을 다뤘습니다. 클래스 팩토리의 함수 형태, 클래스 형태를 살펴봤고, type이라는 메타클래스가 어떻게 동작하는지도 살펴봤습니다. 저를 포함한 대부분의 개발자들은 프레임워크를 개발하지 않으므로 메타클래스의 동작이 익숙치 않을 것 같습니다. 파이썬이 내부적으로 여러가지를 처리 때문에 파이썬에서는 프레임워크를 사용할 때 getter, setter같은 반복적인 메서드를 구현하지 않고 생산성을 높여 개발할 수 있는 것 같습니다. 아직은 개념이 익숙하지는 않지만 다시 천천히 읽다보면 조금 더 가까질 것 같습니다. 드디어 21장까지 어떻게 어떻게 정리를 끝냈습니다. 책에 대한 마무리는 한 번 정리한 내용을 훑어보면서 정리하면서 제가 많이 느꼈던 것이나 생각한 것들에 대해서 따로 정리해보려고 합니다. 그리고 맛있는 것도 먹으면서 기념한 사진도 함께 포스팅해보도록 하겠습니다.

 

 

Reference

- Fluent Python chapter 21

- https://github.com/fluentpython/example-code/tree/master/21-class-metaprog

 

GitHub - fluentpython/example-code: Example code for the book Fluent Python, 1st Edition (O'Reilly, 2015)

Example code for the book Fluent Python, 1st Edition (O'Reilly, 2015) - GitHub - fluentpython/example-code: Example code for the book Fluent Python, 1st Edition (O'Reilly, 2015)

github.com

 

반응형