2.12 파이썬 객체지향 프로그래밍

객체

프로그램이 어떤 작업을 수행하기 위해서는 (1)데이터와 (2)데이터를 조작하는 행위, 두 가지 요소가 필요하다. 일반적으로 데이터는 변수에 넣어서 사용하고 데이터를 조작하는 일은 함수로 구성해서 쉽게 실행할 수 있도록 만들어 놓는다.

객체(object, instance)는 서로 연관된 데이터와 그 데이터를 조작하기 위한 함수를 하나의 집합에 모아놓은 것을 말한다. 이 때 집합의 원소가 되는 변수나 함수는 멤버(member) 또는 속성(attribute)이라고 한다. 특히 객체의 속성인 함수는 메서드(method)라고 부른다.

예를 들어 사각형의 면적을 구하는 프로그램을 만든다고 하자. 필요한 변수와 함수는 다음과 같다.

  1. 가로 길이와 세로 길이라는 두 개의 데이터를 넣을 변수

  2. 두 길이를 곱해서 면적을 구하는 함수

객체지향을 사용하지 않고 파이썬으로 구현하면 다음과 같다.

h = 10
v = 20


def area(h, v):
    return h * v


a = area(h, v)
print(a)
200

위 프로그램에서 사각형의 가로 길이를 나타내는 변수 h, 사각형의 세로 길이를 나타내는 변수 v, 그리고 이 사각형의 면적을 계산하는 함수 area는 제각기 떨어져 있다. 하지만 객체지향 프로그래밍에서는 이 세가지를 하나의 객체(object)로 묶을 수 있다.

다음은 이 프로그램을 객체지향 방식으로 다시 구현한 것이다.

class Rectangle(object):

    def __init__(self, h, v):
        self.h = h
        self.v = v

    def area(self):
        return self.h * self.v

이 부분은 클래스(class) 구현이라고 한다. 실제로 길이 변수들을 저장하고 면적을 계산하는 코드는 다음과 같다.

r = Rectangle(10, 20)
a = r.area()
print(a)
200

위 프로그램에서 r이 바로 객체이다. 어떻게 위와 같은 코드를 만드는지에 대해서는 곧 자세히 설명할 것이다.

객체 r은 사각형의 가로 길이와 세로 길이를 나타내는 변수 hv 그리고 면적을 계산하는 함수 area()가 합쳐져서 만들어진 것이다. 객체 r에 포함된 이 변수들과 함수, 즉 속성을 꺼내려면 객체 이름 뒤에 점(.)을 붙인 다음 속성 이름을 입력하면 된다. 다음과 같이 입력해 보면 알 수 있다.

r.h
10
r.v
20
r.area()
200

클래스

객체지향 프로그래밍에서 객체를 만들려면 객체를 바로 만들지 못하고 항상 클래스(class)라는 것을 만든 후에 그 클래스를 이용하여 객체를 만들어야 한다.

위 예제에서 Rectangle은 클래스이고 rRectangle 클래스로 만들어진 객체이다. 객체와 클래스의 관계는 “붕어빵”과 “붕어빵을 굽는 틀”에 비유할 수 있다. 즉, 정해진 속성, 여기에서는 가로 길이 h와 세로 길이 v라는 속성을 가지도록 사각형 클래스를 한 번 만들어 놓으면 이 속성을 가지는 실제 사각형은 얼마든지 많이 만들 수 있다.

예를 들어 위에서 만들어 놓은 Rectangle 클래스로 다음과 같이 5개의 사각형을 만들 수도 있다.

a = Rectangle(1, 1)   # 가로 1, 세로 1인 사각형
b = Rectangle(2, 1)   # 가로 2, 세로 1인 사각형
c = Rectangle(4, 2)   # 가로 4, 세로 2인 사각형
d = Rectangle(6, 3)   # 가로 6, 세로 3인 사각형
e = Rectangle(8, 5)   # 가로 8, 세로 5인 사각형

이 사각형들의 면적은 다음과 같이 계산한다.

print(a.area())
print(b.area())
print(c.area())
print(d.area())
print(e.area())
1
2
8
18
40

생성자

파이썬에서 클래스를 정의하는 문법은 다음과 같다.

class 클래스이름(object):
    
    def __init__(self, 속성값1, 속성값2, 속성값3):
        self.속성이름1 = 속성값1
        self.속성이름2 = 속성값2
        self.속성이름3 = 속성값3

이때 속성값 인수는 필요하지 않다면 없어도 된다. 여기에서 class 블럭 안에 정의된 __init__란 함수는 생성자(constructor)라고 하며 클래스 정의에서 가장 중요한 함수이다.

객체를 생성할 때는 클래스이름을 함수처럼 호출해야 하는데, 이때 실제로는 __init__로 정의된 생성자 함수가 호출된다. 생성자 함수 내부에서는 생성자를 호출할 때 넣은 입력 변수, 즉 인자의 값을 속성값으로 저장한다.

연습 문제 2.12.1

삼각형의 넓이를 계산하기 위한 클래스를 만든다. 이 클래스는 다음과 같은 속성을 가진다.

  • 밑변의 길이 b 와 높이 h

  • 삼각형의 넓이를 계산하는 메서드 area

연습 문제 2.12.2

사각 기둥의 부피를 계산하기 위한 클래스를 만든다. 이 클래스는 다음과 같은 속성을 가진다.

  • 밑면의 가로 길이 a, 밑면의 세로 길이 b, 높이 h

  • 부피를 계산하는 메서드 volume

  • 겉넓이를 계산하는 메서드 surface

게임 캐릭터와 객체

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

  • 플레이어의 캐릭터

  • 속성: 캐릭터의 능력치, 경험치 등

  • 메서드: 캐릭터를 움직이는 방법, 이동, 공격 등

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

class Character(object):

    def __init__(self):
        self.life = 1000

    def attacked(self):
        self.life -= 10
        print("공격받음! 생명력 =", self.life)

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

a = Character()
b = Character()
c = Character()

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

a.life, b.life, c.life
(1000, 1000, 1000)

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

a.attacked()
공격받음! 생명력 = 990
b.attacked()
공격받음! 생명력 = 990
a.attacked()
a.attacked()
a.attacked()
a.attacked()
a.attacked()
공격받음! 생명력 = 980
공격받음! 생명력 = 970
공격받음! 생명력 = 960
공격받음! 생명력 = 950
공격받음! 생명력 = 940
a.life, b.life, c.life
(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라는 속성값을 초기화하므로 자식 클래스에서는 이 속성값을 초기화해줄 필요가 없다.

class Warrior(Character):

    def __init__(self):
        super(Warrior, self).__init__()
        self.strength = 15
        self.intelligence = 5
class Wizard(Character):

    def __init__(self):
        super(Wizard, self).__init__()
        self.strength = 5
        self.intelligence = 15

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

a = Warrior()
b = Wizard()
a.life, b.life
(1000, 1000)
a.strength, b.strength
(15, 5)
a.intelligence, b.intelligence
(5, 15)
a.attacked()
공격받음! 생명력 = 990
b.attacked()
공격받음! 생명력 = 990

메서드 오버라이딩

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

class Character(object):

    def __init__(self):
        self.life = 1000
        self.strength = 10
        self.intelligence = 10

    def attacked(self):
        self.life -= 10
        print("공격받음! 생명력 =", self.life)

    def attack(self):
        print("공격!")
class Warrior(Character):

    def __init__(self):
        super(Warrior, self).__init__()
        self.strength = 15
        self.intelligence = 5

    def attack(self):
        print("육탄 공격!")
class Wizard(Character):

    def __init__(self):
        super(Wizard, self).__init__()
        self.strength = 5
        self.intelligence = 15

    def attack(self):
        print("마법 공격!")
a = Character()
b = Warrior()
c = Wizard()
a.attack()
공격!
b.attack()
육탄 공격!
c.attack()
마법 공격!
a.attacked()
공격받음! 생명력 = 990
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) 까지의 길이

연습 문제 2.12.3

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

연습 문제 2.12.4

다음과 같이 자동차를 나타내는 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 미만으로 감소할 수도 없다.

  • 메서드는 호출시 속도 정보를 출력하고 명시적인 반환값을 가지지 않는다.

  • 위 기능이 모두 정상적으로 구현되었음을 보이는 코드를 추가한다.

연습 문제 2.12.5

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

  • SportCar 클래스는 max_speed 속성이 200 이고 speed_up, speed_down 호출시 속도가 45씩 증가 혹은 감소한다.

  • Truck 클래스는 max_speed 속성이 100 이고 speed_up, speed_down 호출시 속도가 15씩 증가 혹은 감소한다.

  • 스피드 속성 speed의 값은 max_speed 속성 값을 넘을 수 없다. 또 0 미만으로 감소할 수도 없다.

  • 메서드는 호출시 속도 정보를 출력하고 명시적인 반환값을 가지지 않는다.

  • 위 기능이 모두 정상적으로 구현되었음을 보이는 코드를 추가한다.

특수 메서드

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

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

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

class Complex(object):

    def __init__(self, realpart, imagpart):
        self.r = realpart
        self.i = imagpart

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

c = Complex(1, 2)
c
<__main__.Complex at 0x7fef01144160>
str(c)
'<__main__.Complex object at 0x7fef01144160>'

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

class Complex2(Complex):

    def __repr__(self):
        return "Complex: real = %f imag = %f" % (self.r, self.i)

    def __str__(self):
        return "[for str] " + self.__repr__()
c2 = Complex2(1, 1)
c2
Complex: real = 1.000000 imag = 1.000000
str(c2)
'[for str] Complex: real = 1.000000 imag = 1.000000'

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

class Complex3(Complex2):

    def __getitem__(self, key):
        if key == "r":
            return self.r
        if key == "i":
            return self.i
c3 = Complex3(1, 2)
c3
Complex: real = 1.000000 imag = 2.000000
c3["i"]
2

연습 문제 2.12.6

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

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