17. 상속#

클래스와 객체를 보다 실용적으로 활용하는 상속을 소개한다.

17.1. 상속이란?#

상속inheritance은 기존의 부모 클래스에서 선언된 속성(변수)과 기능(메서드)을 재활용 또는 수정하거나,필요한 속성과 기능을 추가해서 새로운 자식 클래스를 선언하여 객체를 보다 효율적으로 활용하는 OOP의 핵심 기법 중 하나다.

클래스 상속은 다음과 같이 정의한다.

class 자식 클래스(부모 클래스):
    본문

부모 클래스 예제

아래 클래스는 영화 어벤저스의 주인공 캐릭터 생성에 활용되는 클래스를 선언한다.

class Character:
    # 생성자
    def __init__(self, name, power, damage, inventory):
        self.name = name
        self.power = power
        self.damage = damage
        self.inventory = inventory
        
    # 자기소개
    def introduction(self):
        print(f"이름: {self.name}")
        print(f"파워: {self.power}")
        print(f"공격력: {self.damage}")
        print(f"수트 성능: {self.inventory['suit']}")
        print(f"무기: {self.inventory['weapon']}")
        
    # 파워 정보 확인
    def getPower(self):
        return self.power
    
    # 파워 업데이트
    def setPower(self, power):
        self.power = self.power + power
        
    # 상대 캐릭터 공격력
    def attack(self, other):
        print(f"{self.name}: {other.name} 공격!")
        # 공격력의 10% 만큼 상대 파워 감소시킴
        attackPower = self.damage * 0.1 
        other.setPower(-attackPower)

예를 들어 아이언맨과 데드풀 캐릭터 객체를 다음과 같이 생성한다.

ironman = Character('아이언맨', 100, 200, {'suit': 500, 'weapon': '레이저'})
deadpool = Character('데드풀', 300, 30, {'suit': 300, 'weapon': '장검'})

Character 메서드 활용

아이언맨이 자기를 소개할 때 introduction() 메서드를 활용한다.

ironman.introduction()
이름: 아이언맨
파워: 100
공격력: 200
수트 성능: 500
무기: 레이저

아이언맨 파워의 확인과 업데이트는 각각 getPower()setPower()를 이용한다.

print(ironman.getPower())
100
ironman.setPower(50)
print(ironman.getPower())
150

attack() 메서드는 적을 공격해서 적의 파워를 줄인다. 예를 들어, 파워가 100인 침입자가 출현했다고 가정한다.

intruder = Character('적군', 100, 50, {'suit': 100, 'weapon': '독'})

이제 데드풀이 침입자를 공격하니 파워가 3만큼 줄게 된다. 여기서 3은 데드풀의 공격력의 10분의 1을 가리킨다.

deadpool.attack(intruder)
intruder.getPower()
데드풀: 적군 공격!
97.0

게터와 세터

getPower()setPower() 같이 인스턴스의 속성값을 확인하거나 지정하는 메서드를 각각 게터getter 메서드와 세터setter 메서드라 부른다. 관용적으로 게터/세터 메서드명은 get/set 접두사를 붙인다.

자식 클래스 예제

아래 코드는 Character 클래스를 상속하면서 비행 능력이 추가된 FlyingCharacter를 자식 클래스로 선언한다.

class FlyingCharacter(Character):
    
    # fly 기능 추가
    def fly(self, speed):
        print(f"{self.name}: {speed} km/h로 날아요!!!")

속성과 메서드 상속

아이언맨에게 비행 능력을 주기위해 FlyingCharacter 클래스의 인스턴스로 새로 지정한다.

ironman = FlyingCharacter('아이언맨', 100, 200, {'suit': 500, 'weapon': '레이저'})

자식 클래스는 부모 클래스의 모든 것을 물려 받는다.

isinstance() 함수를 사용하여 ironman은 여전히 Character 클래스의 인스턴스임을 확인할 수 있다. 즉, 자식 클래스의 인스턴스는 모두 부모 클래스의 인스턴스이기도 하다.

isinstance(ironman, Character)
True

따라서 ironmanCharacter 클래스의 모든 메서드를 사용할 수 있다.

ironman.getPower()
100

아이언맨은 날 수도 있다.

ironman.fly(100)
아이언맨: 100 km/h로 날아요!!!

객체 교감

헐크 캐릭터는 아예 비행 능력이 없으므로 Character 클래스의 인스턴스로 선언한다.

hulk = Character('헐크', 400, 300, {'suit': 0, 'weapon': '주먹'})

앞서 언급한 대로 ironman 역시 Character 클래스의 기능을 공유한다. 따라서, 예를 들어, ironmanhulk가 서로 공격할 수 있다. 즉, 두 객체가 서로 교감한다.

ironman.attack(hulk)
아이언맨: 헐크 공격!
hulk.attack(ironman)
헐크: 아이언맨 공격!

부모 클래스의 인스턴스는 자식 클래스의 기능을 갖지 못한다!

반면에 헐크는 비행 능력이 없으며 fly 메서드를 아예 모른다. 아래 코드는 오류발생을 피하기 위해 예외처리를 함께 사용한다.

try: 
    hulk.fly(100)
except AttributeError: 
    print("헐크: 전 날지 못해요!")
헐크: 전 날지 못해요!

17.2. 메서드 오버라이딩#

자식 클래스의 생성자 메서드를 포함하여 부모 클래스로부터 상속받은 모든 메서드를 수정하여 부모와는 다른 방식으로 인스턴스를 생성하고 다른 기능을 수행할 수 있다. 이처럼 부모 클래스의 메서드를 재정의 하면서 상속하는 방식을 메서드 오버라이딩method overriding이라 부른다.

자식 클래스의 생성자

모든 클래스에는 생성자 메서드인 init() 메서드가 포함되어야 한다. FlyingCharacter 클래스에는 부모 클래스인 Character 클래스의 init() 메서드를 포함해서 모든 메서드를 상속받는다.

FlyingCharacter의 인스턴스를 생성할 때 Character 클래스의 인스턴스를 생성하는 것처럼 네 개의 인자를 사용한 이유가 여기에 있다. 실제로 FlyingCharacter 클래스의 생성자 메서드를 아래와 같이 선언하는 것과 동일하게 작동한다.

def __init__(self, name, power, damage, inventory):
    super().__init__(name, power, damage, inventory)

위 코드에서 super()는 부모 클래스인 Character 클래스를 가리킨다. 따라서 super().__init__()는 부모 클래스의 생성자 호출을 의미한다.

생성자 오버라이딩

예를 들어, 아래 FlyingCharacter 클래스는 영웅 캐릭터의 suit 성능에 따라 비행 속도를 다르게 지정한다. 이를 위해 생성자를 새롭게 정의한다

  • hero(영웅) 여부 추가

  • 영웅 캐릭터 여부와 self.inventory[suit] 값의 크기에 따라 speedUp 속성을 다르게 설정

  • 영웅 여부에 따라 정해진 speedUp 속성을 fly() 메서드에서 활용

class FlyingCharacter(Character):
    # 생성자: hero 매개변수 추가
    def __init__(self, name, power, damage, inventory, hero=True):
        super().__init__(name, power, damage, inventory)
        
        self.hero = hero
        
        # suit의 성능이 400 이상인 경우 두 배 속력으로 비행
        if self.hero and self.inventory['suit'] >= 400:
            self.speedUp = 2
        else:
            self.speedUp = 1
    
    # 영웅 캐릭터: 경우 지정 속도보다 두 배 빠르게!
    def fly(self, speed):
        print(f"{self.name}: {speed*self.speedUp} km/h로 날아요!!!")
        
    # attack 메서드 재정의
    # 상대 캐릭터 공격력
    def attack(self, other):
        print(f"{self.name}{other.name} 공격!")
        
        # 영웅이 악당 공격할 때 타격효과 두 배
        if self.hero:
            attackPower = self.damage * 0.2
        else:
            attackPower = self.damage * 0.1 
        
        other.setPower(-attackPower)        

아래 코드는 아이언맨과 울트론을 생성한 후에 시속 100으로 날으라고 할 때 두 캐릭터의 속도가 2배 차이나는 것을 보여준다.

ironman = FlyingCharacter('아이언맨', 100, 200, {'suit': 500, 'weapon': '레이저'})
ultron = FlyingCharacter('울트론', 400, 300, {'suit': 300, 'weapon': '플라즈마 빔'}, hero=False)

ironman.attack(ultron)
ultron.attack(ironman)
# 비행속도를 100으로 지정하면 아이언맨은 200으로 날아 오름.
ironman.fly(100)
ultron.fly(100)
아이언맨의 울트론 공격!
울트론의 아이언맨 공격!
아이언맨: 200 km/h로 날아요!!!
울트론: 100 km/h로 날아요!!!

attack() 메서드 오버라이딩

Character 클래스의 attack 메서드는 공격자의 damage 공격력의 10%만큼 상대방 파워를 감소시켰다. 반면에 FlyingChracter에서 영웅 캐릭터는damage 공격력의 10%가 아닌 20%만큼 상대방의 파워를 감소시킨다. 따라서 아이언맨과 울트론이 서로 공격할 때 각 캐릭터의 파워가 다르게 줄어든다.

print('아이언맨 파워:', ironman.getPower())
print('울트론 파워:', ultron.getPower())

# 10% 타격
ironman.attack(ultron)                      
print('울트론 파워:', ultron.getPower())

# 20% 타격
ultron.attack(ironman)
print('아이언맨 파워:', ironman.getPower())
아이언맨 파워: 70.0
울트론 파워: 360.0
아이언맨의 울트론 공격!
울트론 파워: 320.0
울트론의 아이언맨 공격!
아이언맨 파워: 40.0

17.3. 예제: 스프라이트 클래스 상속#

감사의 글

아래 내용은 (유튜브) Python Game Coding: Introduction to Collision Detection의 내용을 참고합니다.

준비사항

여기서 사용하는 코드는 (레플릿) 충돌 감지: 상속에서 확인하고 직접 실행할 수 있다. 오프라인 환경에서 실행하려면 먼저 아래 코드의 실행에 필요한 이미지 파일과 소스코드를 다운로드해서 압축을 푼 다음에 inheritance.py 파일을 실행하면 된다.

16장에서 정의한 Sprite 클래스를 상속해서 일부 캐릭터에 점프 기능을 추가한다.

스프라이트 클래스

16장에서 정의한 Sprite 클래스는 다음과 같으며, 여기서는 sprite.py 파일에 저장되어 있다고 가정한다. 저장된 파일을 하나의 모듈로 불러와서 Sprite 클래스를 활용한다.

class Sprite():

    ## 생성자: 스프라이트의 위치, 가로/세로 크기, 이미지 지정
    def __init__(self, x, y, width, height, image):
        """
        x: x-좌표
        y: y-좌표
        width: 가로 크기
        height: 세로 크기
        image: 모양
        """

        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.image = image

    ## 인스턴스 메서드

    # 지정된 위치로 스프라이트 이동 후 도장 찍기
    def render(self, pen):
        pen.goto(self.x, self.y)
        pen.shape(self.image)
        pen.stamp()

    # 충돌 감지 방법 1: 두 객체의 중심이 일치할 때
    def is_overlapping_collision(self, other):
        if self.x == other.x and self.y == other.y:
            return True
        else:
            return False

    # 충돌 감지 방법 2: 두 객체 사이의 거리가 두 객체 너비의 평균값 보다 작을 때
    def is_distance_collision(self, other):
        distance = (((self.x - other.x)**2) + ((self.y - other.y)**2))**0.5
        if distance < (self.width + other.width) / 2.0:
            return True
        else:
            return False

    # 충돌 감지 방법 3: 객체를 둘러썬 경계상자가 겹칠 때
    # aabb: Axis Aligned Bounding Box
    def is_aabb_collision(self, other):
        x_collision = (math.fabs(self.x - other.x) * 2) < (self.width +
                                                           other.width)
        y_collision = (math.fabs(self.y - other.y) * 2) < (self.height +
                                                           other.height)
        return (x_collision and y_collision)

스프라이트 클래스 상속

Sprite 클래스를 상속하면서 Character 클래스를 정의한다. 생성자 함수가 조금 달라지며, 객체의 점프 기능을 담당하는 hop() 메서드가 추가된다. 하지만 hop() 함수를 실행했을 때 점프 기능이 지정된 경우에만 작동하도록 설정한다.

class Character(Sprite):
    def __init__(self, x, y, width, height, image, jump=True):
        super().__init__(x, y, width, height, image)
        self.jump = jump

    # 점프 기능 추가
    def hop(self):
        if self.jump == True:
            self.x += 100

스프라이트 객체 생성

부모 클래스와 자식 클래스 모두를 이용하여 객체를 생성할 수 있다. wizardpacmanCharacter 클래스의 인스턴스로, 나머지는 Sprite 클래스의 인스턴스로 선언된다. 또한 pacman 객체는 점프 기능도 갖는다. wizard 객체는 점프 기능을 제대로 갖추지 못했기에 Sprite 클래스의 인스턴스와 동일한 기능만 수행한다.

## 스프라이트 생성
wizard = Character(-128, 200, 128, 128, "wizard.gif", jump=False)
goblin = Sprite(128, 200, 108, 128, "goblin.gif")

pacman = Character(-128, 0, 128, 128, "pacman.gif")
cherry = Sprite(128, 0, 128, 128, "cherry.gif")

bar = Sprite(0, -400, 128, 24, "bar.gif")
ball = Sprite(0, -200, 32, 32, "ball.gif")

# 스프라이트 리스트
sprites = [wizard, goblin, cherry, pacman, bar, ball]

참고로 파이썬 튜터: 상속에서 상속된 클래스의 생성자가 객체를 생성하는 과정을 살펴볼 수 있다.

이벤트, 이벤트 처리, 콜백

팩맨의 점프 기능을 담당하는 콜백 함수와 이벤트 처리 기능이 추가된다.

# 이벤트 처리: 스페이키 담당
turtle.onkey(jump, "space")  # 스페이스 키 입력
# 콜백: 점프 가능한 경우 활용
def jump():
    try:
        sprites[sprite_idx].hop()
    except AttributeError:
        pass

게임 실행

게임 기본 세팅과 실행 코드는 16장과 동일하다.

17.4. 필수 예제#

참고: (필수 예제) 상속

17.5. 연습문제#

참고: (실습) 상속