파이썬 메타프로그래밍 탐구: 모든 것을 제어하고 싶은 모든 것
Grace Collins
Solutions Engineer · Leapcell

파이썬 메타프로그래밍 탐구
많은 사람들이 "메타프로그래밍"이라는 개념에 익숙하지 않으며, 이에 대한 매우 정확한 정의는 없습니다. 이 글은 파이썬 내에서의 메타프로그래밍에 중점을 둡니다. 하지만 실제로 여기서 논의되는 내용은 "메타프로그래밍"의 엄격한 정의에 완전히 부합하지 않을 수 있습니다. 단지 이 글의 주제를 나타내기에 더 적합한 용어를 찾지 못해서 이 용어를 빌려왔습니다.
부제는 "제어하고 싶은 모든 것을 제어하기"입니다. 기본적으로 이 글은 한 가지 - 파이썬이 제공하는 기능을 활용하여 코드를 최대한 우아하고 간결하게 만드는 데 초점을 맞춥니다. 구체적으로 프로그래밍 기술을 통해 더 높은 수준의 추상화에서 추상화의 특징을 수정합니다.
무엇보다도 파이썬의 모든 것은 객체라는 것은 잘 알려진 상투적인 표현입니다. 또한 파이썬은 특수 메소드와 메타클래스와 같은 수많은 "메타프로그래밍" 메커니즘을 제공합니다. 객체에 속성과 메서드를 동적으로 추가하는 것과 같은 작업은 파이썬에서는 "메타프로그래밍"으로 간주되지 않습니다. 그러나 일부 정적 언어에서는 이를 달성하기 위해 특정 기술이 필요합니다. 파이썬 프로그래머를 쉽게 당황하게 할 수 있는 몇 가지 측면에 대해 논의해 보겠습니다.
객체를 다양한 수준으로 분류하는 것으로 시작하겠습니다. 일반적으로 객체에는 유형이 있으며 파이썬은 오랫동안 유형을 객체로 구현해 왔습니다. 따라서 인스턴스 객체와 클래스 객체가 있으며, 이는 두 가지 수준입니다. 기본적인 이해가 있는 독자는 메타클래스의 존재를 알고 있을 것입니다. 간단히 말해서 메타클래스는 "클래스"의 "클래스"이며, 이는 클래스보다 높은 수준에 있음을 의미합니다. 이는 또 다른 수준을 추가합니다. 더 많은 것이 있을까요?
ImportTime vs RunTime
만약 우리가 다른 관점에서 보고 이전의 세 가지 수준과 동일한 기준을 적용할 필요가 없다면, ImportTime과 RunTime이라는 두 가지 개념을 구별할 수 있습니다. 그 경계는 뚜렷하지 않습니다. 이름에서 알 수 있듯이 이는 임포트 시점과 실행 시점이라는 두 순간을 나타냅니다.
모듈을 임포트할 때 어떤 일이 발생할까요? 전역 범위(정의문이 아닌 구문)의 구문이 실행됩니다. 함수 정의는 어떻게 될까요? 함수 객체가 생성되지만 그 안의 코드는 실행되지 않습니다. 클래스 정의의 경우 클래스 객체가 생성되고 클래스 정의 범위의 코드가 실행되며 클래스 메서드의 코드는 자연스럽게 실행되지 않습니다.
실행 중에는 어떻게 될까요? 함수와 메서드의 코드가 실행됩니다. 물론 먼저 호출해야 합니다.
메타클래스
따라서 메타클래스와 클래스는 ImportTime에 속한다고 말할 수 있습니다. 모듈을 임포트한 후 생성됩니다. 인스턴스 객체는 RunTime에 속합니다. 모듈을 임포트하기만 해서는 인스턴스 객체가 생성되지 않습니다. 그러나 모듈 범위 내에서 클래스를 인스턴스화하면 인스턴스 객체도 생성되기 때문에 너무 독단적일 수는 없습니다. 단지 우리는 보통 인스턴스화를 함수 내부에 작성하므로 이러한 분류가 이루어집니다.
생성된 인스턴스 객체의 특징을 제어하려면 어떻게 해야 할까요? 아주 간단합니다. 클래스 정의에서 __init__
메서드를 오버라이드합니다. 그렇다면 클래스의 일부 속성을 제어하고 싶다면 어떻게 해야 할까요? 그러한 필요가 있을까요? 당연히 있습니다!
고전적인 싱글톤 패턴과 관련하여 모든 사람은 이를 구현하는 여러 가지 방법이 있다는 것을 알고 있습니다. 요구 사항은 클래스가 하나의 인스턴스만 가질 수 있다는 것입니다.
가장 간단한 구현은 다음과 같습니다.
class _Spam: def __init__(self): print("Spam!!!") _spam_singleton = None def Spam(): global _spam_singleton if _spam_singleton is not None: return _spam_singleton else: _spam_singleton = _Spam() return _spam_singleton
이 팩토리와 유사한 패턴은 그다지 우아하지 않습니다. 요구 사항을 다시 한번 검토해 보겠습니다. 우리는 클래스가 하나의 인스턴스만 갖기를 바랍니다. 클래스에서 정의하는 메서드는 인스턴스 객체의 동작입니다. 따라서 클래스의 동작을 변경하려면 더 높은 수준의 무언가가 필요합니다. 여기에 메타클래스가 등장합니다. 앞에서 언급했듯이 메타클래스는 클래스의 클래스입니다. 즉, 메타클래스의 __init__
메서드는 클래스의 초기화 메서드입니다. __call__
메서드도 있는데, 이를 통해 인스턴스를 함수처럼 호출할 수 있습니다. 그러면 메타클래스의 이 메서드는 클래스가 인스턴스화될 때 호출되는 메서드입니다.
코드는 다음과 같이 작성할 수 있습니다.
class Singleton(type): def __init__(self, *args, **kwargs): self._instance = None super().__init__(*args, **kwargs) def __call__(self, *args, **kwargs): if self._instance is None: self._instance = super().__call__(*args, **kwargs) return self._instance else: return self._instance class Spam(metaclass = Singleton): def __init__(self): print("Spam!!!")
일반적인 클래스 정의와 비교하여 두 가지 주요 차이점이 있습니다. 하나는 Singleton
의 기본 클래스가 type
이라는 것이고, 다른 하나는 Spam
의 정의에 metaclass = Singleton
이 있다는 것입니다. type
은 무엇일까요? 이는 object
의 서브클래스이며, object
는 그 인스턴스입니다. 즉, type
은 모든 클래스의 클래스이며 가장 기본적인 메타클래스입니다. 이는 모든 클래스가 생성될 때 필요한 몇 가지 작업을 규정합니다. 따라서 사용자 정의 메타클래스는 type
을 서브클래스해야 합니다. 동시에 type
은 객체이기도 하므로 object
의 서브클래스입니다. 이해하기가 약간 어렵지만 일반적인 아이디어를 얻으십시오.
데코레이터
데코레이터에 대해 이야기해 보겠습니다. 대부분의 사람들은 데코레이터를 파이썬에서 이해하기 가장 어려운 개념 중 하나로 생각합니다. 사실 이건 그냥 구문 설탕일 뿐입니다. 함수도 객체라는 것을 이해하면 나만의 데코레이터를 쉽게 작성할 수 있습니다.
from functools import wraps def print_result(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) print(result) return result return wrapper @print_result def add(x, y): return x + y # 다음과 동일합니다. # add = print_result(add) add(1, 3)
여기서는 데코레이터 @wraps
도 사용합니다. 이는 반환된 내부 함수 wrapper
가 원본 함수와 동일한 함수 서명을 갖도록 하는 데 사용됩니다. 기본적으로 데코레이터를 작성할 때 추가해야 합니다.
주석에 썼듯이 @decorator
형식은 func = decorator(func)
과 동일합니다. 이 점을 이해하면 더 많은 유형의 데코레이터를 작성할 수 있습니다. 예를 들어 클래스 데코레이터 및 클래스로 데코레이터를 작성하는 것입니다.
def attr_upper(cls): for attrname, value in cls.__dict__.items(): if isinstance(value, str): if not value.startswith('__'): setattr(cls, attrname, bytes.decode(str.encode(value).upper())) return cls @attr_upper class Person: sex ='man' print(Person.sex) # MAN
일반 데코레이터와 클래스 데코레이터 구현의 차이점에 주의하십시오.
데이터 추상화 - 디스크립터
클래스가 특정 공통 특성을 갖거나 클래스 정의 내에서 이를 제어할 수 있도록 하려면 메타클래스를 사용자 정의하고 이를 해당 클래스의 메타클래스로 만들 수 있습니다. 일부 함수가 특정 공통 기능을 갖고 코드 중복을 피하도록 하려면 데코레이터를 정의할 수 있습니다. 그렇다면 인스턴스의 속성이 일부 공통 특성을 갖도록 하려면 어떻게 해야 할까요? 일부 사람들은 property
를 사용할 수 있다고 말할 수 있으며 실제로 사용할 수 있습니다. 그러나 이 논리는 각 클래스 정의에 작성해야 합니다. 이러한 클래스의 인스턴스 속성이 동일한 특성을 갖도록 하려면 디스크립터 클래스를 사용자 정의할 수 있습니다.
디스크립터에 관해서는 이 글 https://docs.python.org/3/howto/descriptor.html 에서 매우 잘 설명하고 있습니다. 동시에 디스크립터가 함수 뒤에 숨겨져 함수와 메서드 간의 통합과 차이점을 달성하는 방법에 대해서도 자세히 설명합니다. 몇 가지 예가 있습니다.
class TypedField: def __init__(self, _type): self._type = _type def __get__(self, instance, cls): if instance is None: return self else: return getattr(instance, self.name) def __set_name__(self, cls, name): self.name = name def __set__(self, instance, value): if not isinstance(value, self._type): raise TypeError('Expected' + str(self._type)) instance.__dict__[self.name] = value class Person: age = TypedField(int) name = TypedField(str) def __init__(self, age, name): self.age = age self.name = name jack = Person(15, 'Jack') jack.age = '15' # 오류 발생
여기에는 여러 역할이 있습니다. TypedField
는 디스크립터 클래스이고 Person
의 속성은 디스크립터 클래스의 인스턴스입니다. 디스크립터는 Person
의 속성, 즉 인스턴스 속성이 아닌 클래스 속성으로 존재하는 것 같습니다. 그러나 실제로 Person
의 인스턴스가 동일한 이름을 가진 속성에 액세스하면 디스크립터가 적용됩니다. Python 3.5 이전 버전에는 __set_name__
특수 메서드가 없습니다. 즉, 클래스 정의에서 디스크립터에 지정된 이름을 알아야 하는 경우 인스턴스화할 때 명시적으로 디스크립터에 전달해야 합니다. 즉, 하나의 매개변수가 더 필요합니다. 그러나 Python 3.6에서는 이 문제가 해결되었습니다. 디스크립터 클래스 정의에서 __set_name__
메서드를 오버라이드하기만 하면 됩니다. 또한 __get__
작성에 주의하십시오. 기본적으로 instance
에 대한 판단이 필요합니다. 그렇지 않으면 오류가 발생합니다. 이유는 이해하기 어렵지 않으므로 자세히 설명하지 않겠습니다.
서브클래스 생성 제어 - 메타클래스의 대안
Python 3.6에서는 __init_subclass__
특수 메서드를 구현하여 서브클래스 생성을 사용자 정의할 수 있습니다. 이렇게 하면 어떤 경우에는 다소 번거로운 메타클래스를 사용하지 않아도 됩니다.
class PluginBase: subclasses = [] def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls.subclasses.append(cls) class Plugin1(PluginBase): pass class Plugin2(PluginBase): pass
요약
메타클래스와 같은 메타프로그래밍 기술은 대부분의 사람들에게 다소 모호하고 이해하기 어렵고 대부분의 경우 사용할 필요가 없습니다. 그러나 대부분의 프레임워크 구현에서는 사용자가 작성한 코드를 간결하고 이해하기 쉽게 만들 수 있도록 이러한 기술을 활용합니다. 이러한 기술에 대한 더 깊은 이해를 얻고 싶다면 Fluent Python 및 Python Cookbook과 같은 일부 책을 참조하거나(이 기사의 일부 내용은 해당 책에서 참조되었습니다.) 위에 언급된 디스크립터 How - To 및 데이터 모델 섹션 등과 같은 공식 문서의 일부 장을 읽어보십시오. 또는 Python으로 작성된 소스 코드와 CPython 소스 코드를 포함하여 Python 소스 코드를 직접 검토하십시오.
이러한 기술을 완전히 이해한 후에만 사용하고 모든 곳에서 사용하려고 시도하지 마십시오.
Leapcell: 웹 호스팅을 위한 최고의 서버리스 플랫폼
마지막으로 Python 서비스를 배포하는 데 매우 적합한 플랫폼 Leapcell을 추천하고 싶습니다.
1. 다중 언어 지원
- JavaScript, Python, Go 또는 Rust로 개발합니다.
2. 무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하세요. 요청이 없으면 요금이 부과되지 않습니다.
3. 최고의 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하세요.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
4. 능률적인 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
5. 간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장.
- 운영 오버헤드가 전혀 없으며 구축에만 집중할 수 있습니다.
Leapcell 트위터: https://x.com/LeapcellHQ