상속을 통한 확장 가능한 프로그램

Beginner

This tutorial is from open-source community. Access the source code

소개

상속은 확장 가능한 프로그램을 작성하기 위해 널리 사용되는 도구입니다. 이 섹션에서는 그 아이디어를 살펴봅니다.

상속 (Inheritance)

상속은 기존 객체를 특수화하는 데 사용됩니다.

class Parent:
    ...

class Child(Parent):
    ...

새로운 클래스 Child는 파생 클래스 또는 서브클래스라고 불립니다. Parent 클래스는 기본 클래스 또는 슈퍼클래스로 알려져 있습니다. Parent는 클래스 이름 뒤의 () 안에 지정됩니다. 즉, class Child(Parent): 와 같습니다.

확장 (Extending)

상속을 사용하면 기존 클래스를 가져와서 다음을 수행할 수 있습니다.

  • 새로운 메서드 추가
  • 기존 메서드 중 일부 재정의
  • 인스턴스에 새로운 속성 추가

결국, 기존 코드를 확장하는 것입니다.

예시 (Example)

다음은 시작 클래스라고 가정해 보겠습니다.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

상속을 통해 이 클래스의 모든 부분을 변경할 수 있습니다.

새로운 메서드 추가 (Add a new method)

class MyStock(Stock):
    def panic(self):
        self.sell(self.shares)

사용 예시입니다.

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.sell(25)
>>> s.shares
75
>>> s.panic()
>>> s.shares
0
>>>

기존 메서드 재정의 (Redefining an existing method)

class MyStock(Stock):
    def cost(self):
        return 1.25 * self.shares * self.price

사용 예시입니다.

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.cost()
61262.5
>>>

새로운 메서드가 이전 메서드를 대체합니다. 다른 메서드들은 영향을 받지 않습니다. 굉장합니다.

오버라이딩 (Overriding)

때로는 클래스가 기존 메서드를 확장하지만, 재정의 내에서 원래 구현을 사용하고 싶을 수 있습니다. 이를 위해 super()를 사용합니다.

class Stock:
    ...
    def cost(self):
        return self.shares * self.price
    ...

class MyStock(Stock):
    def cost(self):
        ## Check the call to `super`
        actual_cost = super().cost()
        return 1.25 * actual_cost

super()를 사용하여 이전 버전을 호출합니다.

주의: Python 2 에서는 구문이 더 장황했습니다.

actual_cost = super(MyStock, self).cost()

__init__과 상속 (inheritance)

__init__이 재정의된 경우, 부모를 초기화하는 것이 필수적입니다.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

class MyStock(Stock):
    def __init__(self, name, shares, price, factor):
        ## Check the call to `super` and `__init__`
        super().__init__(name, shares, price)
        self.factor = factor

    def cost(self):
        return self.factor * super().cost()

이전에 보여드린 것처럼, 이전 버전을 호출하는 방법인 super에서 __init__() 메서드를 호출해야 합니다.

상속 (Inheritance) 사용하기

상속은 때때로 관련 객체를 구성하는 데 사용됩니다.

class Shape:
    ...

class Circle(Shape):
    ...

class Rectangle(Shape):
    ...

논리적 계층 구조 또는 분류 체계를 생각해 보세요. 그러나 더 일반적이고 실용적인 사용법은 재사용 가능하거나 확장 가능한 코드를 만드는 것과 관련이 있습니다. 예를 들어, 프레임워크는 기본 클래스를 정의하고 이를 사용자 정의하도록 지시할 수 있습니다.

class CustomHandler(TCPHandler):
    def handle_request(self):
        ...
        ## Custom processing

기본 클래스에는 몇 가지 범용 코드가 포함되어 있습니다. 사용자의 클래스는 특정 부분을 상속받아 사용자 정의합니다.

"is a" 관계

상속은 타입 관계를 설정합니다.

class Shape:
    ...

class Circle(Shape):
    ...

객체 인스턴스를 확인합니다.

>>> c = Circle(4.0)
>>> isinstance(c, Shape)
True
>>>

중요: 이상적으로, 부모 클래스의 인스턴스로 작동하는 모든 코드는 자식 클래스의 인스턴스로도 작동합니다.

object 기본 클래스

클래스에 부모가 없는 경우, 때때로 object가 기본 클래스로 사용되는 것을 볼 수 있습니다.

class Shape(object):
    ...

object는 Python 의 모든 객체의 부모입니다.

*참고: 기술적으로는 필요하지 않지만, Python 2 에서 필수적으로 사용되었던 것에서 유래하여 종종 명시적으로 지정되는 것을 볼 수 있습니다. 생략하더라도 클래스는 여전히 암묵적으로 object를 상속받습니다.

다중 상속 (Multiple Inheritance)

클래스 정의에서 여러 클래스를 지정하여 여러 클래스에서 상속받을 수 있습니다.

class Mother:
    ...

class Father:
    ...

class Child(Mother, Father):
    ...

Child 클래스는 두 부모로부터 기능을 상속받습니다. 몇 가지 까다로운 세부 사항이 있습니다. 무엇을 하고 있는지 확실히 알고 있지 않다면 사용하지 마십시오. 다음 섹션에서 추가 정보가 제공되지만, 이 과정에서는 다중 상속을 더 이상 사용하지 않을 것입니다.

상속의 주요 용도는 특히 라이브러리나 프레임워크에서 다양한 방식으로 확장하거나 사용자 정의할 수 있도록 설계된 코드를 작성하는 것입니다. 예를 들어, report.py 프로그램의 print_report() 함수를 생각해 보십시오. 다음과 같은 형태일 것입니다.

def print_report(reportdata):
    '''
    Print a nicely formatted table from a list of (name, shares, price, change) tuples.
    '''
    headers = ('Name','Shares','Price','Change')
    print('%10s %10s %10s %10s' % headers)
    print(('-'*10 + ' ')*len(headers))
    for row in reportdata:
        print('%10s %10d %10.2f %10.2f' % row)

보고서 프로그램을 실행하면 다음과 같은 출력을 얻을 수 있습니다.

>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84

연습 문제 4.5: 확장성 문제

print_report() 함수를 수정하여 일반 텍스트, HTML, CSV 또는 XML 과 같은 다양한 출력 형식을 지원하려는 경우를 가정해 보겠습니다. 이를 위해 모든 것을 수행하는 거대한 함수 하나를 작성할 수 있습니다. 그러나 그렇게 하면 유지 관리가 불가능한 혼란을 초래할 수 있습니다. 대신, 이는 상속을 사용하는 완벽한 기회입니다.

시작하려면 테이블을 만드는 데 관련된 단계에 집중하십시오. 테이블 상단에는 테이블 헤더 집합이 있습니다. 그 후, 테이블 데이터 행이 나타납니다. 이러한 단계를 가져와 자체 클래스에 넣습니다. tableformat.py라는 파일을 만들고 다음 클래스를 정의합니다.

## tableformat.py

class TableFormatter:
    def headings(self, headers):
        '''
        Emit the table headings.
        '''
        raise NotImplementedError()

    def row(self, rowdata):
        '''
        Emit a single row of table data.
        '''
        raise NotImplementedError()

이 클래스는 아무것도 하지 않지만, 곧 정의될 추가 클래스에 대한 일종의 설계 사양 역할을 합니다. 이와 같은 클래스를 때때로 "추상 기본 클래스 (abstract base class)"라고 합니다.

print_report() 함수를 수정하여 TableFormatter 객체를 입력으로 받아 출력을 생성하기 위해 해당 객체에서 메서드를 호출하도록 합니다. 예를 들어, 다음과 같이 합니다.

## report.py
...

def print_report(reportdata, formatter):
    '''
    Print a nicely formatted table from a list of (name, shares, price, change) tuples.
    '''
    formatter.headings(['Name','Shares','Price','Change'])
    for name, shares, price, change in reportdata:
        rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
        formatter.row(rowdata)

print_report()에 인수를 추가했으므로 portfolio_report() 함수도 수정해야 합니다. 다음과 같이 TableFormatter를 생성하도록 변경합니다.

## report.py

import tableformat

...
def portfolio_report(portfoliofile, pricefile):
    '''
    Make a stock report given portfolio and price data files.
    '''
    ## Read data files
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Create the report data
    report = make_report_data(portfolio, prices)

    ## Print it out
    formatter = tableformat.TableFormatter()
    print_report(report, formatter)

이 새로운 코드를 실행합니다.

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
... crashes ...

NotImplementedError 예외로 즉시 충돌해야 합니다. 그렇게 흥미롭지는 않지만, 정확히 예상했던 것입니다. 다음 부분으로 진행합니다.

연습 문제 4.6: 상속을 사용하여 다른 출력 생성

(a) 부분에서 정의한 TableFormatter 클래스는 상속을 통해 확장되도록 설계되었습니다. 사실, 그것이 바로 핵심 아이디어입니다. 이를 설명하기 위해 다음과 같이 TextTableFormatter 클래스를 정의합니다.

## tableformat.py
...
class TextTableFormatter(TableFormatter):
    '''
    Emit a table in plain-text format
    '''
    def headings(self, headers):
        for h in headers:
            print(f'{h:>10s}', end=' ')
        print()
        print(('-'*10 + ' ')*len(headers))

    def row(self, rowdata):
        for d in rowdata:
            print(f'{d:>10s}', end=' ')
        print()

portfolio_report() 함수를 다음과 같이 수정하고 시도해 보십시오.

## report.py
...
def portfolio_report(portfoliofile, pricefile):
    '''
    Make a stock report given portfolio and price data files.
    '''
    ## Read data files
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Create the report data
    report = make_report_data(portfolio, prices)

    ## Print it out
    formatter = tableformat.TextTableFormatter()
    print_report(report, formatter)

이전과 동일한 출력이 생성되어야 합니다.

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

하지만 출력을 다른 것으로 변경해 보겠습니다. CSV 형식으로 출력을 생성하는 새로운 클래스 CSVTableFormatter를 정의합니다.

## tableformat.py
...
class CSVTableFormatter(TableFormatter):
    '''
    Output portfolio data in CSV format.
    '''
    def headings(self, headers):
        print(','.join(headers))

    def row(self, rowdata):
        print(','.join(rowdata))

주 프로그램을 다음과 같이 수정합니다.

def portfolio_report(portfoliofile, pricefile):
    '''
    Make a stock report given portfolio and price data files.
    '''
    ## Read data files
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Create the report data
    report = make_report_data(portfolio, prices)

    ## Print it out
    formatter = tableformat.CSVTableFormatter()
    print_report(report, formatter)

이제 다음과 같은 CSV 출력을 볼 수 있습니다.

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84

유사한 아이디어를 사용하여 다음과 같은 출력을 생성하는 HTMLTableFormatter 클래스를 정의합니다.

<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</th></tr>
<tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr>
<tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr>
<tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr>
<tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr>
<tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr>
<tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr>
<tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>

주 프로그램을 수정하여 CSVTableFormatter 객체 대신 HTMLTableFormatter 객체를 생성하여 코드를 테스트합니다.

연습 문제 4.7: 다형성 (Polymorphism) 실행

객체 지향 프로그래밍의 주요 특징은 객체를 프로그램에 연결하면 기존 코드를 변경하지 않고도 작동한다는 것입니다. 예를 들어, TableFormatter 객체를 사용하도록 예상하는 프로그램을 작성했다면, 실제로 어떤 종류의 TableFormatter를 제공하든 상관없이 작동합니다. 이러한 동작을 때때로 "다형성 (polymorphism)"이라고 합니다.

한 가지 잠재적인 문제는 사용자가 원하는 포맷터를 선택할 수 있도록 하는 방법을 파악하는 것입니다. TextTableFormatter와 같은 클래스 이름을 직접 사용하는 것은 종종 성가십니다. 따라서 몇 가지 단순화된 접근 방식을 고려할 수 있습니다. 아마도 다음과 같이 코드에 if-문을 포함할 수 있습니다.

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    Make a stock report given portfolio and price data files.
    '''
    ## Read data files
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Create the report data
    report = make_report_data(portfolio, prices)

    ## Print it out
    if fmt == 'txt':
        formatter = tableformat.TextTableFormatter()
    elif fmt == 'csv':
        formatter = tableformat.CSVTableFormatter()
    elif fmt == 'html':
        formatter = tableformat.HTMLTableFormatter()
    else:
        raise RuntimeError(f'Unknown format {fmt}')
    print_report(report, formatter)

이 코드에서 사용자는 'txt' 또는 'csv'와 같은 단순화된 이름을 지정하여 형식을 선택합니다. 그러나 portfolio_report() 함수에 큰 if-문을 넣는 것이 최선의 아이디어일까요? 해당 코드를 다른 곳의 범용 함수로 이동하는 것이 더 나을 수 있습니다.

tableformat.py 파일에 사용자가 'txt', 'csv' 또는 'html'과 같은 출력 이름을 지정하여 포맷터를 생성할 수 있도록 하는 함수 create_formatter(name)을 추가합니다. portfolio_report()를 다음과 같이 수정합니다.

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    Make a stock report given portfolio and price data files.
    '''
    ## Read data files
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Create the report data
    report = make_report_data(portfolio, prices)

    ## Print it out
    formatter = tableformat.create_formatter(fmt)
    print_report(report, formatter)

다양한 형식으로 함수를 호출하여 제대로 작동하는지 확인합니다.

연습 문제 4.8: 모든 것을 통합하기

report.py 프로그램을 수정하여 portfolio_report() 함수가 출력 형식을 지정하는 선택적 인수를 받도록 합니다. 예를 들어:

>>> report.portfolio_report('portfolio.csv', 'prices.csv', 'txt')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

명령줄에서 형식을 지정할 수 있도록 주 프로그램을 수정합니다.

$ python3 report.py portfolio.csv prices.csv csv
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
$

토론

확장 가능한 코드를 작성하는 것은 라이브러리 및 프레임워크에서 상속을 가장 일반적으로 사용하는 경우 중 하나입니다. 예를 들어, 프레임워크는 제공된 기본 클래스에서 상속하는 자체 객체를 정의하도록 지시할 수 있습니다. 그런 다음 다양한 기능의 비트를 구현하는 다양한 메서드를 채우도록 지시받습니다.

또 다른 다소 심오한 개념은 "추상화 (abstraction) 를 소유하는 것"에 대한 아이디어입니다. 연습 문제에서 우리는 테이블 형식을 지정하기 위한 자체 클래스를 정의했습니다. 코드를 보고 "그냥 다른 사람이 이미 만든 형식 지정 라이브러리나 무언가를 사용해야 해!"라고 생각할 수 있습니다. 아니요, 자신의 클래스와 라이브러리를 모두 사용해야 합니다. 자체 클래스를 사용하면 느슨한 결합 (loose coupling) 이 촉진되고 더 유연해집니다. 애플리케이션이 클래스의 프로그래밍 인터페이스를 사용하는 한, 내부 구현을 원하는 방식으로 변경할 수 있습니다. 모든 사용자 정의 코드를 작성할 수 있습니다. 다른 사람의 타사 패키지를 사용할 수 있습니다. 더 나은 패키지를 찾으면 하나의 타사 패키지를 다른 패키지로 교체할 수 있습니다. 중요하지 않습니다. 인터페이스를 유지하는 한 애플리케이션 코드는 깨지지 않습니다. 이것은 강력한 아이디어이며, 이와 같은 경우 상속을 고려할 수 있는 이유 중 하나입니다.

그렇긴 하지만, 객체 지향 프로그램을 설계하는 것은 매우 어려울 수 있습니다. 자세한 내용은 디자인 패턴 (design patterns) 에 대한 책을 찾아보는 것이 좋습니다 (이 연습에서 일어난 일을 이해하면 객체를 실용적으로 사용하는 데 상당히 도움이 될 것입니다).

요약

축하합니다! 상속 (Inheritance) 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.