초콜릿 케익 ⊂ 케익 ⊂ 음식

초콜릿 케익은 케익에 포함되고, 케익은 음식에 포함된다. 범주는 더 넓은 범주에 포함되고, 더 좁은 범주를 포함한다. 데이터의 범주도 마찬가지여서, ‘케익’ 유형에 속하는 ‘초콜릿 케익’ 유형과 ‘아이스크림 케익’ 유형을 정의할 수도 있다. 이 절에서는 다른 클래스에 포함되는 하위 클래스를 정의하는 방법과 클래스의 포함 관계에서 나타나는 특징을 알아본다.

8.4.1 하위 클래스 정의하기

클래스를 정의할 때 상위 클래스를 지정할 수 있다. 헤더 행에서 클래스 이름 뒤에 괄호를 붙이고, 괄호 속에 상위 클래스로 삼을 클래스를 표기하면 된다.

class B(A):
    """A 클래스의 하위 클래스인 B 클래스"""
    이 하위 클래스의 공용 속성

케익이라는 범주는 초콜릿 케익, 치즈 케익, 과일 케익 등의 하위 범주로 세분화해 볼 수 있다. 다음은 Cake 클래스의 하위 클래스로 초콜릿 케익을 나타내는 ChocolateCake 클래스를 정의해 본 것이다.

코드 8-40 ‘케익’ 클래스를 상위 클래스로 삼는 ‘초콜릿 케익’ 클래스 정의하기

class ChocolateCake(Cake):                # ❶ 
    """초콜릿 케익을 나타내는 클래스."""
    coat = '초콜릿'                       # ❷
    cacao_percent = 32.0                  # ❸

ChocolateCake 클래스 정의에서 괄호 안에 Cake을 표기했다. Cake 클래스를 상위 클래스로 삼은 것이다.

coat 속성은 상위 클래스인 Cake'생크림'으로 정의되어 있다. 그런데 ChocolateCake 클래스에서는 초콜릿으로 정의했다. 하위 클래스에서 coat 속성을 읽으면 상위 클래스의 '생크림'이 아니라 새로 정의한 '초콜렛'으로 평가된다. 이처럼 상위 클래스에 정의된 속성은 하위 클래스에서 고쳐 정의할 수 있다. 이를 재정의(override)라고 한다.

cacao_percentCake에는 없지만 ChocolateCake에서 새로 추가되었다. 이처럼 상위 클래스에 없는 속성을 하위 클래스에 추가로 정의할 수도 있다.

ChocolateCake 클래스의 속성을 읽어 보고, 인스턴스를 생성해 메서드를 실행해 보자.

코드 8-41 ChocolateCake 클래스와 인스턴스의 동작

print(ChocolateCake.coat)           # ❶ 재정의한 클래스 속성
print(ChocolateCake.cacao_percent)  # ❷ 추가한 클래스 속성

# ❸ 상위 클래스 Cake의 __init__() 메서드와 __describe__() 메서드 이용하기
chocolate_cake_1 = ChocolateCake('이슬', 12000)
chocolate_cake_1.describe()

실행 결과:

초콜릿
32.0
이 케익은 초콜릿 으로 덮여 있다.
이슬 을 올려 장식했다.
가격은 12000 원이다.
초가 0 개 꽂혀 있다.

❶ 하위 클래스에서 재정의한 속성은 상위 클래스의 속성을 덮어버린다. ❷ 하위 클래스에서 추가한 속성은 하위 클래스에서 읽을 수 있다. ❸ 재정의하지 않은 속성과 메서드는 그대로 사용할 수 있다. chocolate_cake_1 인스턴스를 만들고 사용할 때, 상위 클래스 Cake__init__() 메서드와 describe() 메서드를 그대로 사용했다.

상속·확장: 하위 클래스 정의를 가리키는 다른 용어

어떤 클래스의 하위 클래스를 정의하는 것을 ‘상속(inherit)’ 또는 ‘확장(extend)’이라고 부르기도 한다. 다음은 모두 같은 뜻이다.

  • 상위 클래스 A의 하위 클래스 B를 정의한다.
  • A 클래스를 상속하는 B 클래스를 정의한다.
  • A 클래스를 확장하는 B 클래스를 정의한다.

하위 클래스가 상위 클래스의 속성을 공유하는 것이 마치 자식이 부모의 특성을 대물림하는 것과 비슷하기 ‘상속’이라는 용어가 붙었다. 하위 클래스는 상위 클래스의 모든 속성을 고스란히 가지면서도 속성을 추가로 더 가질 수 있기 때문에 상위 클래스를 ‘확장’한다고 부르기도 한다.

__init__() 메서드 재정의하기

하나의 상위 범주에는 다양한 하위 범주가 속할 수 있다. Cake 클래스의 또 다른 하위 객체로, 아이스크림 케익을 나타내는 클래스 IceCreamCake을 정의해 보자. __init__() 메서드도 재정의하자.

코드 8-42 IceCreamCake을 정의하고 __init__() 메서드 재정의하기

class IceCreamCake(Cake):
    """아이스크림 케익을 나타내는 클래스."""
    coat = '아이스크림'
    flavor = '정해지지 않은 맛'
    
    def __init__(self, flavor, topping, price, candles=0):  # ❶
        """인스턴스를 초기화한다."""
        self.flavor = flavor  # 아이스크림의 맛             # ❷
        super().__init__(self, topping, price, candles)     # ❸

IceCreamCake 클래스는 ❶ __init__() 메서드를 재정의했다. 이 메서드는 전달받은 인자 중에서 flavor만 ❷ 인스턴스 초기화에 사용하고, 나머지 인자는 직접 사용하지 않는다. ❸ 대신, 상위 클래스인 Cake 클래스의 __init__() 메서드에 남은 인자를 전달해 초기화하도록 하였다. 여기서 사용한 super()는 이 인스턴스가 속한 클래스의 상위 클래스로 평가되는 함수로, 실행하면 Cake 클래스가 된다. 이 메서드에서 인스턴스의 모든 속성을 직접 초기화할 수도 있지만, 코드가 중복되는 것을 방지하고 상위 클래스의 동작을 그대로 따를 수 있으므로 이 방식이 더 유리하다. 아이스크림 케익 클래스의 인스턴스를 생성하여 초기화 결과를 확인해보자.

코드 8-43 하위 클래스의 인스턴스

ice_cream_cake_1 = IceCreamCake('바닐라맛', '쿠키 인형', 12000)
ice_cream_cake_1.describe()

실행 결과:

이 케익은 아이스크림 으로 덮여 있다.
쿠키 인형 을 올려 장식했다.
가격은 12000 원이다.
초가 0 개 꽂혀 있다.

하위 클래스의 초기화 메서드 IceCreamCake.__init__()에서 정의한 속성 flavor와 상위 클래스의 초기화 메서드 super().__init__()에서 정의한 속성 topping, price, candles가 모두 잘 초기화되어 있다.

개념 정리

  • 클래스의 하위 클래스를 정의할 수 있다.
  • 하위 클래스는 상위 클래스의 속성을 사용할 수 있고, 자신만의 속성을 가질 수도 있다.
  • 하위 클래스에서 상위 클래스를 가리킬 때 super() 함수를 사용할 수 있다.

8.4.2 클래스의 계층 살펴보기

범주는 여러 단계의 계층을 형성할 수 있으므로, 여러 단계의 하위 클래스를 정의할 수도 있다. 다음은 Cake 클래스의 하위 클래스인 IceCreamCake 클래스의 하위 클래스인 FruitIceCreamCake 클래스를 정의해 본 것이다.

코드 8-44 하위 클래스의 하위 클래스

class FruitIceCreamCake(IceCreamCake):
    """과일 아이스크림 케익을 나타내는 클래스."""
    def __init__(self, fruit_percent, flavor, topping, price, candles=0):
        """인스턴스를 초기화한다."""
        self.fruit_percent = fruit_percent  # 과일 함량 (퍼센트)
        super().__init__(self, flavor, topping, price, candles)  # IceCreamCake.__init__()

파이썬의 모든 클래스는 object 클래스의 하위 클래스다. 클래스를 정의할 때 다른 클래스를 상속하지 않으면 object 클래스를 상속하여 정의된다. 과일 아이스크림 케익 클래스와 다른 클래스의 계층 관계를 생각해보면, FruitIceCreamCakeIceCreamCakeCakeobject라는 것을 이해할 수 있을 것이다.

그림 8-4 이 장에서 정의한 케익 클래스들의 계층 (준비중)

하위 클래스 확인하기

issubclass() 함수를 사용하면 어떤 클래스가 다른 클래스의 하위 클래스인지를 검사할 수 있다. 대화식 셸에 케익 클래스들을 입력해 정의한 뒤, 아래와 같이 시험해 보자.

코드 8-45 issubclass() 함수로 하위 클래스 검사

>>> issubclass(ChocolateCake, Cake)  # ChocolateCake이 Cake의 하위 클래스인가?
True

>>> issubclass(IceCreamCake, Cake)   # IceCreamCake이 Cake의 하위 클래스인가?
True

>>> issubclass(Cake, Cake)           # Cake이 Cake의 하위 클래스인가?
True

>>> issubclass(int, Cake)            # int가 Cake의 하위 클래스인가?
False

ChocolateCakeIceCreamCakeCake의 하위 클래스이지만, intCake의 하위 클래스가 아니다. CakeCake의 하위클래스라는 점이 흥미롭다. 집합 이론에서 한 집합이 자신의 부분집합인 것과 같다.

모든 클래스는 object의 하위 클래스이므로 object를 대상으로 검사하면 항상 참이 반환된다.

코드 8-46 모든 클래스는 object의 하위 클래스다

>>> issubclass(object, object)        # object가 object의 하위 클래스인가?
True

>>> issubclass(int, object)           # int가 object의 하위 클래스인가?
True

>>> issubclass(Cake, object)          # Cake이 object의 하위 클래스인가?
True

>>> issubclass(IceCreamCake, object)  # IceCreamCake이 object의 하위 클래스인가?
True

>>> issubclass(FruitIceCreamCake, object)
True

issubclass(FruitIceCreamCake, object)의 실행 결과에서 알 수 있듯이, issubclass() 함수는 클래스가 다른 클래스를 직접 상속하지 않고 상위 클래스를 통해 간접적으로 상속하더라도 하위 클래스로 판단한다.

클래스 계층의 이름공간

하위 클래스는 상위 클래스의 이름공간을 공유하는 동시에, 자신만의 이름공간도 갖는다. 반면 상위 클래스는 하위 클래스의 이름공간에 접근하지 못한다. 다음 예를 보자.

코드 8-47 상위 클래스와 하위 클래스의 이름공간 비교

>>> Cake.radius = 20               # ❶ 상위 클래스에 속성을 추가한 뒤
>>> IceCreamCake.radius            #    하위 클래스에서 그 속성을 읽을 수 있다
20

>>> IceCreamCake.radius = 16       # ❷ 하위 클래스에서 속성을 재정의하면
>>> Cake.radius                    #    상위 클래스의 속성은 그대로 유지된 채
20

>>> IceCreamCake.radius            #    하위 클래스의 속성만 변경된다
16

>>> IceCreamCake.temperature = -1  # ❸ 클래스에 새 속성을 추가하면
>>> FruitIceCreamCake.temperature  #    하위 클래스에서는 읽을 수 있지만
-1

>>> Cake.temperature               #    상위 클래스에서는 읽을 수 없다
AttributeError: type object 'Cake' has no attribute 'temperature'

상위 클래스와 하위 클래스의 이름공간의 관계는 마치 클래스와 인스턴스의 이름공간의 관계와 유사하다. 클래스의 계층과 인스턴스에 따른 이름공간의 범위를 그림으로 보면 좀 더 이해하기 쉬울 것이다.

그림 8-5 상위 클래스, 하위 클래스, 인스턴스의 이름공간의 범위 (준비중)

개념 정리

  • issubclass() 함수로 어떤 클래스가 다른 클래스의 하위 클래스인지 확인할 수 있다.
  • 모든 클래스는 object 클래스의 하위 클래스다.
  • 하위 클래스에서 속성을 읽을 때, 먼저 자신의 이름공간에서 찾고 없으면 상위 클래스에서 찾는다.

8.4.3 다중 상속

다중 상속이란 클래스가 두 개 이상의 상위 클래스를 나란히 상속하는 것이다. 예를 들어, 초콜릿 클래스와 조각 케익 클래스를 각각 정의한 뒤에 이 둘을 나란히 상속하는 초콜릿 조각 케익 클래스를 정의하는 것이다. 단, 수직적 관계로 여러 개의 클래스를 상속하는 것을 다중 상속이라고 하지는 않는다.

그림 8-6 다중 상속 (준비중)

클래스가 여러 개의 상위 클래스를 상속하도록 하려면 괄호 안에 상속할 클래스의 이름을 콤마로 구분하여 나열하면 된다.

코드 8-49 치즈 조각 케익 클래스의 다중 상속

class CakePiece:
    """조각 케익"""
    size = 1 / 8
    calorie = 200

class CheeseCake(Cake):
    """치즈 케익"""
    body = '치즈'
    calorie = 1600

class CheeseCakePiece(CakePiece, CheeseCake):  # 상속할 클래스를 괄호 안에 나열한다
    """치즈 조각 케익"""
    pass  # 추가로 정의할 속성이 없으면 pass 문으로 비워 둔다

하위 클래스는 자신이 상속한 상위 클래스들의 속성을 읽을 수 있다. 이 때, 상속한 상위 클래스의 나열 순서에 의미가 있다. 하위 클래스에 속성이 없을 때는 상위 클래스의 이름공간에서 속성을 찾는다. 이 때 상위 클래스가 여러 개인 경우 왼쪽에 나열한 클래스의 이름공간을 먼저 검색한다. 다음 코드를 보자.

코드 8-50 다중 상속한 클래스의 이름공간 검색

print('body:', CheeseCakePiece.body)        # CheeseCake의 속성을 읽는다
print('size:', CheeseCakePiece.size)        # CakePiece의 속성을 읽는다
print('calorie:', CheeseCakePiece.calorie)  # CakePiece의 속성을 읽는다

실행 결과:

body: 치즈
size: 0.125
calorie: 200

calorie 속성은 CheeseCakePiece 클래스가 상속한 두 클래스 모두에 각각 다르게 정의되어 있다. 그렇지만 class CheeseCakePiece(CakePiece, CheeseCake): 에서 더 왼쪽에 나열된 CakePiece 클래스의 속성이 먼저 발견된다. 이처럼, 다중 상속한 클래스에서는 이름공간을 왼쪽 - 오른쪽 - 위쪽 순으로 검색한다.

그림 8-7 다중 상속한 클래스의 이름공간 검색 (준비중)

보다시피 다중 상속은 이름공간의 검색 순서가 복잡해 혼동을 일으킬 우려가 있다. 여기서 예로 든 것은 비교적 단순한 경우다. 대규모 프로젝트에서는 다중상속의 대상이 되는 상위 클래스도 다른 클래스를 다중상속하는 등 훨씬 복잡해질 수 있다. 이런 문제 때문에 다중 상속을 남용하는 것은 바람직하지 않다.

믹스인

클래스 중에는 멤버 변수 없이 모든 속성이 메서드로만 이루어진 특별한 클래스가 있는데, 믹스인(mixin)이라고 부른다. 믹스인은 다양한 클래스에서 공통적으로 사용할 수 있는 메서드들을 묶어둔 클래스다. 클래스를 정의할 때 필요한 믹스인을 다중 상속하여 섞어 넣어(mix in), 믹스인의 메서드를 이용할 수 있다. 믹스인에는 변화하는 상태가 존재하지 않고 오직 메서드만 있기 때문에 다중 상속을 하더라도 비교적 안전하다.

연습문제

연습문제 8-10 도형 클래스 정의하기 1

다음 요구사항을 참고해 도형(Shape), 삼각형(Triangle), 사각형(Rectangle)을 나타내는 클래스를 정의하여라.

  • Shape: 도형을 나타내는 클래스
    • 클래스 메서드 describe(): “이 도형은 3 개의 변을 갖고 있습니다.”와 같이 이 도형의 특징을 화면에 출력한다. 변의 개수는 self.sides 속성을 읽어 구한다.
  • Triangle: 삼각형을 나타내는 클래스
    • Shape 클래스를 상속
    • 클래스 속성 sides: 변의 개수를 나타내는 속성. 3으로 고정
  • Rectangle: 사각형을 나타내는 클래스
    • Shape 클래스를 상속
    • 클래스 속성 sides: 변의 개수를 나타내는 속성. 4로 고정

클래스를 정의한 후에는 프로그램 하단에 도형의 인스턴스를 만들어 특징을 출력하는 다음 코드를 삽입하여라.

shapes = [
    Triangle(),
    Rectangle(),
]
for shape in shapes:
    shape.describe()

프로그램을 실행한 결과는 다음과 같아야 한다.

이 도형은 3 개의 변을 갖고 있습니다.
이 도형은 4 개의 변을 갖고 있습니다.

연습문제 8-11 도형 클래스 정의하기 2

아래 사항을 참고해 연습문제 8-10에서 정의한 도형 클래스에 도형의 각 좌표 속성과 둘레 계산 메서드를 추가하여라. 이 때, Coordinate 클래스는 연습문제 8-9에서 정의한 것을 활용하도록 한다.

  • Triangle 클래스에 추가할 속성
    • 클래스 메서드 circumference(): 이 삼각형의 둘레를 계산하여 반환한다.
    • 인스턴스 속성 point_a: 꼭지점 A의 좌표. (Coordinate 유형)
    • 인스턴스 속성 point_b: 꼭지점 B의 좌표. (Coordinate 유형)
    • 인스턴스 속성 point_c: 꼭지점 C의 좌표. (Coordinate 유형)
  • Rectangle 클래스에 추가할 속성
    • 클래스 메서드 circumference(): 이 사각형의 둘레를 계산하여 반환한다.
    • 인스턴스 속성 point_a: 꼭지점 A의 좌표. (Coordinate 유형)
    • 인스턴스 속성 point_b: 꼭지점 B의 좌표. (Coordinate 유형)
    • 인스턴스 속성 point_c: 꼭지점 C의 좌표. (Coordinate 유형)
    • 인스턴스 속성 point_d: 꼭지점 D의 좌표. (Coordinate 유형)

클래스를 정의한 후에는 프로그램 하단에 도형의 인스턴스를 만들어 특징과 둘레를 출력하는 다음 코드를 삽입하여라.

shapes = [
    Triangle(Coordinate(0, 0), Coordinate(3, 0), Coordinate(3, 4)),
    Rectangle(Coordinate(2, 2), Coordinate(6, 2), Coordinate(6, 6), Coordinate(2, 6)),
]
for shape in shapes:
    shape.describe()
    print('둘레:', shape.circumference())

프로그램을 실행한 결과는 다음과 같아야 한다.

이 도형은 3 개의 변을 갖고 있습니다.
둘레: 12.0
이 도형은 4 개의 변을 갖고 있습니다.
둘레: 16.0

힌트: Coordinate 클래스의 거리 계산 메서드 distance()를 활용해라.

힌트: 인스턴스의 속성은 초기화 메서드 __init__()를 이용해 정의해라.