본문 바로가기
GD's IT Lectures : 기초부터 시리즈/파이썬(Python) 기초부터 ~

[파이썬(PYTHON) : 고급] 메타프로그래밍

by GDNGY 2023. 5. 12.

1. 메타프로그래밍

메타프로그래밍은 '프로그램이 자기 자신을 데이터로 처리하도록 하는 기법'을 말합니다. 파이썬에서는 메타클래스, 동적 속성 및 메서드 생성, 디스크립터 등을 통해 메타프로그래밍을 수행할 수 있습니다.

 

1.1. 메타클래스

1.1.1. 메타클래스 개념

1.1.1.1. 클래스 vs 메타클래스

클래스는 객체를 생성하는 '틀'로, 객체의 속성과 메소드를 정의합니다. 반면, 메타클래스는 클래스의 '틀'을 만드는 클래스입니다. 즉, 메타클래스는 클래스를 생성하고, 클래스의 동작을 제어하는 역할을 합니다.

# 클래스의 예
class MyClass:
    pass

# 메타클래스의 예
class MyMeta(type):
    pass

 

1.1.1.2. 메타클래스의 역할

메타클래스는 클래스의 동작을 제어하고, 클래스를 생성할 때 일관된 방식으로 속성을 추가하거나, 클래스 이름을 확인하거나 변경하는 등의 기능을 수행합니다.

# 메타클래스를 이용해 모든 클래스 이름을 대문자로 만드는 예
class UppercaseMeta(type):
    def __new__(mcs, name, bases, attrs):
        uppercase_attrs = {k.upper(): v for k, v in attrs.items() if not k.startswith('__')}
        return super().__new__(mcs, name, bases, uppercase_attrs)

class MyClass(metaclass=UppercaseMeta):
    my_attr = 'value'

print(hasattr(MyClass, 'my_attr'))  # False
print(hasattr(MyClass, 'MY_ATTR'))  # True


1.1.2. 메타클래스 생성 및 활용

1.1.2.1. 메타클래스 정의 방법

메타클래스를 정의하기 위해서는 type을 상속받아 __new__ 또는 __init__ 메서드를 오버라이드합니다.

class MyMeta(type):
    def __new__(mcs, name, bases, attrs):
        print('Creating a new class named', name)
        return super().__new__(mcs, name, bases, attrs)

 

1.1.2.2. 메타클래스를 활용한 코딩 기법

메타클래스는 클래스 생성 시점에 속성을 추가하거나 변경하는 등의 작업을 할 수 있습니다. 이를 통해 코드 중복을 줄이고, 일관성을 유지하는 등의 이점을 얻을 수 있습니다.

class AutoStringMeta(type):
    def __new__(mcs, name, bases, attrs):
        if 'to_string' not in attrs
            attrs['to_string'] = lambda self: ', '.join(f'{k}={v}' for k, v in self.__dict__.items())
        return super().__new__(mcs, name, bases, attrs)

class MyClass(metaclass=AutoStringMeta):
    def __init__(self, a, b):
        self.a = a
        self.b = b

obj = MyClass(10, 20)
print(obj.to_string())  # 'a=10, b=20'

 

반응형

 

1.2. 동적 속성 및 메소드 생성

1.2.1. 동적 속성 생성

1.2.1.1. __getattr__, __setattr__ 활용

__getattr__ 메소드는메서드는 객체의 존재하지 않는 속성에 접근하려고 할 때 호출됩니다. __setattr__ 메서드는 속성을 설정하려고 할 때 호출됩니다. 이 두 메서드를 사용하여 동적으로 속성을 생성하고 설정할 수 있습니다.

class DynamicAttr:
    def __getattr__(self, name):
        return f'You are trying to get a non-existent attribute: {name}'

    def __setattr__(self, name, value):
        self.__dict__[name] = f'Value of {name} is {value}'

obj = DynamicAttr()
print(obj.foo)  # 'You are trying to get a non-existent attribute: foo'
obj.bar = 'baz'
print(obj.bar)  # 'Value of bar is baz'

 

1.2.1.2. 동적 속성 활용 예제

동적 속성은 존재하지 않는 속성에 대한 기본값을 제공하거나, 속성 이름에 따라 다른 동작을 수행하도록 하는 등 다양하게 활용할 수 있습니다.

class DefaultAttr:
    def __getattr__(self, name):
        return 'default value'

obj = DefaultAttr()
print(obj.foo)  # 'default value'

 

 

1.2.2. 동적 메소드 생성

1.2.2.1. types.MethodType 활용

types.MethodType 함수를 사용하여 동적으로 메서드를 생성할 수 있습니다.

import types

class MyClass:
    pass

def print_hello(self):
    print('Hello, world!')

MyClass.say_hello = types.MethodType(print_hello, MyClass)
obj = MyClass()
obj.say_hello()  # 'Hello, world!'

 

1.2.2.2. 동적 메소드 활용 예제

동적 메소드는 필요에 따라 메서드를 추가하거나 변경하는 등의 작업을 수행할 수 있습니다. 예를 들어, 런타임에 특정 기능을 추가하거나 변경하는 등의 작업을 수행할 수 있습니다.

import types

class MyClass:
    pass

def greet(self, name):
    print(f'Hello, {name}!')

MyClass.greet = types.MethodType(greet, MyClass)
obj = MyClass()
obj.greet('Python')  # 'Hello, Python!'

 

1.3. 디스크립터

1.3.1. 디스크립터 개념

1.3.1.1. 디스크립터 소개

디스크립터는 파이썬의 속성에 대한 접근을 사용자 정의하는 메커니즘이며, __get__, __set__, __delete__ 등의 메서드를 오버라이드하여 작성할 수 있습니다.

class Descriptor:
    def __get__(self, instance, owner):
        print('Getting the attribute')

    def __set__(self, instance, value):
        print('Setting the attribute')

    def __delete__(self, instance):
        print('Deleting the attribute')

class MyClass:
    attr = Descriptor()

 

1.3.1.2. 디스크립터의 필요성

디스크립터는 속성 접근의 논리를 재정의하고, 이를 통해 코드 재사용, 데이터 유효성 검사, 신호 전송 등의 작업을 수행할 수 있습니다.

class PositiveInteger:
    def __init__(self, value=0):
        self.value = value

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError('Value cannot be negative')
        self.value = value

class MyClass:
    number = PositiveInteger()

obj = MyClass()
obj.number = 10  # OK
obj.number = -10  # Raises ValueError

 

1.3.2. 디스크립터 활용

1.3.2.1. 디스크립터 정의 및 사용 방법

디스크립터는 클래스에 정의되고, 이를 통해 해당 클래스의 인스턴스가 속성에 접근할 때의 동작을 제어할 수 있습니다.

class Descriptor:
    def __get__(self, instance, owner):
        return instance.__dict__.get('attr', 'default value')

    def __set__(self, instance, value):
        instance.__dict__['attr'] = value

class MyClass:
    attr = Descriptor()

obj = MyClass()
print(obj.attr)  # 'default value'
obj.attr = 'new value'
print(obj.attr)  # 'new value'

 

1.3.2.2. 디스크립터를 활용한 실용적인 예제

디스크립터는 유효성 검사, 속성 접근 로깅, 신호 전송 등 다양한 작업을 수행할 수 있습니다.

class LoggingDescriptor:
    def __get__(self, instance, owner):
        value = instance.__dict__.get('attr', 'default value')
        print(f'Accessed the attribute with value: {value}')
        return value

    def __set__(self, instance, value):
        print(f'Set the attribute to: {value}')
        instance.__dict__['attr'] = value

class MyClass:
    attr = LoggingDescriptor()

obj = MyClass()
print(obj.attr)  # 'Accessed the attribute with value: default value', 'default value'
obj.attr = 'new value'  # 'Set the attribute to: new value'
print(obj.attr)  # 'Accessed the attribute with value: new value', 'new value'

 

이와 같이 디스크립터를 활용하면, 속성에 접근하거나 설정할 때 추가적인 로직을 수행하도록 할 수 있습니다. 이는 유효성 검사, 타입 체크, 변환, 로깅 등 다양한 분야에서 활용될 수 있습니다.

반응형

댓글