파이썬 코딩의 기술

파이썬코딩의기술 - Betterway 48 : __init_subclass__를 사용해 하위 클래스를 검증하라

J-Chris 2023. 3. 24. 10:28

메타클래스의 가장 간단한 활용법 중 하나는 어떤 클래스가 제대로 구현됐는지 검증하는 것 입니다.

- (예) 메서드를 오버라이드 하도록 요청 / 클래스 애트리뷰트 사이에 엄격한 관계를 가지도록 요구/ 복잡한 클래스 계층을 설계할 때 어떤 스타일 강제로 지키도록 만듬

- 메타클래스는 이런 목적을 달성할 수 있음

- 새로운 하위 클래스가 정의될 때마다 이런 검증 코드를 수행하는 신뢰성 있는 방법을 제공하기 때문.

 

어떤 클래스 타입의 객체가 실행 시점에 생성될 때 클래스 검증 코드를 __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__를 호출해 여러 계층에 걸쳐 클래스를 검증하고 다중 상속을 제대로 처리하도록 하라.