Python Metaclass 간략정리

noname2048·2021년 6월 23일
0

Python metaclass 정리

Pycon Korea 2019 리얼월드 메타클래스 - 김성현

위의 자료를 통해 정리하며 작성한 글입니다.

object

객체(object)라는 개념은 1) 클래스의 인스턴스를 뜻한다. 2) 행동, 속성을 가지는 실체 혹은 개체를 말한다. (실체: 자동차, 속성1: 4륜구동, 속성2: 바퀴는 4개, 행동: 움직이기)

컴퓨팅적사고로 보면, 실제 세상에서 실존하는 개념을 컴퓨터 데이터로 옮긴것을 말한다. 이에, 같은 강아지가 둘이 있더라도 "강아지" 라는 큰 틀만 같고, 실제 몸무게, 이름등이 다르듯이. (클래스는 강아지, 객체는 실제 강아지를 생각하면 되겠다.)

python, javascript에서는 거의 모든것을 객체로 본다. 따라서 객체의 정의는, 숫자형데이터, 실수형데이터, 문자열데이터, 데이터의집합 등이 될 수 있다. 즉, 숫자(int) 클래스는 숫자형 데이터를 가진 객체(인스턴스)를 생성할 수 있다.

구체적으로 들어가자면 파이썬에서 데이터를 객체(인스턴스)로 추상화하기 위한 조건으로는 1) identity 2) value 3) type 이 필요하다.

사람클래스와 실제 사람객체를 예를 들어 비교하면, 나는 사람 클래스이지만 (type), 사람이 다른사람과 구분되는 이름, 얼굴, 지문, 주민번호등을 가지고 (id), 실제 내가 가지는 속성인 키, 몸무게, 취미, 나이, 스펙 (value) 등이다. 즉, 나는 사람 클래스의 인스턴스인 셈이다.

class

class A(object):
    value = 10

a = A()
>>> type(A)
<class 'type'>
>>> type(a)
<class '__main__.A'>

위의 구문에서 a 객체(인스턴스는)는 명확히 A클래스의 인스턴스 임이 드러난다.

그런데, 파이썬에서 사용자가 정의한 class는 type값으로 type을 가진다. (헷갈린다. 다시표현해보자.)

custom class (사용자가 정의한)는 type이라는 클래스의 인스턴스임을 뜻한다.

type("A", (object,), {value: 1})

a = A()
# 결과동일

type은 argument 개수가 1개이면 해당 객체의 type을 준다. argument 개수가 3개이면 type의 인스턴스인 클래스(class 객체, 혹은 type의 인스턴스=type객체)를 반환한다.

다른 오래된 언어에서 class라는 예약어는 단순히 인스턴스를 생성하는 코드 조각(일련의 함수개념)으로 취급되었지만, 파이썬은 새로운 객체를 만들어 취급한다.

이것이 의미하는 바는 type 이라는 최상위의 장치(bulit-in class) 통해 사용자 정의 클래스를 만들 수 있다는 것이다.

metaclass

metaclass의 필요성은 어디에서 찾을 수 있을까. 만약 특정한 클래스 (type인스턴스)를 만드는데 있어서, 이 클래스의 멤버함수 이름을 바꾼다거나, dict에 저장하는 방식을 달리한다 거나 하는데 쓸 수 있다.

하지만 쉽게 접하기 힘든 개념이기 때문에, 이러한 말보다 실제 작동되는 원리와 구조를 통해 이해하는게 빠르다.

metaclass를 사용할때 주의 깊게 봐야할 함수 몇가지를 먼저 살펴보자. 1) __new__ 2) __prepare__ 3) __call__

class MetaA(type):
    def __new__(cls, clsname, base, attrs):
        # your code
        return super()__new__(cls, clsname, base, attrs)

class B(metaclass=MetaA):
    pass

__new__는 실제 super()를 최종으로 호출하여 클래스 객체(type인스턴스)를 반환해야 한다. 이 사이에 attr안의 변화, 혹은 특정한 조상 (base)에 대한 제약이라던가, type의 이름등에 변화를 줄 수 있다.

from collections import OrderedDict

class MetaA(type):
    # ...
    def __prepare__(mcs, name, bases, **kwargs):
        return OrderedDict()

__prepare____new__ 보다 먼저 실행된다.

attrs가 담길 dict의 형태를 미리 정의하여 리턴하여야 한다. 이 dict를 '클래스의 네임스페이스' 라고 한다.

python은 exec() 라는 함수를 불러, 리턴된 dict 형태에 attrs를 넣어 업데이트 한다.

class MetaA(type):
    # ...
    def __call__(cls, *args, **kwargs):
        x = super().__call__(*args, **kwargs)
        print(f"A new object created: {x}")
        return x

__call__는 클래스가 인스턴스를 만들때 관여한다. 위의 예제는 인스턴스를 만들 때, 콘솔에 새로만들었음을 표기하게 하였다.

  • __prepare__: 네임스페이스 준비
  • __new__: (type 인스턴스) class 객체 반환
  • __init__:
  • __call__: 인스턴스를 생성한다.

실제에의 간략한 사용법

  1. __new__를 통한 클래스의 상속, 혹은 필드에 대한 통제
  2. 클래스 attributes 에 대한 검증
# 상속을 하나만 하도록 강제하기 (다중상속방지)
class DisallowMultipleInheritance(type):
    # mcs, mcsname, base(튜플형), namespace
    def __new__(mcs, *args, **kwargs):
        # 튜플의 길이를 검사
        if len(args[1]) > 1:
            raise Exception("...")

        new_class = super().__new__(mcs, *args, **kwargs)
        return new_class

class Foo(metaclass=DisallowMultipleInheritance):
    pass

class Bar:
    pass

class Zee(Foo, Bar): # -> this makes exceptions
    pass
# Django와 유사한 방식. (ModelForm)
# 컴파일 타임에 Meta라는 속성이 namespace 안에 정의되어 있는지 확인한다.
class CheckMeta(type):
    def __new__(mcs, *args, **kwargs):
        name, bases, namespace = args

    if (not namespace.get("Meta", None)) and (bases != ()):
        raise Exception("Can not configure class. Meta is missing")

    r = super().__new__(mcs, *args, **kwargs)

    return r
# 싱글톤을 metaclass를 통해 정의하기
class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(...)

        return cls._instances[cls]

class C(metaclass=Singleton)
    pass
>>> a = C()
>>> b = C()
>>> print(a is b)
True

실제예(part2: 디스크립터 -> )

디스크립터 (3.6 부터 자체 지원, PEP-484)

해당부분은 잘 이해하지 못하기도 했고, 3.6부터 자체지원한다고 하여, 3.9.5 버전을 쓰는 사람으로 생략

profile
설명을 쉽게 잘하는 개발자를 꿈꾸는 웹 개발 주니어

0개의 댓글