Computer Engineering/Fluent Python 정리

Fluent Python Chapter 12. 내장 자료형 상속과 다중 상속

jordan.bae 2022. 2. 8. 12:43

Introduction

11장에서는 파이썬에서 인터페이스를 동적 프로토콜(덕타이핑)과 ABC를 활용해서(구스타이핑) 구현하면서 각각의 장단점과 어떤 상황에 적절한지를 살펴봤습니다. 12장에서는 내장 자료형을 상속하면 어떤 문제가 있는지와 다중 상속을 하려고 할 때 어떤 부분들을 주의해야 하는지를 살펴봅니다. 다중 상속과 관련하여 믹스인과 MRO와 같은 개념들도 살펴보기 때문에 나중에 프레임워크를 만들어보고 싶으신 분들은 주의 깊게 보시면 좋을 것 같습니다.

 

책 정리를 시작한 이유

책 정리를 시작한 이유

Chapter1의 Introduction 부분에서 이야기 한 것처럼 지난 5년간 다양한 언어나 프레임워크 및 프로그램을 공부하고 이용하여 소프트웨어를 개발했는데 이것저것 하다 보니 자주 사용하는 언어임에도 불구하고 파이썬을 잘 활용하고 있느냐에 대한 답변을 자신 있게 하기 어렵다고 느껴서 Fluent Python이라는 책을 공부하며 정리하고 있습니다. 올해에는 새로운 기술들도 좋지만 기존에 활용하던 언어나 프레임워크 그리고 소프트웨어를 더 잘 사용할 수 있도록 깊게 공부할 수 있는 한 해를 만들고 싶은 소망을 가지고 있습니다. 21장까지 정리를 성공하고 맛있는 걸 먹으면서 스스로 축하할 날이 어서 왔으면 좋겠네요! 혹시, 제가 정리한 글이 괜찮으시면 구독 하시면 다음편이 나오면 바로 알림을 받으실 수 있습니다.

 

지난 chapter 정리한 포스팅

Fluent Python Chapter 1. 파이썬 데이터 모델 (Feat. 일관성 있는 언어)

Fluent Python Chapter 2-1. Sequence (Sequence의 분류, listcomp, generator, tuple, slicing

Fluent Python Chapter 2-2. Sequence (bisect, deque, memoryview)

Fluent Python Chapter 3. Dictionary (feat. hashtable)

Fluent Python Chapter 4. 텍스트와 바이트 (feat. 깨지지마라..)

Fluent Python Chapter 5. 일급 함수

Fluent Python Chapter 6. 일급 함수 디자인 패턴

Fluent Python Chapter 7. 함수 데커레이터와 클로저 (feat. 메타프로그래밍)

Fluent Python Chapter 8. 객체 잠조, 가변성, 재활용

Fluent Python Chapter 9. 파이썬스러운 객체

Fluent Python Chapter 10. 시퀀스 해킹, 해시, 슬라이스

Fluent Python Chapter 11. 인터페이스: 프로토콜에서 ABC까지

 

 

내장 자료형 상속

내장 자료형 상속은 위험 합니다. 왜냐하면 C 언어로 작성된 내장 클래스의 코드는 사용자가 오버라이드한 코드를 호출하지 않기 때문에 상당한 주의가 필요합니다. (그냥 사용하지 않고 사용자 데이터 class를 상속하는 것이 좋습니다.) 파이썬이 사용자가 오버라이드한 메서드를 지원하지 않는 이유는 성능 문제이다. 개인적으로 거의 사용할 일이 없는 경우를 위해서 속도와 복잡성을 포기하는 것은 좋지 않다.

 

코드를 보면 이해가 쉽다.

class DoppelDict(dict):
    def __setitem__(self, key, value):
        super().__setitem__(key, [value]*2)
        
dd = DoppelDict(one=1)
dd
{'one': 1}

dd['two'] = 2
dd
{'one': 1, 'two': [2, 2]}

dd.update(three=3)
dd
{'one': 1, 'two': [2, 2], 'three': 3}

위의 코드를 보는 것 처럼 우리가 overide한 __setitem__ 매직메서드는 update(), __init__() 에 사용되지 않는다. 이런 이유 때문에 내장형 자료형을 상속하는 것은 위험하다. (예상외의 동작)

 

파이썬으로 구현된 class를 상속해서 이 문제를 해결해보자.

import collections


class DoppelDict(collections.UserDict):
    def __setitem__(self, key, value):
        super().__setitem__(key, [value]*2)


dd = DoppelDict(one=1)
dd
{'one': [1, 1]}

dd['two'] = 2
dd
{'one': [1, 1], 'two': [2, 2]}

dd.update(three=3)
dd
{'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}

아주 잘 동작한다. 

 

 

다중 상속과 MRO

다중 상속의 정의를 먼저 살펴보면 아래와 같다. 

다중상속
(Multiple inheritance)이란 
객체 지향 프로그래밍
의 특징 중 하나이며, 어떤 클래스가 하나 이상의 상위 클래스로부터 여러 가지 행동이나 특징을 상속받을 수 있는 것을 말한다

출처: 위키백과

즉, 이름 그대로 클래스가 여러개의 클래스에서 상속받을 수 있는 것을 의미한다.

 

대표적인 예시로 '다이아몬드 문제'가 있다.

다이아몬드 문제

   
class A:
    def ping(self):
        print('ping:', self)


class B(A):
    def pong(self):
        print('pong:', self)


class C(A):
    def pong(self):
        print('PONG:', self)


class D(B, C):

    def ping(self):
        super().ping()
        print('post-ping:', self)

    def pingpong(self):
        self.ping()
        super().ping()
        self.pong()
        super().pong()
        C.pong(self)

 

위와 같이 상속을 받으면 D class의 pong() 메서드는 B와 C중에 어떤 클래스의 메서드를 상속 받는 것일까?

그래서 메서드 결정 순서(=Method Resolution Order, MRO)가 존재한다.

__mro__ 속성으로 MRO를 확인 할 수 있다. 여러 클래스를 한 번에 상속받을 때 왼쪽이 가장 나중에 상속받는 클래스이다. (즉, 전에 같은이름의 메서드가 있다면 override된다.) 그래서 아래 코드를 보면 B Class의 pong()이 실행된다. 만약 C클래스의 pong을 실행시키고 싶다면 C.pong(self) 이렇게 직접 호출할 수 있다. super()를 보여준 이유는 super()도 MRO를 따르기 때문이다. 만약에 Framework를 사용해서 override하는 경우에는 super()를 사용하는 것이 좋다. (직접 Class 호출하는 것은 지양.)

D.__mro__

(__main__.D, __main__.B, __main__.C, __main__.A, object)

d = D()
d.pingpong()
ping: <__main__.D object at 0x109789070>
post-ping: <__main__.D object at 0x109789070>
ping: <__main__.D object at 0x109789070>
pong: <__main__.D object at 0x109789070>
pong: <__main__.D object at 0x109789070>
PONG: <__main__.D object at 0x109789070>

 

 

Tkinter GUI 툴킷을 통해 다중상속에 대해 살펴보기

Tkinter GUI 툴킷은 표준 라이브러리에서 다중 상속을 극단적으로 사용하는 에를 보여준다.

import tkinter

tkinter.Text.__mro__
(tkinter.Text,
 tkinter.Widget,
 tkinter.BaseWidget,
 tkinter.Misc,
 tkinter.Pack,
 tkinter.Place,
 tkinter.Grid,
 tkinter.XView,
 tkinter.YView,
 object)
 
tkinter.Button.__mro__
(tkinter.Button,
 tkinter.Widget,
 tkinter.BaseWidget,
 tkinter.Misc,
 tkinter.Pack,
 tkinter.Place,
 tkinter.Grid,
 object)

다중 상속이 어려운 이유는 앨런 케이가 말한대로 상속은 다양한 목적으로 사용되는데 이 것을 다중으로 하니 설계가 어렵고 복잡도가 많이 증가 하기 때문이다.  책에서는 아래와 같은 조언을 따르게 뒤엉킨 클래스 그래프가 만들어지는 것을 예방할 수 있다고 말한다.

 

1. 인터페이스 상속과 구현 상속을 구분한다.

- 구현 상속은 재사용을 통해 코드 중복을 피하게 한다. -> 구성이나 위임으로 대체할 수 있는 경우도 있음.

- 인터페이스 상속은 'is-a' 관계를 의미하는 서브타입을 생성한다.

 

2. ABC를 이용해서 인터페이스를 명확히 한다.

인터페이스를 위한 클래스이면 abc.ABC를 상속하거나 다른 ABC를 상속한다.

 

3. 코드를 재사용하기 위해 믹스인을 사용한다.

믹스인 클래스는 재사용할 메서드를 묶어놓은 것 뿐이다. 'is-a' 관계를 나타내는 것이 아니라 여러 서브클래스에서 코드를 재사용하기 위해 설계된 클래스는 명시적으로 믹스인 클래스로 만들어야 한다. 믹스인 클래스로 객체를 생성해서는 안되고 단독으로 상속되도록 사용해서도 안된다. 다른 구상 메서드와 함께 상속되어 사용해야 한다. 

 

4. 이름을 통해 믹스인임을 명확히 한다.

Tkinter는 이런 권고를 따르지 않았지만 Django에서는 이런 권고를 따라서 구현되어 있다.

ex) TemplateResponseMixin

 

5. ABC가 믹스인 될 수는 있지만, 믹스인이라고 해서 ABC인 것은 아니다.

11장에서 본 것 처럼 ABC는 구상 클래스를 구현할 수 있으므로 믹스인으로 사용될 수 있지만 믹스인이라고 ABC는 아니다. ABC는 자료형을 정의하고, 단일 기저 클래스가 될 수도 있다.

 

6. 두 개 이상의 구상 클래스에서 상속받지 않는다.

구상 클래스는 0개 or 하나의 구상 슈퍼클래스를 가져야 한다. 즉, 구상클래스의 슈퍼클래스 중 하나를 제외한 나머지 클래스는 ABC나 믹스인이어야 한다. 아래의 사이트는 CBV에 대한 정보를 잘 정리해 두었다.

BaseListView는 View 구상 메서드와 MultipleObjectMixin, ContextMixin을 상속했다.

https://ccbv.co.uk/projects/Django/4.0/django.views.generic.base/View/

 

7. 사용자에게 집합 클래스를 제공한다.

ABC 또는 믹스인을 조합해서 호출 코드에 유용한 기능을 제공할 수 있을 때는, 이들을 적절히 통합하는 클래스를 제공하는 것이 좋다.

 

8. 클래스 상속보다 객체 구성을 사용하라.

객체를 계층구조로 깔끔하게 정리하면 보기 좋아지며, 프로그래머는 이를 즐기지만 구성을 통해 구현하면 더 융통성이 생기고 유연해진다.

 

 

위의 8가지 조언을 토대로 Tkinter를 평가해본다.

Tkinder는 7번을 제외하고는 거의 모든 조언을 따르지 않았다라고 한다... 

몇 가지만 보면 Widget은 구상 메서드를 가지고 있지 않다. 즉, 인터페이스를 정의하는데 ABC를 상속하지 않았다. 또한, 구현 상속과 인터페이스 상속을 구분하지 않았다.

 

위의 제안들을 지키지 않았지만 굉장히 파워풀하고 좋은 패키지라고 말한다...(병주고 약주고 느낌...)

 

 

Django GenericView의 믹스인

Django는 원래 함수를 사용해서 generic view를 구현해서 비슷한 무언가를 만들 때 처음부터 구현했어야 했다.

1.3 (이게 언제지..)에서 기반 클래스, 믹스인, 바로 사용할 수 있는 구상 클래스로 구성된 일련의 범용뷰 클래스와 함께 클래스 기반 뷰를 소개했다고 한다. View는 모든 뷰의 기반 클래스로서, 다양한 HTTP 동사를 처리하는 구상 서브클래스이다. 구상 서브 클래스가 구현한 get(), head(), post()등의 처리 메서드를 호출하는 dispatch() 메서드와 같은 핵심 기능을 제공한다. 

 

여기서 서브 클래스가 get(), post()같은 구상메서드를 구현해야 하는데 인터페이스로 정의되어 있지 않을까? 라는 질문을 할 수 있다.

아래 코드를 한 번 참고해서 생각해보자.

https://github.com/django/django/blob/67d0c4644acfd7707be4a31e8976f865509b09ac/django/views/generic/base.py#L29
class View:
    """
    Intentionally simple parent class for all views. Only implements
    dispatch-by-method and simple sanity checking.
    """

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

    def __init__(self, **kwargs):
        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in kwargs.items():
            setattr(self, key, value)

    @classonlymethod
    def as_view(cls, **initkwargs):
        """Main entry point for a request-response process."""
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError(
                    'The method name %s is not accepted as a keyword argument '
                    'to %s().' % (key, cls.__name__)
                )
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            self.setup(request, *args, **kwargs)
            if not hasattr(self, 'request'):
                raise AttributeError(
                    "%s instance has no 'request' attribute. Did you override "
                    "setup() and forget to call super()?" % cls.__name__
                )
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # __name__ and __qualname__ are intentionally left unchanged as
        # view_class should be used to robustly determine the name of the view
        # instead.
        view.__doc__ = cls.__doc__
        view.__module__ = cls.__module__
        view.__annotations__ = cls.dispatch.__annotations__
        # Copy possible attributes set by decorators, e.g. @csrf_exempt, from
        # the dispatch method.
        view.__dict__.update(cls.dispatch.__dict__)

        return view

    def setup(self, request, *args, **kwargs):
        """Initialize attributes shared by all view methods."""
        if hasattr(self, 'get') and not hasattr(self, 'head'):
            self.head = self.get
        self.request = request
        self.args = args
        self.kwargs = kwargs

    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

    def http_method_not_allowed(self, request, *args, **kwargs):
        logger.warning(
            'Method Not Allowed (%s): %s', request.method, request.path,
            extra={'status_code': 405, 'request': request}
        )
        return HttpResponseNotAllowed(self._allowed_methods())

    def options(self, request, *args, **kwargs):
        """Handle responding to requests for the OPTIONS HTTP verb."""
        response = HttpResponse()
        response.headers['Allow'] = ', '.join(self._allowed_methods())
        response.headers['Content-Length'] = '0'
        return response

    def _allowed_methods(self):
        return [m.upper() for m in self.http_method_names if hasattr(self, m)]

이는 인터페이스를 통해서 강제하지 않은 이유는 서브클래스가 자신이 지원하려고 하는 handler만 구현할수 있도록 하게 하기 위해서이다.

이런 부분은 파이썬의 철학을 잘 담은 것 같다는 생각이 들었다. ListView라면 post(), put() 핸들러가 필요 없을 것이다.

 

 

마무리

가장 먼저 살펴본 것 처럼 C언어로 구현된 네이티브 메서드는 서브클래스에서 오버라이드해도 호출 되지 않기 때문에 내장형을 상속하는 것은 위험하다는 것을 살펴봤다. 또한 다중 상속을 어떻게 사용해야 하는지에 대해서 살펴봤고, 다중 상속을 사용하고 있는 Tkinter와 Django 클래스 기반 뷰에 대해서도 살펴봤다. Django코드를 볼 때 많이 도움이 될 것 같다.

 

 

Reference

Fluent Python Chapter 12

https://ccbv.co.uk/projects/Django/4.0/django.views.generic.base/View/

 

View -- Classy CBV

def http_method_not_allowed(self, request, *args, **kwargs): logger.warning( 'Method Not Allowed (%s): %s', request.method, request.path, extra={'status_code': 405, 'request': request} ) return HttpResponseNotAllowed(self._allowed_methods())

ccbv.co.uk

https://docs.djangoproject.com/en/4.0/topics/class-based-views/mixins/

 

Using mixins with class-based views | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

https://github.com/fluentpython/example-code/tree/master/12-inheritance

 

GitHub - fluentpython/example-code: Example code for the book Fluent Python, 1st Edition (O'Reilly, 2015)

Example code for the book Fluent Python, 1st Edition (O'Reilly, 2015) - GitHub - fluentpython/example-code: Example code for the book Fluent Python, 1st Edition (O'Reilly, 2015)

github.com

 

반응형