Program/Python

12장. 객체 지향 프로그래밍

Hue Kim 2016. 5. 29. 23:04

12장. 객체 지향 프로그래밍


지금까지 프로그램을 작성할 때, 우리는 데이터를 다루는 명령들의 블록인 함수들의 조합으로 프로그램을 구성하였습니다. 이러한 설계 방식을 절차 지향 프로그래밍 기법이라고 부릅니다. 이와 달리 데이터와 기능을 객체라고 불리우는 것으로 묶어서 프로그램을 구성하는 또 다른 기법이 있습니다. 이것을 객체 지향 프로그래밍 기법이라고 부릅니다. 아마도 여러분의 대부분의 시간 동안 절차 지향 프로그래밍 기법을 통해 프로그램을 작성하게 되겠지만, 큰 프로그램을 작성할 때나 이 기법을 이용하는 것이 더 편리한 문제를 해결해야 할 경우 객체 지향 프로그래밍 기법을 활용할 수 있습니다.


객체 지향 프로그래밍에서는 클래스와 객체라는 두 가지 주인공이 있습니다. 클래스*는 새로운 형식을 정의하는 것이며, *객체*는 클래스의 *인스턴스 를 의미하는 것입니다. 이것을 다시 표현하면 여러분이 int 라는 형식의 변수를 만들 수 있다는 것으로, 이것은 곧 정수형을 저장하는 변수는 int 클래스의 인스턴스(객체)를 변수에 할당하는 것이라고도 말할 수 있습니다.


정적 언어 프로그래머들을 위한 주석

파이썬에서는 정수형조차도 객체로 다루어집니다 ( int 클래스의 객체입니다). C++이나 Java (버전 1.5 미만)처럼 정수형이 자체 기본 형식들 중 하나로 다루어지는 것과는 다릅니다.


help(int) 를 입력하여 정수형 클래스에 대해 좀 더 자세히 알아보시기 바랍니다.


C# 이나 Java 1.5 프로그래머들은 아마 이것이 boxing 과 unboxing 과 비슷하다는 것을 눈치채셨을 것입니다.


C# 이나 Java 1.5 프로그래머들은 아마 이것이 boxing 과 unboxing 과 비슷하다는 것을 눈치채셨을 것입니다.

객체는 그 객체에 내장된 일반적인 변수들을 사용하여 데이터를 저장할 수 있습니다. 이 때 객체 혹은 클래스에 소속된 변수들을 필드(field) 라고 부릅니다. 객체는 또한 내장된 함수를 이용하여 어떤 기능을 갖도록 할 수 있는데 이것을 클래스의 메소드(method) 라고 부릅니다. 이러한 명칭을 구별하여 부르는 것은 중요한데, 이는 일반적인 변수와 함수와 달리 이들은 클래스나 객체에 소속되어 있는 대상들이기 때문입니다. 또, 이러한 필드와 메소드들을 통틀어 클래스의 속성(attribute) 이라 부릅니다.


필드는 두 가지 종류가 있습니다. 하나는 클래스의 인스턴스/객체에 내장되어 있는 것이고, 또 하나는 클래스 자체에 내장되어 있는 것입니다. 각각을 인스턴스 변수클래스 변수 라 부릅니다.


클래스는 class 키워드를 통해 생성됩니다. 클래스의 필드와 메소드는 그 아래 들여쓰기 된 블록에 차례로 정의됩니다.


12.1. self 에 대하여


클래스 메소드는 일반적인 함수와 딱 한 가지 다른 점이 있는데, 그것은 메소드의 경우 매개 변수의 목록에 항상 추가로 한 개의 변수가 맨 앞에 추가되어야 한다는 점입니다. 또한 메소드를 호출할 때 이 변수에는 우리가 직접 값을 넘겨주지 않으며, 대신 파이썬이 자동으로 값을 할당합니다.이 변수에는 현재 객체 자신의 참조가 할당되며, 일반적으로 self 라 이름을 짓습니다.


이 변수의 이름은 마음대로 지을 수 있지만, self 라는 이름을 사용할 것을 강력히 권합니다. 이것은 일종의 약속이며, 다른 이름을 사용하는 것은 다른 프로그래머들에게 눈살을 찌푸려지게 하는일이 될 수 있기 때문입니다. `self`라는 표준적인 이름을 사용하면 여러분의 프로그램을 읽는 사람들로부터 이것이 바로 그 변수를 의미함을 쉽게 알아보게 할 수 있고, 특별한 IDE (Integrated Development Environment)를 사용하는 사람들도 이를 쉽게 알아볼 수 있는 등 여러 장점이 있습니다.


C++/Java/C# 프로그래머를 위한 주석

파이썬의 self 는 C++ 의 this 포인터와 같은 것이며, Java와 C# 의 this 참조와 같습니다


아마 여러분은 파이썬이 self 에 어떻게 값을 할당하는 것인지 그리고 정말 값을 직접 할당할 필요가 없는지 궁금할 것입니다. 이해를 돕기 위해 예를 하나 들어 보겠습니다. 여러분이 MyClass라는 클래스를 생성했고, 이 클래스의 객체를 myobject 라는 이름으로 생성했다고 해 봅시다.이제 이 객체의 메소드를 호출할 때는 yobject.method(arg1, arg2) 와 같이 하며, 이것은 파이썬에 의해 자동적으로 MyClass.method(myobject, arg1, arg2) 의 형태로 바뀌게 됩니다. 이것이 self 에 대한 모든 것입니다.


또한 이것은 아무런 인수도 넘겨받지 않는 메소드를 정의할 때에도, self 라는 하나의 인수를 추가해 주어야 한다는 것을 의미합니다.



12.2. 클래스


가장 단순한 클래스의 예시가 아래 예제에 나타나 있습니다( oop_simplestclass.py 로 저장하세요).


1
2
3
4
5
class Person:
    pass # An empty block
 
= Person()
print(p)
cs


실행 결과:


1
2
$ python oop_simplestclass.py
<__main__.Person instance at 0x10171f518>
cs


동작 원리 먼저 class 문을 사용하여 새로운 클래스를 생성하였고 적당한 이름을 지어 주었습니다. 그 아래로는 들여쓰기 된 새로운 블록이 시작되며 이 블록은 클래스의 몸체를 구성합니다. 위 예제의 경우에는 pass 문으로 해당 블록이 빈 블록임을 나타내 주었습니다.


다음으로, 이 클래스의 이름 뒤에 괄호를 열고 닫아 주어 클래스의 객체/인스턴스를 만들었습니다 (다음 섹션에서 객체 초기화 에 대해 좀 더 자세히 배울 것입니다). 객체가 잘 생성되었는지 확인해 보기 위해, 정의한 변수명을 입력하여 결과를 확인해 봅니다. 그러면 이 객체는 main 모듈의 Person 클래스의 인스턴스임을 알 수 있습니다.


또 객체가 실제로 저장된 컴퓨터 메모리의 위치가 함께 반환되는 것을 확인하시기 바랍니다. 컴퓨터마다 그 객체를 저장하기 위한 빈 공간이 위치한 곳이 다를 것이므로 컴퓨터마다 이 값은 다르게 출력될 것입니다.


12.3. 메소드


앞서 클래스/객체는 메소드를 가질 수 있으며, 메소드는 추가된 self 변수를 제외하고 함수와 똑같다는 것에 대해 이야기했습니다. 아래는 예제입니다( oop_method.py 로 저장하세요).


1
2
3
4
5
6
7
8
class Person:
    def say_hi(self):
        print('Hello, how are you?')
 
= Person()
p.say_hi()
# The previous 2 lines can also be written as
# Person().say_hi()
cs


실행 결과:


1
2
$ python oop_method.py
Hello, how are you?
cs


동작 원리위 예제는 self 가 어떻게 동작하는지 보여줍니다. 여기서 say_hi 메소드는 아무 매개 변수도 넘겨받지 않지만 함수 정의에 self 를 가지고 있음을 확인하시기 바랍니다.


12.4. init 메소드


파이썬의 클래스에는 여러가지 특별한 메소드 이름이 존재합니다. 우선 그 중 init 메소드의 중요성에 대해 알아보겠습니다.


init 메소드는 클래스가 인스턴스화 될 때 호출됩니다. 따라서 이 메소드는 객체가 생성될 때 여러가지 초기화 명령들이 필요할 때 유용하게 사용됩니다. 여기서 init의 앞과 뒤에 있는 밑줄은 두번씩 입력해야 한다는 점을 기억하시기 바랍니다.


예제 ( oop_init.py 로 저장하세요):


1
2
3
4
5
6
7
8
9
10
class Person:
    def __init__(self, name):
        self.name = name
    def say_hi(self):
        print ('Hello, my name is', self.name)
 
= Person('Swaroop')
p.say_hi()
# The previous 2 lines can also be written as
# Person('Swaroop').say_hi()
cs


실행 결과:


1
2
$ python oop_init.py
Hello, my name is Swaroop
cs


동작 원리먼저 매개 변수 name 을 넘겨 받는 init 메소드를 정의합니다(물론 self`를 포함하여 정의합니다). 그리고, `name 이라는 필드를 생성합니다. 이 때 두 다른 변수의 이름으로 'name' 이라는 동일한 이름을 지정해 주었다는 점에 주목하시기 바랍니다. 이것이 문제가 되지않는 이유는 하나는 "self" 라 칭해지는 객체에 내장된 것으로써 self.name 의 형태로 사용되며 또 하나인 name 은 지역 변수를 의미하는 것으로 사용되기 때문입니다. 프로그램 상에서 각각을 완전하게 구분할 수 있으므로, 혼란이 일어나지 않습니다.


위 예제에서 가장 중요한 것은, 우리가 init 메소드를 직접 호출해 주지 않고 클래스로부터 인스턴스를 생성할 때 괄호 안에 인수를 함께 넘겨 주었다는 점입니다. 이 점이 이 메소드가 좀 특별하게 다뤄지는 이유입니다.


이제, sayHi 메소드에서처럼 객체 내부에서 self.name 필드를 사용할 수 있습니다.


12.5. 클래스 변수와 객체 변수


앞서 클래스와 객체가 어떤 기능을 갖도록 하는 방법, 즉 메소드에 대해 설명했습니다. 이제 데이터의 경우 어떻게 하는지 배워봅시다. 데이터, 즉 필드는 일반적인 변수와 다를 것이 없으나 딱 한가지, 그 클래스 혹은 객체의 네임스페이스 에 묶여 있다는 점이 다릅니다. 이것은 필드의 이름은 그 클래스 혹은 객체 내부에서만 의미가 있음을 의미합니다. 그래서 이것을 이름이 통용되는 공간이라고 하여 네임스페이스 라고 부릅니다.


필드 에는 두 종류가 있는데, 클래스 변수와 객체 변수입니다. 각각은 그것을 소유하고 있는 대상이 클래스인지 객체인지에 따라 구분됩니다.


클래스 변수 는 공유됩니다. 즉, 그 클래스로부터 생성된 모든 인스턴스들이 접근할 수 있습니다. 클래스 변수는 한 개만 존재하며 어떤 객체가 클래스 변수를 변경하면 모든 다른 인스턴스들에 변경 사항이 반영됩니다.


객체 변수 는 클래스로부터 생성된 각각의 객체/인스턴스에 속해 있는 변수입니다. 이 경우에는, 각각의 객체별로 객체 변수를 하나씩 따로 가지고 있으며, 서로 공유되지 않고 각 인스턴스에 존재하는 같은 이름의 필드끼리 서로 어떤 방식으로든 간섭되지 않습니다. 아래 예제를 통해 좀 더 자세히 알아봅시다. ( oop_objvar.py 로 저장하세요):



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Robot:
    """Represents a robot, with a name."""
    # A class variable, counting the number of robots
    population = 0
 
    def __init__(self, name):
        """Initializes the data."""
        self.name = name
        print ("(Initializing {})".format(self.name))
 
        # When this person is created, the robot
        # adds to the population
        Robot.population += 1
 
    def die(self):
        """I am dying."""
        print ("{} is being destroyed!".format(self.name))
 
        Robot.population -= 1
 
        if Robot.population == 0:
            print ("{} was the last one.".format(self.name))
        else:
            print("There are still {:d} robots working.".format(Robot.population))
 
        def say_hi(self):
            """Greeting by the robot.
            Yeah, they can do that."""
            print("Greetings, my masters call me {}.".format(self.name))
 
    @classmethod
    def how_many(cls):
        """Prints the current population."""
        print ("We have {:d} robots.".format(cls.population))
 
droid1 = Robot("R2-D2")
droid1.say_hi()
Robot.how_many()
 
droid2 = Robot("C-3PO")
droid2.say_hi()
Robot.how_many()
 
print"\nRobots can do some work here.\n")
 
print"Robots have finished their work. So let's destroy them.")
droid1.die()
droid2.die()
 
Robot.how_many()

cs


실행 결과:

1
2
3
4
5
6
7
8
9
$ python oop_objvar.py
(Initializing R2-D2)
Greetings, my masters call me R2-D2.
We have 1 robots.
(Initializing C-3PO)
Greetings, my masters call me C-3PO.
We have 2 robots.
Robots can do some work here.
Robots have finished their work. So let's destroy them.
R2-D2 is being destroyed! There are still 1 robots working. C-3PO is being destroyed! C-3PO was the last one. We have 0 robots.

cs



동작 원리예제가 좀 길지만, 클래스/객체 변수의 이해를 돕도록 만들어져 있습니다. 여기서 population 은 Robot 클래스에 속해 있는 클래스 변수입니다. 또, name 변수는 객체에 소속되어 있는 (즉 self 를 이용하여 사용되는) 객체 변수입니다. 

또한, population 클래스 변수는 Robot.population 과 같이 사용하며 self.population 과 같이 사용하지 않습니다. 반면 객체 변수 name 은 그 객체 안에서 self.name 과 같이 사용됩니다. 이러한 클래스 변수와 객체 변수의 작은 차이점에 유의하시기 바랍니다. 또, 클래스 변수와 같은 이름을 가진 객체 변수는 클래스 변수를 감춘다는 점을 기억하세요!

Ropot.population 대신에 self.class.population 라고도 사용할 수 있는데 이것은 모든 객체는 그 객체를 생성하는 데 사용되었던 클래스를 self.class 속성을 통해 참조하고 있기 때문입니다.

메소드 how_many 는 객체에 소속되어 있지 않고 클래스에 소속되어 있는 메소드입니다. 여기서 우리가 해당 클래스의 어떤 부분까지 알아야 할 지에 따라 메소드를 클래스 메소드(class mathod) 로 정의할지 스태틱 메소드(static method) 로 정의할지 결정할 수 있습니다. 기서는 클래스 변수를 사용할 것이므로, 클래스 메소드 를 사용합시다.

여기서는 how_many 메소드를 클래스 메소드로 만들어 주기 위해 데코레이터 를 이용하였습니다.데코레이터는 어떤 일을 추가로 해 주는 더 큰 함수로 해당 부분을 감싸주는 것이라고 생각하면 됩니다. 즉, @classmethod 데코레이터는 아래처럼 호출하는 것과 같습니다:

1
how_many = classmethod(how_many)
cs

init 메소드는 Robot 의 인스턴스를 초기화시킬 때 사용됩니다. 이 메소드를 통해 로봇이 하나추가될 때마다 로봇의 개수를 의미하는 변수 population 을 1 씩 증가시켜 줍니다. 또한 각 생성된 객체별로 객체 변수 self.name 의 값을 따로따로 지정해 주었습니다.

객체에 속해 있는 변수와 메소드에 접근하기 위해서는 반드시 self 를 사용해야 한다는 점을 기억하시기 바랍니다. 이것을 다른 말로 속성 참조(attribute reference) 라 부릅니다.

프로그램을 살펴보면 메소드에 정의된 것 처럼 클래스에도 DocString 이 정의되어 있는 것을 보실 수 있습니다. 마찬가지로 이 DocString에도 Robot.doc 을 통해 접근할 수 있고, 또 메소드의 DocString 은 Robot.say_hi.doc 과 같이 접근할 수 있습니다.

die 메소드가 실행되면, 간단히 Robot.population 을 하나 줄여 줍니다.

모든 클래스 멤버는 클래스 외부에 공개되어 있습니다. 한가지 예외가 있는데, 여러분이 밑줄 두개 로 시작하는 데이터 멤버를 정의할 때, 즉 예를 들어 __privatevar 와 같이 하면, 파이썬이 이것을 클래스 외부로 드러나지 않도록 숨겨 줍니다.

이것은 클래스나 객체에 속해 있는 어떤 변수에나 적용됩니다. 클래스와 객체에 정의된 모든 이름은 밑줄로 시작하지 않는 이상 외부로 공개하고 다른 클래스나 객체에서 불러와 사용할 수 있도록 하는 규칙을 따르는 것이 좋습니다. 그러나 이것은 파이썬에서 강제하는 것이 아니며 (밑줄 두 개로 시작하는 경우를 제외하고) 프로그래머들끼리의 약속입니다.

C++/Java/C# 프로그래머를 위한 주석

모든 클래스 멤버는 (데이터 멤버를 포함하여) public 이며 따라서 파이썬의 모든 메소드는 virtual 입니다.


12.6. 상속
객체 지향 프로그래밍의 또 다른 큰 장점은 코드를 재사용 할 수 있다는 것인데 이를 위한 한 가지 방법으로 상속 이 사용됩니다. 상속은 클래스 간의 형식과 세부 형식 을 구현하는 것이라고 생각해볼 수 있습니다.

어러분이 어떤 대학의 교수들과 학생들의 명부를 작성하는 프로그램을 작성한다고 해 봅시다. 이때 교수와 학생 모두 공통적으로 이름, 나이, 주소 등의 성질을 가지고 있을 것이며, 교수에만 적용되는 성질로는 연봉, 과목, 휴가 등이 있을 것이고, 학생에만 적용되는 성질로는 성적, 등록금등이 있을 것입니다.

따라서 여러분은 각각의 경우에 두 독립적인 클래스를 만들 수 있겠지만, 이 경우 각각의 공통적인 성질 또한 각각의 클래스에 두 번씩 반복해서 정의해 주어야 할 것입니다. 매우 불편합니다. 

더 나은 방법은 SchoolMember 라는 이름으로 공통 클래스를 생성한 뒤 교수와 학생 클래스를이 클래스로부터 상속 받아 생성하는 것입니다. 이 경우 상속받은 클래스들은 이를테면 상위 형식(클래스) 의 세부 형식이 되는 것이고, 따라서 이 세부 형식에 각 상황에 맞는 세부적인 성질들을 추가해 줄 수 있는 것입니다.

이러한 접근 방식에는 많은 장점이 있습니다. 그 중 한 장점은 우리가 SchoolMember 에 새로운 기능을 추가하거나 혹은 있던 기능을 수정하게 되면, 그 하위 클래스인 교수와 학생 클래스에도 이러한 변경 사항이 자동으로 추가된다는 점입니다. 예를 들어 교수와 학생들에게 새로 출입증을 발급해야 할 경우 SchoolMember 클래스에 이를 적용해 주기만 하면 되는 것이죠. 반대로 하위 클래스에 적용된 변경 사항은 다른 하위 클래스에 적용되지 않습니다. 또 다른 장점은 여러분이 예를 들어 대학에 소속된 사람들의 모든 숫자를 파악해야 한다고 할 경우 교수와 학생 객체를 SchoolMember 객체로써 참조하여 사용할 수 있다는 점입니다. 이것을 다형성 이라고 부르는데, 하위 형식이 부모 형식을 필요로 하는 어떤 상황에서건 이를 대신하여 사용될 수 있다는 것을 의미합니다. 즉, 자식 클래스의 객체를 부모 클래스의 인스턴스인 것처럼 다루어질 수 있습니다.

따라서 상속을 이용하면 부모 클래스의 코드를 재사용할 수 있고 서로 완전히 독립적인 클래스들을 정의했을 때처럼 각각 다른 클래스에 이를 또 반복해서 써 줄 필요가 없다는 것입니다. 이 상황에서의 SchoolMember 클래스를 기본 클래스 혹은 슈퍼 클래스 라고 부릅니다. 또 Teacher 와 Student 클래스는 파생 클래스 혹은 서브 클래스 라고 부릅니다.

다음 프로그램을 예제로 살펴 보겠습니다( oop_subclass.py 로 저장하세요):


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class SchoolMember:
    '''Represents any school member.'''
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print ('(Initialized SchoolMember: {})'.format(self.name))
 
    def tell(self):
        '''Tell my details.'''
        print ('Name:"{}" Age:"{}"'.format(self.name, self.age),)
 
class Teacher(SchoolMember):
    '''Represents a teacher.'''
    def __init__(self, name, age, salary):
        SchoolMember.__init__(self, name, age)
        self.salary = salary
        print ('(Initialized Teacher: {})'.format(self.name))
 
    def tell(self):
        SchoolMember.tell(self)
        print ('Salary: "{:d}"'.format(self.salary))
 
class Student(SchoolMember):
    '''Represents a student.'''
    def __init__(self, name, age, marks):
        SchoolMember.__init__(self, name, age)
        self.marks = marks
        print ('(Initialized Student: {})'.format(self.name))
 
    def tell(self):
        SchoolMember.tell(self)
        print ('Marks: "{:d}"'.format(self.marks))
 
= Teacher('Mrs. Shrividya'4030000)
= Student('Swaroop'2575)
# prints a blank line
print
 
members = [t, s]
for member in members:
    # Works for both Teachers and Students
    member.tell()

cs


실행 결과:

1
2
3
4
5
6
7
$ python oop_subclass.py
(Initialized SchoolMember: Mrs. Shrividya)
(Initialized Teacher: Mrs. Shrividya)
(Initialized SchoolMember: Swaroop)
(Initialized Student: Swaroop)
Name:"Mrs. Shrividya" Age:"40" Salary: "30000"
Name:"Swaroop" Age:"25" Marks: "75"
cs

동작 원리상속을 사용하기 위해, 예제에서 정의된 여러 기본 클래스들의 이름들이 상속 튜플에 지정됩니다. 다음으로, 기본 클래스의 init 메소드가 self 변수를 이용하여 명시적으로 호출되며 따라서 객체의 기본 클래스에 정의된 초기화 명령들을 호출합니다. 즉, 파이썬은 기본 클래스의 생성자를 자동으로 호출해 주지 않으므로 명시적으로 이것을 호출해 주어야 한다는 점을 기억하시기 바랍니다.

또한 기본 클래스의 메소드를 호출할 때 클래스 이름을 메소드 호출에 지정해 주었고 또 self 변수에 인수들과 함께 넘겨 주었습니다.

여기서 SchoolMember 클래스의 tell 메소드를 사용할 때, Teacher 나 Student 와 같은 인스턴스들을 SchoolMember 의 인스턴스로써 사용하였다는 점을 확인하시기 바랍니다.

또, 위에서 tell 메소드를 호출할 때 하위 클래스의 메소드가 호출되었고 SchoolMember 클래스의 메소드가 호출되지 않았다는 것을 확인하세요. 즉, 파이썬은 언제나 해당 형식 안에서 해당 메소드가 있는지 찾고, 여기서 메소드를 찾지 못한 경우 그 클래스의 기본 클래스를 한 단계씩 찾아 올라가면서 해당 메소드가 있는지 계속 확인한다는 것을 기억하시면 이해하기 쉬울 것입니다.

상속 튜플에 하나 이상의 클래스가 등록되어 있을 경우, 이것을 다중 상속 이라고 부릅니다.슈퍼 클래스의 tell() 메소드에서 print 문 뒤에 붙어 있는 쉼표는 그 다음에 출력 될 내용을 새로운 줄에 출력하지 말고 그 줄에 이어서 출력하라는 것을 의미합니다. 이것은 print 가 \n (줄바꿈) 문자를 마지막에 입력하지 않게 하는 것입니다.

12.7. 요약
지금까지 클래스와 객체의 다양한 속성에 대해 알아 보았으며, 또 통용되는 용어들에 대해서도 알아 보았습니다. 또한 객체 지향 프로그래밍을 사용할 때의 장점과 주의해야 할 점에 대해서도 알아 보았 습니다. 파이썬은 고도의 객체 지향 언어로 이러한 개념을 잘 익혀 두면 여러분이 좋은 파이썬 프로그래머로서 꾸준히 성장할 수 있게 될 것입니다.

다음으로, 파이썬에서의 입/출력을 다루는 법과 파일을 읽고 쓰는 법에 대해 배워 보겠습니다.