파이썬코딩의기술 - Betterway 48 : __init_subclass__를 사용해 하위 클래스를 검증하라
메타클래스의 가장 간단한 활용법 중 하나는 어떤 클래스가 제대로 구현됐는지 검증하는 것 입니다.
- (예) 메서드를 오버라이드 하도록 요청 / 클래스 애트리뷰트 사이에 엄격한 관계를 가지도록 요구/ 복잡한 클래스 계층을 설계할 때 어떤 스타일 강제로 지키도록 만듬
- 메타클래스는 이런 목적을 달성할 수 있음
- 새로운 하위 클래스가 정의될 때마다 이런 검증 코드를 수행하는 신뢰성 있는 방법을 제공하기 때문.
어떤 클래스 타입의 객체가 실행 시점에 생성될 때 클래스 검증 코드를 __init__ 메서드 안에서 실행하는 경우도 종종 있습니다.
하위 클래스를 검증하는 메타클래스를 정의하는 방법을 살펴보기 전에, 일반적인 객체에 대해 메타클래스가 어떻게 작동하는지 이해하는 것이 중요합니다.
- 메타클래스는 type을 상속해 정의됨
- (기본적으로) 메타클래스는 __new__메서드를 통해 자신과 연관된 클래스의 내용을 받음
- 어떤 타입이 실제로 구성되기 전에 클래스 정보를 살펴보고 변경하는 모습을 보여줌
class Meta(type):
def __new__(meta, name, bases, class_dict):
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__on MyClass.foo at 0x0000012CB7108BF8>}
기반 클래스들: ()
{'__module__': '__main__', '__qualname__': 'MyClass', 'stuff': 123, 'foo': <function MyClass.foo at 0x0000012CB7108BF8>}ction MySubclass.bar at 0x0000012CB7108C80>}
* 실행: MySubclass의 메타 <class '__main__.Meta'>.__new__
기반 클래스들: (<class '__main__.MyClass'>,ills_2nd (main)
{'__module__': '__main__', '__qualname__': 'MySubclass', 'other': 567, 'bar': <function MySubclass.bar at 0x0000012CB7108C80>}
메타클래스는 클래스 이름, 클래스가 상속하는 부모 클래스들(bases), class의 본문에 정의된 모든 클래스 애트리뷰트에 접근할 수 있습니다,
연관된 클래스가 정의되기 전에 이 클래스의 모든 파라미터를 검증하려면 Meta.__new__에 기능을 추가해야합니다.
- (예) 다각형을 표현하는 타입을 만든다고 하자.
- 이때 검증을 수행하는 특별한 메타클래스를 정의하고, 이 메타클래스를 모든 다각형 클래스 계층 구조의 가반 클래스로 사용할 수 있다.
- 기반 클래스에 대해서는 같은 검증을 수행하지 않는다는 사실에 유의하라.
class ValidatePolygon(type):
def __new__(meta, name, bases, class_dict):
# Polygon 클래스의 하위 클래스만 검증한다
if bases:
if class_dict['sides'] < 3:
raise ValueError('다각형 변은 3개 이상이어야함')
return type.__new__(meta, name, bases, class_dict)
class Polygon(metaclass=ValidatePolygon):
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
이 검증은 class문에서 변 개수가 3보다 작은 경우에 해당 class 정의문의 본문이 실행된 직후 예외를 발생시킨다.
print('class 이전')
class Line(Polygon):
print('sides 이전')
sides = 2
print('sides 이후')
print('class 이후')
>>>
class 이전
sides 이전
sides 이후
파이썬 3.6에는 메타클래스를 정의하지 않고 같은 동작을 구현할 수 있는 더 단순한 구문(__init_subclass__ 특별 클래스 메서드를 정의하는 방식)이 추가됐다.
다음 코드는 이 방식을 사용해 앞에서 본 예제와 똑같은 수준의 검증을 제공합니다.
class BetterPolygon:
sides = None # 하위 클래스에서 이 애트리뷰트의 값을 지정해야함
def __init_subclass__(cls):
super().__init_subclass__()
if cls.sides < 3:
raise ValueError('다각형 변은 3개 이상이어야 함')
@classmethod
def interior_angles(cls):
return ()
3
- 코드가 훨씬 짧아졌고, ValidatePolygon 메타클래스가 완전히 사라졌습니다.
- class_dict['sides']를 통해 클래스 딕셔너리에서 sides를 가져올 필요가 없습니다.
- __init_subclass__안에서는 cls 인스턴스에서 sides 애트리뷰트를 직접 가자올 수 있으므로 코드를 이해하기 훨씬 쉽습니다.
BetterPolyfon의 하위 클래스를 잘못 정의하면 앞의 예제와 똑같은 예외를 볼 수 있습니다.
4
표준 파이썬 메타클래스 방식의 또 다른 문제점은 클래스 정의마다 메타클래스를 단 하나만 지정할 수 있다는 것입니다.
- 다음 코드는 어떤 영역에 칠할 색을 검증하기 위한 메타클래스임
5
Polygon 메타클래스와 Filled 메타클래스를 함께 사용하려고 시도하면 이해하기 힘든 오류 메시지를 볼 수 있습니다.
6
검증을 여러 단계로 만들기 위해 복잡한 메타클래스 type 정의를 복잡한 계층으로 설계함으로써 이런 문제를 해결할 수도 있습니다.
7
이렇게 정의하면 모든 FilledPolygon은 Polygon의 인스턴스가 됩니다.
8
색을 검증하면 잘 작동합니다.
9
변의 개수 검증도 잘 작동합니다.
하지만 이런 접근 방법은 합성성(ㅇcompasability)을 해친다.
__init_subclass__ 특별 클래스 메서드를 사용하면 이 문제도 해결할 수 있습니다.
10
새로운 클래스에서 BetterPolygon과 Filled 클래스를 모두 상속할 수 있습니다.
- 두 클래스는 모두 super().__init_subclass__()를 호출하기 때문에 하위 클래스가 생성될 때 각각의 검증 로직이 실행됨
11
변의 수를 잘못 지정하면 검증 오류가 발생합니다.
12
색을 잘못 지정해도 검증 오류가 발생합니다.
13
심지어 __init__subclass를 다이아몬드 상속 같은 복잡한 경우에도 사용할 수 있습니다.
- 다음 코드에서는 기본적인 다이아몬드 상속 구조를 만들어 __init_subclass__의 동작을 보여줍니다.
14
예상한 대로 Bottom 클래스에서 Top에 이르는 상속 경로가 두가지지만,
각 클래스마다 Top.__init_subclass__는 단 한 번만 호출됩니다.
### 기억해야할 내용 ###
- 메타클래스의 __new__메서드는 class문의 모든 본문이 처리된 직후에 호출된다.
- 메타클래스를 사용해 클래스가 정의된 직후이면서 클래스가 생성되기 직전인 시점에 클래스 정의를 변경할 수 있다. 하지만 메타클래스는 원하는 목적을 달성하기에 너무 복잡해지는 경우가 많습니다.
- __init_subclass를 사용해 하위 클래스가 정의된 직후, 하위 클래스 타입이 만들어지 직전에 해당 클래스가 원하는 요건을 잘 갖췄는지 확인하라
- __init_subclass 정의 안에서 super().__init_subclass__를 호출해 여러 계층에 걸쳐 클래스를 검증하고 다중 상속을 제대로 처리하도록 하라.