메타클래스에 대해 알아보기 이전에 파이썬의 데이터 모델에 대한 이해가 필요하다.

파이썬에서 모든 것은 데이터를 추상화 한 객체로 이루어져 있다.
또한, 파이썬의 객체는 아이덴티티, 값, 타입을 가지고 있다.

아이덴티티 (id)

id() 함수를 통해 얻을 수 있으며 객체의 수명동안 유일하고 불변함이 보장되는 정수다.

값 (value)

객체의 타입에 따라 불변할 수 있고 가변할 수도 있다. ex)tuple : 불변, list : 가변

타입 (type)

객체가 지원하는 연산들과 그 타입의 객체가 가질 수 있는 값(ex) int : 1, list : [1,2])들을 통해 객체의 특성을 정의한다. 객체의 타입은 type()을 통해 얻을 수 있으며, 불변하다.

여기서 말한 타입과 같이 파이썬의 모든 객체들은 어떠한 타입에 의해 정의된다.

파이썬의 type() 빌트인 함수를 사용하면 객체의 타입을 알 수 있다.

class Test:
    pass
t = Test()
type(t)
# <class '__main__.Test'>

def hello():
    pass
type(hello)
# <class 'function'>

type(1)
# <class 'int'>

위 예제를 보면 Test 클래스의 인스턴스인 tTest가 타입이고, hello 함수는 function이 타입이며, 정수 1은 int가 타입이다.

그렇다면, Test 클래스의 타입은 존재할까?

class Test:
    pass
type(Test)
# <class 'type'>

놀랍게도, Test 클래스 객체의 타입이 존재한다.
여기서 출력된 typeTest 클래스의 메타 클래스라고 하며, 인스턴스로 클래스를 가진다.

그러면 메타클래스는 무슨 용도로 사용하는 걸까?

그전에 몇가지 메타클래스의 매직 메소드에 대해 알아보자

class TestMeta(type):
    def __prepare__(mcs, *args, **kwarg): # 메타 클래스가 결정되었을 때 (mro가 구성된 후) 클래스 정의를 위해 호출된다.
        # mcs = metaclass
        print("__prepare__()")
        return super.__prepare__(mcs, *args, **kwarg)

    def __new__(mcs, *args, **kwargs):  # 클래스를 생성 할 때 호출됨
        # mcs = metaclass
        print("__new__()")
        return super().__new__(mcs, *args, **kwargs)

    def __init__(cls, *args, **kwargs):  # 클래스가 생성 된 후 호출됨
        print("__init__()")
        super().__init__(*args, **kwargs)

    def __call__(cls, *args, **kwargs):  # 클래스의 인스턴스를 생성할 때 호출됨
        print("__call__()")
        return super().__call__(*args, **kwargs)


class Test(metaclass=TestMeta):
    pass

# __prepare__()
# __new__()
# __init__()

t = Test()
# __call__()

위 코드를 보면 __prepare__, __new__, __init__, __call__ 메소드를 작성하고 사용하는 것을 볼 수 있다.

  • __prepare__ 메소드는 메타 클래스가 결정되었을 때 호출되며, 클래스의 네임 스페이스를 준비한다.
  • __new__ 메소드는 클래스 객체를 생성 할 때 호출된다.
  • __init__ 메소드는 클래스가 생성 된 후 호출되어 클래스를 초기화 한다.
  • __call__ 메소드는 클래스의 인스턴스를 생성 할 때 호출 된다.

이 매직 메서드를 가지고 무슨 일을 할 수 있을까?

싱글톤 패턴 구현

싱글톤 패턴은 클래스의 인스턴스화를 항상 하나의 개체로만 제한하는 설계 패턴이다.
구현해 보자면

class SingletonMeta(type):
    _instances = {}

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


class SingletonClass(metaclass=SingletonMeta):
    pass

sl1 = SingletonClass()
sl2 = SingletonClass()

print(id(sl1))
# 1877212284048
print(id(sl2))
# 1877212284048

인스턴스 생성에 관여하는 __call__()메소드를 오버라이딩 해서 클래스를 key로 두고 인스턴스를 value로 만들어 클래스당 하나의 인스턴스를 가지도록 했다.

애트리뷰트 검증

DRF의 ModelSerializer 에는 내부 Meta 클래스가 없다면 오류를 일으킨다. 이에 대한 오류 검증을 DRF에서는 get_fields() 메소드에 구현해 두었는데, 이를 메타 클래스로 검증 할 수 있을 것 같다.

def get_fields(self):
    ...

    assert hasattr(self, 'Meta'), (
        'Class {serializer_class} missing "Meta" attribute'.format(
            serializer_class=self.__class__.__name__
        )
    )
    ...

위 코드는 클래스 내부에 Meta 애트리뷰트가 있는지 확인하는 코드이다. 이를 메타클래스로 검증하는 코드를 짜보자

class ModelSerializerMetaclass(SerializerMetaclass):
    def __new__(mcs, *args, **kwargs):
        name, bases, namespace = args
        if name not in ("ModelSerializer","HyperlinkedModelSerializer"):
            mcs._check_meta(name,namespace)

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

    def _check_meta(name,namespace):
        if not namespace.get("Meta", None):
            raise Exception(f'Class {name} missing "Meta" attribute')
        return


class ModelSerializer(Serializer, metaclass=ModelSerializerMetaclass):
    pass
import peewee

class MySQLModel(peewee.Model):
    @property
    @classmethod
    def unique_fields(cls) -> list:
        for field_name in cls._meta.fields.keys():
            field = getattr(cls, field_name)
            if field.unique == True or field.primary_key == True:
                yield field

peewee 모델의 unique한 field의 이름을 iterable한 객체로 반환하는 property 함수를 만들었다.

key in [field.name for field in MySQLModel.unique_fields]

그런데 위와 같이 사용 할 때 TypeError: 'property' object is not iterable 에러가 발생했다.

해결방법을 찾아보니 metaclass에서 property 함수를 만드는 방법, class decorator를 직접 만들어서 처리하는 방법 등이 있었다.

class decorator를 직접 만들어서 처리하는 방법은 아래와 같은데, 이게 마음에 들었다.


class classproperty(object):
    def __init__(self, function):
        self.function = function

    def __get__(self, owner_self, owner_cls):  # classproperty 객체에 접근할 때 inner_func 결과값을 반환하도록
        return self.function(owner_cls)


class MySQLModel(peewee.Model):
    @classproperty
    def unique_fields(cls) -> list:
        for field_name in cls._meta.fields.keys():
            field = getattr(cls, field_name)
            if field.unique == True or field.primary_key == True:
                yield field

하위 디렉토리의 모든 테스트를 실행하고 싶을 때가 있다.

 

그럴때는

$ python -m unittest discover

를 입력하면 하위 디렉토리모든 테스트를 실행한다. (기본적으로 "test*.py" 포맷의 파일을 찾아 실행한다.)

 

$ python -m unittest discover 
F.
======================================================================
FAIL: test_failure (test2.HelloTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\workspace\study\TIL\test2.py", line 9, in test_failure
    self.assertEqual(0,1)
AssertionError: 0 != 1

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

문제 풀이 코드

def solution(board, moves):
    answer = 0
    result_stack = []

    for move in moves:
        for i, column in enumerate(board):
            if column[move - 1] != 0:
                if len(result_stack) > 0:
                    if (recent := result_stack.pop()) == column[move - 1]:  #1
                        answer += 2
                        board[i][move - 1] = 0
                        break
                    else:
                        result_stack.append(recent)
                result_stack.append(column[move - 1])
                board[i][move - 1] = 0
                break

    return answer

#1

왈러스 연산자를 사용해서 pop 한 값(recent)을 if else 네임스페이스에서 사용할 수 있도록 했다.

문제 풀이 코드

def solution(a, b):
    return sum([x*y for x,y in zip(a, b)])

졸리고 머리아파서 그냥 1단계 풀었다.

아이디어

DFS 문제이다.
예를 들어numbers : [ 1, 1, 1 ],target : 1이 들어왔다면 아래와 같은 이진 트리 구조를 갖는다.

노드의 종점에서 target 값과 비교해 같다면 answer에 +1을 해주는 것이다.

문제 풀이 코드

def solution(numbers, target):
    answer = 0
    def dfs(current_total, current_index, sign): # dfs(0,0,1)
        nonlocal answer

        current_total += numbers[current_index] * sign 

        if len(numbers) <= current_index + 1: # 1
            if current_total == target:
                answer += 1
        else: # 2
            dfs(current_total, current_index + 1, 1)
            dfs(current_total, current_index + 1, -1)
        return

    dfs(0,0,1)
    dfs(0,0,-1)
    return answer

#1

다음 인덱스가 존재하지 않을 때 (노드의 종점일때 )

#2

다음 인덱스가 존재 할 때 (노드의 종점이 아닐 때)

네임스페이스란?

네임스페이스란 프로그래밍 언어에서 특정한 객체를 이름에 따라 구분할 수 있는 공간을 의미한다.

my_string = "asdf"



my_integer = 12



my_list = [1,2,3]

my_list2 = my_list

위 예시에서는 my_string"asdf"객체를 가리키고 있다.

위와 같이 이름과 객체를 연결한 것을네임스페이스 라고 한다.

왜 필요한데?

프로그래밍을 하다보면 모든 변수와 함수명을 겹치지 않도록 하는 것은 불가능 하다.

그렇기 때문에 특정한 이름의 변수 혹은 함수가 통용될 수 있는 범위를 제한하기 위해 네임스페이스가 등장한 것이다.

아래의 코드를 보자

class TestA:

    a = 1



class TestB:

    a = 2

a라는 변수이름이 중복되어 사용되고 있다. 만약 네임스페이스라는 개념이 없다면 원하는 a를 호출하기 힘들 것이다.

TestA 라는 로컬 네임스페이스에서 a를 호출하면 1의 값을 가진 변수 a를 얻을 수 있고,

TestB 라는 로컬 네임스페이스에서 a를 호출하면 2의 값을 가진 변수 a를 얻을 수 있게 된다.

Local, Global, Built-in Namespace

앞서 말했듯이 네임스페이스는 변수 혹은 함수가 통용될 수 있는 범위를 제한하기 위해 등장했다.

이런 네임스페이스는 Local, Global, Built-in 3가지로 분류할 수 있다.

  • Build-in
    기존 내장 함수 들의 이름이 소속된다. 파이썬으로 작성 된 모든 범위가 포함된다.

  • Global
    모듈별로 존재하며, 모듈 전체에서 통용될 수 있는 이름들이 소속된다.

  • Local
    함수 및 메서드 별로 존재하며, 함수 내의 지역 변수들의 이름들이 소속된다.

문제 풀이 코드

import heapq

def solution(scoville, K):
    answer = 0
    heapq.heapify(scoville)  #1 
    while True:
        answer += 1
        heapq.heappush(scoville,heapq.heappop(scoville) + heapq.heappop(scoville) * 2)  #2
        if scoville[0] >= K:  #3
            return answer
        elif len(scoville) <= 1:
            return -1

#1

기존 scoville배열을 heap 자료구조로 변경시킴 (O(N))

#2

맨 앞 + (앞에서 두번째 X 2) 를 heap queue에 다시 집어넣음

#3

만약 가장 작은 인자가 K보다 크다면 섞은 횟수 반환

+ Recent posts