meta class
동적 attribute 접근
하지만 이런 2가지 강력함에는 많은 함정이 뒤따른다.(이해하기 어려움 + 부작용)
최소 놀람의 법칙을 따르고, 잘 정해진 관용어로만 이런 기능을 사용하는 것이 중요하다.
metaclass가 어떻게 작동하는가?
__new__
메서드를 통해, 자신과 연관된 클래스의 내용을 받는다.class Meta(type):
def __new__(meta, name, bases, class_dict): # bases는 부모 클래스들
print(f'* 실행: {name}의 메타 {meta}.__new__')
print('기반클래스들:', bases)
print(class_dict)
return type.__new__(meta, name, bases, class_dict)
class MyClass(metaclass=Meta):
stuff = 123
def foo(self):
pass
class MySubClass(MyClass):
other = 567
def bar(self):
pass
>>>
* 실행: Myclass의 메타 <class '__main__.Meta'>.__new__
* 기반 클래스들: ()
{'__module__': '__main__', '__qualname__': 'MyClass',
'stuff': 123, 'foo': <function MyClass.foo at ___>}
* 실행: MySubClass의 메타 <class '__main__.Meta'>.__new__
* 기반 클래스들: (<class '__main__.MyClass'>, )
{'__module__': '__main__', '__qualname__': 'MySubClass',
'other': 567, 'bar': <function MySubClass.bar at ___>}
__init__subclass__
를 사용해 하위 클래스를 검증하라.__new__
메서드"는 class 문의 "모든 본문이 처리(실행)된 직후"에 호출된다.__init_subcalss__
를 사용해 "하위 클래스가 정의된 직후, 하위 클래스 타입이 만들어지기 직전"에 해당 클래스가 원하는 요건을 잘 갖췄는지 확인하라.__init_subcalss__
정의 안에서, super().__init_subcalss__
를 호출해, 여러 계층에 걸쳐 클래스를 검증하고 다중 상속을 제대로 처리하도록 해라.class ValidatePolygon(type):
# 메타 클래스 (검증용)
def __new__(meta, name, bases, class_dict): # bases는 부모 클래스들
# Polygon 클래스의 하위 클래스만 검증한다
if bases:
if class_dict['sides'] < 3:
raise ValueError('다각형 변은 3개 이상이어야 함')
return type.__new__(meta, name, bases, class_dict)
class Polygon(metaclass=ValidatePolygon):
# 기반 클래스( 검증 수행 x)
sides = None # 하위 클래스는 이 애트리뷰트에 값을 지정해야 한다
@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180
class Triangle(Polygon):
sides = 3
class Rectangle(Polygon):
sides = 4
class Nonagon(Polygon):
sides = 9
assert Triangle.interior_angles() == 180
assert Rectangle.interior_angles() == 360
assert Nonagon.interior_angles() == 1260
print('class 이전')
class Line(Polygon):
print('sides 이전')
sides = 2
print('sides 이후')
print('class 이후')
>>>
class 이전
sides 이전
sides 이후
Traceback ...
ValueError: 다각형 변은 3개 이상이어야 함.
__init_subclass__
special class method를 정의하는 방식을 활용하라.class BetterPolygon:
sides = None # 하위클래스에서 이 애트리뷰트의 값을 지정해야 함
def __init_subclass__(cls):
super().__init_subclass__()
if cls.sides < 3:
raise ValueError('다각형 변은 3개 이상이어야 함')
@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180
class Hexagon(BetterPolygon):
sides = 6
assert Hexagon.interior_angles() == 720
__init_subclass__
special class method를 사용하면 이 문제도 해결할 수 있다.super
내장 함수를 사용해 부모나 형제자매 클래스의 __init_subclass__
를 호출해주는 한, 여러 단계로 이뤄진 __init__subclass__
를 활용하는 클래스 계층 구조를 쉽게 정의할 수 있다.class Filled:
color = None # 하위 클래스에서 이 애트리뷰트 값을 지정해야 한다
def __init_subclass__(cls):
super().__init_subclass__()
if cls.color not in ('red', 'green', 'blue'):
raise ValueError('지원하지 않는 color 값')
class RedTriangle(Filled, BetterPolygon):
color = 'red'
sides = 3
ruddy = RedTriangle()
assert isinstance(ruddy, Filled)
assert isinstance(ruddy, Polygon)
__init__subclass__
를 사용해 클래스 확장을 등록하라.__init_subclass__
가 더 낫다. __init_subclass__
쪽이 더 깔끔하고, 초보자가 이해하기도 더 쉽다. registry = {}
register_class(EvenBetterPoint2D)
before = EvenBetterPoint2D(5, 3)
print('이전: ', before)
data = before.serialize()
print('직렬화한 값:', data)
after = deserialize(data)
print('이후: ', after)
class EvenBetterPoint2D(BetterSerializable):
def __init__(self, x, y):
super().__init__(x, y)
self.x = x
self.y = y
class BetterSerializable:
def __init__(self, *args):
self.args = args
def serialize(self):
return json.dumps({
'class': self.__class__.__name__,
'args': self.args,
})
def __repr__(self):
name = self.__class__.__name__
args_str = ', '.join(str(x) for x in self.args)
return f'{name}({args_str})'
def register_class(target_class):
registry[target_class.__name__] = target_class
def deserialize(data):
params = json.loads(data)
name = params['class']
target_class = registry[name]
return target_class(*params['args'])
register_class
호출을 잊어버릴 수 있다는 점이다. before = Vector3D(10, -7, 3)
print('이전: ', before)
data = before.serialize()
print('직렬화한 값:', data)
print('이후: ', deserialize(data))
class Vector3D(RegisteredSerializable):
def __init__(self, x, y, z):
super().__init__(x, y, z)
self.x, self.y, self.z = x, y, z
class RegisteredSerializable(BetterSerializable,
metaclass=Meta):
pass
class BetterSerializable:
def __init__(self, *args):
self.args = args
def serialize(self):
return json.dumps({
'class': self.__class__.__name__,
'args': self.args,
})
def __repr__(self):
name = self.__class__.__name__
args_str = ', '.join(str(x) for x in self.args)
return f'{name}({args_str})'
class Meta(type):
def __new__(meta, name, bases, class_dict):
cls = type.__new__(meta, name, bases, class_dict)
register_class(cls)
return cls
__init_subclass__
magic method 이용하는 방법 (더 추천)before = Vector1D(6)
print('이전: ', before)
data = before.serialize()
print('직렬화한 값:', data)
print('이후: ', deserialize(data))
class Vector1D(BetterRegisteredSerializable):
def __init__(self, magnitude):
super().__init__(magnitude)
self.magnitude = magnitude
class BetterRegisteredSerializable(BetterSerializable):
def __init_subclass__(cls):
super().__init_subclass__()
register_class(cls)
class BetterSerializable:
def __init__(self, *args):
self.args = args
def serialize(self):
return json.dumps({
'class': self.__class__.__name__,
'args': self.args,
})
def __repr__(self):
name = self.__class__.__name__
args_str = ', '.join(str(x) for x in self.args)
return f'{name}({args_str})'
__set_name__
으로 class attribute 를 표시하라.__set_name__
special method를 descriptor 클래스에 정의하면, descriptor가 포함된 class의 프로퍼티 이름을 처리할 수 있다.descriptor가 변경한 class의 instance dictionary
에 데이터를 저장하게 만들면cust = Customer()
print(f'이전: {cust.first_name!r} {cust.__dict__}')
cust.first_name = '유클리드'
print(f'이후: {cust.first_name!r} {cust.__dict__}')
>>>
이전: '' {}
이후: '유클리드' {'_first_name': '유클리드'}
class Customer:
# 클래스 애트리뷰트
first_name = Field('first_name')
last_name = Field('last_name')
prefix = Field('prefix')
suffix = Field('suffix')
class Field: # descriptor
def __init__(self, name):
self.name = name
self.internal_name = '_' + self.name
def __get__(self, instance, instance_type):
if instance is None:
return self
return getattr(instance, self.internal_name, '')
def __set__(self, instance, value):
setattr(instance, self.internal_name, value)
Field.name
과 Field.internal_name
을 자동으로 대입할 수 있다.cust = BetterCustomer()
print(f'이전: {cust.first_name!r} {cust.__dict__}')
cust.first_name = '오일러'
print(f'이후: {cust.first_name!r} {cust.__dict__}')
>>>
이전: '' {}
이후: '오일러' {'_first_name': '오일러'}
class BetterCustomer(DatabaseRow):
first_name = Field()
last_name = Field()
prefix = Field()
suffix = Field()
class DatabaseRow(metaclass=Meta):
pass
class Meta(type): # metaclass
def __new__(meta, name, bases, class_dict):
for key, value in class_dict.items():
if isinstance(value, Field):
value.name = key
value.internal_name = '_' + key
cls = type.__new__(meta, name, bases, class_dict)
return cls
class Field: # descriptor
def __init__(self):
# 이 두 정보를 메타클래스가 채워 준다
self.name = None
self.internal_name = None
def __get__(self, instance, instance_type):
if instance is None:
return self
return getattr(instance, self.internal_name, '')
def __set__(self, instance, value):
setattr(instance, self.internal_name, value)
__set_name__
magic method를 사용하는 것이다.__set_name__
은 descriptor instance를 소유 중인 class와 descriptor instnace가 대입될 attribute 이름을 argument로 받는다. cust = FixedCustomer()
print(f'이전: {cust.first_name!r} {cust.__dict__}')
cust.first_name = '메르센'
print(f'이후: {cust.first_name!r} {cust.__dict__}')
>>>
이전: '' {}
이후: '메르센' {'_first_name': '메르센'}
class FixedCustomer:
first_name = Field()
last_name = Field()
prefix = Field()
suffix = Field()
class Field:
def __init__(self):
self.name = None
self.internal_name = None
def __set_name__(self, owner, name):
"""
# 클래스가 생성될 때 모든 스크립터에 대해 이 메서드가 호출된다
owner: descriptor instance를 소유 중인 class
name: descriptor instance가 대입될 attribute 이름
"""
self.name = name
self.internal_name = '_' + name
def __get__(self, instance, instance_type):
if instance is None:
return self
return getattr(instance, self.internal_name, '')
def __set__(self, instance, value):
setattr(instance, self.internal_name, value)
from functools import wraps
trace_dict = TraceDict([('안녕', 1)])
trace_dict['거기'] = 2
trace_dict['안녕']
try:
trace_dict['존재하지 않음']
except KeyError:
pass # 키 오류가 발생할 것으로 예상함
class TraceDict(dict):
@trace_func
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@trace_func
def __setitem__(self, *args, **kwargs):
return super().__setitem__(*args, **kwargs)
@trace_func
def __getitem__(self, *args, **kwargs):
return super().__getitem__(*args, **kwargs)
def trace_func(func):
if hasattr(func, 'tracing'): # 단 한번만 데코레이터를 적용한다
return func
@wraps(func)
def wrapper(*args, **kwargs):
result = None
try:
result = func(*args, **kwargs)
return result
except Exception as e:
result = e
raise
finally:
print(f'{func.__name__}({args!r}, {kwargs!r}) -> '
f'{result!r}')
wrapper.tracing = True
return wrapper
import types
trace_dict = TraceDict([('안녕', 1)])
trace_dict['거기'] = 2
trace_dict['안녕']
try:
trace_dict['존재하지 않음']
except KeyError:
pass # 키 오류가 발생할 것으로 예상함
class TraceDict(dict, metaclass=TraceMeta):
pass
class TraceMeta(type):
def __new__(meta, name, bases, class_dict):
klass = super().__new__(meta, name, bases, class_dict)
for key in dir(klass):
value = getattr(klass, key)
if isinstance(value, trace_types):
wrapped = trace_func(value)
setattr(klass, key, wrapped)
return klass
def trace_func(func):
if hasattr(func, 'tracing'): # 단 한번만 데코레이터를 적용한다
return func
trace_types = (
types.MethodType,
types.FunctionType,
types.BuiltinFunctionType,
types.BuiltinMethodType,
types.MethodDescriptorType,
types.ClassMethodDescriptorType)
dict
)가 이미 metaclass를 정의한 경우 문제가 된다.def my_class_decorator(klass):
klass.extra_param = '안녕'
return klass
@my_class_decorator
class MyClass:
pass
print(MyClass)
>>>
<class '__main__.MyClass'>
print(MyClass.extra_param)
>>>
안녕
trace_dict = TraceDict([('안녕', 1)])
trace_dict['거기'] = 2
trace_dict['안녕']
try:
trace_dict['존재하지 않음']
except KeyError:
pass # 키 오류가 발생할 것으로 예상함
>>>
__new__((<class '__main__.TraceDict'>, [('안녕', 1)]), {}) -> {}
__getitem__(({'안녕': 1, '거기': 2}, '안녕'), {}) -> 1
__getitem__(({'안녕': 1, '거기': 2}, '존재하지 않음'), {}) -> KeyError('존재하지 않음')
@trace
class TraceDict(dict):
pass
def trace(klass):
for key in dir(klass):
value = getattr(klass, key)
if isinstance(value, trace_types):
wrapped = trace_func(value)
setattr(klass, key, wrapped)
return klass
def trace_func(func):
if hasattr(func, 'tracing'): # 단 한번만 데코레이터를 적용한다
return func
trace_types = (
types.MethodType,
types.FunctionType,
types.BuiltinFunctionType,
types.BuiltinMethodType,
types.MethodDescriptorType,
types.ClassMethodDescriptorType)