파이썬 객체지향 프로그래밍 기초 2

이번에는 컴퓨터 게임의 캐릭터를 만드는 예제를 통해 상속(Inheritance)의 개념을 공부한다.

게임 캐릭터와 객체

컴퓨터 게임에 사용되는 플레이어의 캐릭터는 객체 지향 프로그램을 통해 만든다고 생각해 보자. 캐릭터의 능력치, 경험치 등의 숫자는 캐릭터마다 다르게 관리되어야 하므로 객체의 속성이 될 수 있다. 또한 모든 캐릭터 조작에 공통적으로 필요한 이동, 공격 등의 조작은 메서드로 구현할 수 있을 것이다.

  • 플레이어의 캐릭터
    • 속성: 캐릭터의 능력치, 경험치 등
    • 메서드: 캐릭터를 움직이는 방법, 이동, 공격 등

이를 기반으로 캐릭터를 만들어내는 Character라는 클래스를 만든다. 이 클래스로 만든 캐릭터는 1000 이라는 life 속성값을 가지고 생성되며 게임상에서 공격받을 경우에는 attacked라는 메서드가 호출되어 life 속성값을 10만큼 감속시키고 공격 받았음을 표시한다.


In [1]:
class Character(object):
    
    def __init__(self):
        self.life = 1000
    
    def attacked(self):
        self.life -= 10
        print(u"공격받음! 생명력 =", self.life)

이 클래스로 a, b, c 세 개의 캐릭터 객체를 생성한다.


In [2]:
a = Character()
b = Character()
c = Character()

모든 객체의 초기 life 속성값은 모두 1000이다.


In [3]:
a.life, b.life, c.life


Out[3]:
(1000, 1000, 1000)

하지만 공격을 받은 캐릭터의 생명력은 감소된다.


In [4]:
a.attacked()


공격받음! 생명력 = 990

In [5]:
b.attacked()


공격받음! 생명력 = 990

In [6]:
a.attacked()
a.attacked()
a.attacked()
a.attacked()
a.attacked()


공격받음! 생명력 = 980
공격받음! 생명력 = 970
공격받음! 생명력 = 960
공격받음! 생명력 = 950
공격받음! 생명력 = 940

In [7]:
a.life, b.life, c.life


Out[7]:
(940, 990, 1000)

클래스 상속

이제 클래스 상속(class inheritance)이라는 개념을 생각한다. 위에서 만들어 본 클래스는 모든 캐릭터에 공통적인 life 속성만을 가지고 있었다. 하지만 만약 캐릭터도 전사(Warrior), 마법사(Wizard) 등 다양한 직업을 가진 캐릭터가 있고 각 캐릭터들은 서로 다른 초기 속성값을 가지고 태어난다면 어떻게 프로그램해야 할까? 각각의 직업 캐릭터를 별도의 클래스로 만들어도 되겠지만 클래스 상속을 사용하면 이미 만들어진 클래스 코드를 재사용하여 다른 클래스를 생성할 수 있다. 즉, 상속 과정에서 공통적으로 사용하는 속성이나 메서드는 두 번 반복해서 코딩할 필요가 없다. 이 때 상속을 받는 클래스를 자식 클래스(child class), 상속의 대상이 되는 클래스를 부모 클래스(parent class)라고 한다.

Character 부모 클래스에서 상속을 통해 Warrior 라는 자식 클래스와 Wizard 라는 자식 클래스를 만든다 상속을 위한 파이썬 문법은 다음과 같다.

class 자식클래스이름(부모클래스이름):

    def __init__(self, 속성값1, 속성값2):
        super(자식클래스이름, self).__init()
        자식 클래스의 초기화 코드

사실 우리가 지금까지 쓰던 클래스 정의를 살펴보면 object라는 부모 클래스에서 상속을 받는 것이었다.

이 코드에서 super(자식클래스이름, self).__init() 부분은 부모 클래스의 초기화 생성자를 호출하는 부분이다. 예를 들어 Warrior 라는 클래스에서 부모 클래스인 Character 클래스의 생성자를 호출하면 life라는 속성값을 초기화하므로 자식 클래스에서는 이 속성값을 초기화해줄 필요가 없다.


In [8]:
class Warrior(Character):
    
    def __init__(self):
        super(Warrior, self).__init__()
        self.strength = 15
        self.intelligence = 5

In [9]:
class Wizard(Character):
    
    def __init__(self):
        super(Wizard, self).__init__()
        self.strength = 5
        self.intelligence = 15

이 클래스의 객체를 만들어 보면 명시적으로 만들지 않았지만 life라는 속성과 attacked 라는 메서드를 가진다.


In [10]:
a = Warrior()
b = Wizard()

In [11]:
a.life, b.life


Out[11]:
(1000, 1000)

In [12]:
a.strength, b.strength


Out[12]:
(15, 5)

In [13]:
a.intelligence, b.intelligence


Out[13]:
(5, 15)

In [14]:
a.attacked()


공격받음! 생명력 = 990

In [15]:
b.attacked()


공격받음! 생명력 = 990

메소드 오버라이딩

메소드 오버라이딩(Method Overriding) 여러 클래스에 걸쳐서 같은 이름의 메서드를 만드는 것이다. 예를 들어 부모 클래스, 전사 캐릭터 클래스, 마법사 캐릭터 클래스에 공통적으로 attack 이라는 메서드가 있지만 각각의 하는 일이 다른 경우에는 다음과 같이 같은 이름의 메서드를 클래스 별로 구현하면 된다. 이렇게 되면 부모 클래스에서 만든 메서드 정의를 자식 클래스에서는 변경해서 사용한다.


In [16]:
class Character(object):
    
    def __init__(self):
        self.life = 1000
        self.strength = 10
        self.intelligence = 10
        
    def attacked(self):
        self.life -= 10
        print(u"공격받음! 생명력 =", self.life) 
        
    def attack(self):
        print(u"공격!")

In [17]:
class Warrior(Character):
    
    def __init__(self):
        super(Warrior, self).__init__()
        self.strength = 15
        self.intelligence = 5
        
    def attack(self):
        print(u"육탄 공격!")

In [18]:
class Wizard(Character):
    
    def __init__(self):
        super(Wizard, self).__init__()
        self.strength = 5
        self.intelligence = 15
        
    def attack(self):
        print(u"마법 공격!")

In [19]:
a = Character()
b = Warrior()
c = Wizard()

In [20]:
a.attack()


공격!

In [21]:
b.attack()


육탄 공격!

In [22]:
c.attack()


마법 공격!

In [23]:
a.attacked()


공격받음! 생명력 = 990

In [24]:
b.attacked()


공격받음! 생명력 = 990

참조: 오버로딩 Overloading

이와 비슷한 이름으로 오버로딩(Overloading)이라는 것이 있는데 이는 전혀 다른 개념이다. 오버로딩은 같은 메서드가 인수의 자료형이나 갯수를 다르게 받을 수 있는 것을 말한다. C++, Java 등에서는 지원하지만 파이썬에서는 오버로딩을 지원하지 않으므로 프로그래머가 내부적으로 알아서 처리해야 한다

다음은 C++에서 오버로딩을 지원하는 함수 선언의 예이다.

float length(list p1, list p2);                  // 점 (p1[0], p1[1]) - (p1[0], p1[1]) 까지의 길이
float length(int x1, int y1, int x2, int y2);    // 점 (x1, y1) - (x2, y2) 까지의 길이

연습 문제 1

위의 게임 캐릭터 코드에서 attacked 메서드도 오버라이딩을 하여 전사와 마법사가 공격을 받을 때 life 속성값이 다르게 감소하도록 한다.

연습 문제 2

다음과 같이 자동차를 나타내는 Car 클래스를 구현한다.

  • 이 클래스는 최고 속도를 의미하는 max_speed라는 속성과 현재 속도를 나타내는 speed라는 속성을 가진다.
  • 객체 생성시 max_speed 속성은 160이 되고 speed 속성은 0이 된다.
  • speed_up, speed_down이라는 메서드를 가진다. speed_up을 호출하면 speed 속성이 20씩 증가하고 speed_down을 호출하면 speed 속성이 20씩 감소한다.
  • 스피드 속성 speed의 값은 max_speed 속성 값, 즉 160을 넘을 수 없다. 또 0 미만으로 감소할 수도 없다.

연습 문제 3

Car 클래스를 기반으로 SportCarTruck이라는 두 개의 자식 클래스를 구현한다.

  • SportCar 클래스는 max_speed 속성이 200 이고 speed_up, speed_down 호출시 속도가 40씩 증가 혹은 감소한다.
  • Truck 클래스는 max_speed 속성이 100 이고 speed_up, speed_down 호출시 속도가 10씩 증가 혹은 감소한다.
  • 스피드 속성 speed의 값은 max_speed 속성 값, 즉 160을 넘을 수 없다. 또 0 미만으로 감소할 수도 없다.

pass

파이썬은 들여쓰기를 통해 코드 블럭이 존재함을 알려주어야 한다. 그런데 내용이 없는 클래스나 함수의 경우에는 아무것도 쓰지 않는다면 코드 블럭의 존재 자체도 알려주기 못하기 때문에 pass라는 아무 의미 없는 명령을 사용하여 들여쓰기된 코드 블럭을 표시한다.


In [25]:
def Dummy():
    pass

d = Dummy()

특수 메서드

파이썬에는 특수 메사드(Special Methods)라는 것이 존재한다. 메서드 이름의 앞과 뒤에 두 개의 밑줄(underscore)이 붙어있는 메서드이다. 이 메서드들은 특수한 용도에 사용하는 것이다

예를 들어 파이선 셸에서 변수 이름을 치고 엔터 키(주피터 노트북의 경우에는 시프트 + 엔트)를 치면 변수의 값이 호출되는데 사실 이것은 해당 변수가 가지는 __repr__이라는 메서드가 호출되는 것이다. repr은 representation의 약자이다. 또 변수를 str이라는 함수에 넣으면 변수를 문자열로 변환해 주는데 이것도 사실은 __str__이라는 메서드가 호출되는 것이다.

예를 들어 다음과 같이 복소수에 대한 클래스인 Complex 클래스를 만든다. r이라는 속성에 실수부를, i이라는 속성에 실수부를 넣는다.


In [26]:
class Complex(object):
    
    def __init__(self, realpart, imagpart):
        self.r = realpart
        self.i = imagpart

__repr__ 메서드를 정의하지 않으면 object 클래스가 가진 기본 __repr__ 메서드를 사용한다. 이 함수는 클래스 이름과 변수가 위치하고 있는 메모리 주소를 <>안에 써서 반환한다. 기본 __str__ 메서드도 마찬가지이다.


In [27]:
c = Complex(1, 2)
c


Out[27]:
<__main__.Complex at 0x7f7273c8d450>

In [28]:
str(c)


Out[28]:
'<__main__.Complex object at 0x7f7273c8d450>'

이번에는 __repr__ 메서드와 __repr__ 메서드를 다음과 같이 새로 정의하여 오버라이딩한다.


In [29]:
class Complex2(Complex):
    
    def __repr__(self):
        return "Complex: real = %f imag = %f" % (self.r, self.i)
    
    def __str__(self):
        return "[for str] " + self.__repr__()

In [30]:
c2 = Complex2(1, 1)
c2


Out[30]:
Complex: real = 1.000000 imag = 1.000000

In [31]:
str(c2)


Out[31]:
'[for str] Complex: real = 1.000000 imag = 1.000000'

__getitem__ 메서드를 정의하면 마치 리스트나 사전처럼 [] 기호를 사용한 인덱싱을 할 수 있다.


In [32]:
class Complex3(Complex2):
    
    def __getitem__(self, key):
        if key == "r":
            return self.r
        if key == "i":
            return self.i

In [33]:
c3 = Complex3(1, 2)
c3


Out[33]:
Complex: real = 1.000000 imag = 2.000000

In [34]:
c3["i"]


Out[34]:
2

연습 문제 4

학생의 학번, 이름, 수학 성적, 영어 성적을 저장할 수 있는 클래스를 만들고 평균 성적을 출력하는 메서드를 추가한다. 셀에서 이 클래스 객체의 이름을 치면 바로 이름과 학번이 나와야 하고 str 명령을 수행하면 이름이 나와야 한다. 다음과 같이 수학과 영어 성적을 읽을 수 있어야 한다.

obj["math"], obj["english"]