14. 객체 지향 프로그래밍

지금까지 우리는 데이터를 다루는 명령 블록인 함수를 조합하여 프로그램을 구성했습니다. 이러한 설계 방식을 절차 지향 프로그래밍 기법이라고 부릅니다. 이와 달리 데이터와 기능을 객체라고 불리우는 것으로 묶어 프로그램을 구성하는 또 다른 기법이 있습니다. 이를 객체 지향 프로그래밍 기법이라고 합니다. 아마도 여러분은 대부분의 프로그램을 절차 지향 프로그래밍 기법으로 작성할 것입니다. 그러나 규모가 큰 프로그램을 작성해야 하거나, 문제 해결이 더 편리할 경우 객체 지향 프로그래밍 기법도 사용할 일이 있을 것입니다.

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


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

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

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

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



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

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

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


14.1 self 에 대하여

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

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


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

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



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

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


14.2 클래스

다음은 가장 단순한 클래스의 예제입니다.


https://github.com/swaroopch/byte-of-python/blob/master/programs/oop_simplestclass.py

class Person:
    pass # An empty block

p = Person()
print(p)
oop_simplestclass.py(이 코드를 oop_simplestclass.py로 저장하세요)

실행 결과는 다음과 같습니다.

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


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

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

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


14.3 메소드

앞서 클래스/객체는 메소드를 가질 수 있으며, 메소드는 추가된 self 변수를 제외하면 함수와 똑같다고 했습니다. 다음은 예제에서 확인할 수 있습니다.


https://github.com/swaroopch/byte-of-python/blob/master/programs/oop_method.py

class Person:
    def say_hi(self):
        print('Hello, how are you?')

p = Person()
p.say_hi()
# The previous 2 lines can also be written as
# Person().say_hi()
oop_method.py(이 코드를 oop_method.py로 저장하세요)

실행 결과는 다음과 같습니다.

$ python oop_method.py
Hello, how are you?


이 예제는 self가 어떻게 동작하는지 보여줍니다. 여기서 say_hi 메소드는 아무 매개 변수도 넘겨받지 않지만 함수 정의에 self를 가지고 있습니다.


14.4 init 메소드

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

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


https://github.com/swaroopch/byte-of-python/blob/master/programs/oop_init.py

class Person:
    def __init__(self, name):
        self.name = name
    def say_hi(self):
        print 'Hello, my name is', self.name

p = Person('Swaroop')
p.say_hi()
# The previous 2 lines can also be written as
# Person('Swaroop').say_hi()
oop_init.py(이 코드를 oop_init.py로 저장하세요)

실행 결과는 다음과 같습니다.

$ python oop_init.py
Hello, my name is Swaroop


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

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

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


14.5 클래스 변수와 객체 변수

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

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

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

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


https://github.com/swaroopch/byte-of-python/blob/master/programs/oop_objvar.py

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()
oop_objvar.py(이 코드를 oop_objvar.py로 저장하세요)

실행 결과는 다음과 같습니다.

$ 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.


예제가 좀 길지만, 클래스/객체 변수의 이해하기에 좋습니다. 여기서 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 메소드를 클래스 메소드로 만들어 주기 위해 18.8 데코레이터를 이용했습니다.

데코레이터는 어떤 일을 추가로 하는 더 큰 함수로 해당 부분을 감싸준다고 생각하면 됩니다. 즉, @classmethod 데코레이터는 다음처럼 호출하는 것과 같습니다.

how_many = classmethod(how_many)

 

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

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

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

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

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

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


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

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



14.6 상속

객체 지향 프로그래밍의 또 다른 장점은 코드를 재사용할 수 있다는 것입니다. 재사용을 위한 방법으로 상속이 사용되죠. 상속은 클래스 간의 형식과 세부 형식을 구현하는 것입니다.

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

교수와 학생에 대해 독립된 클래스를 만들어도 되지만, 이 경우 공동된 성질이 각각의 클래스에 두 번식 박복해서 정의를 해야 합니다. 매우 불편하죠!

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

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

따라서 상속을 이용하면 부모 클래스의 코드를 재사용할 수 있고 서로 완전히 독립적인 클래스를 정의했을 때처럼 각각 다른 클래스에 이를 반복해서 쓸 필요가 없습니다.

이런 상황에서의 SchoolMember 클래스를 기본 클래스 혹은 슈퍼 클래스라고 부릅니다. 또 Teacher와 Student 클래스는 파생 클래스 혹은 서브 클래스라고 부릅니다.

다음 프로그램을 예제로 살펴 보겠습니다.


https://github.com/swaroopch/byte-of-python/blob/master/programs/oop_subclass.py

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)

t = Teacher('Mrs. Shrividya', 40, 30000)
s = Student('Swaroop', 25, 75)

# prints a blank line
print

members = [t, s]
for member in members:
    # Works for both Teachers and Students
    member.tell()
oop_subclass.py(이 코드를 oop_subclass.py로 저장하세요)

실행 결과는 다음과 같습니다.

$ 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"


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

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

여기서 SchoolMember 클래스의 tell 메소드를 사용할 때, Teacher나 Student와 같은 인스턴스를 SchoolMember의 인스턴스로서 사용하였다는 점에 주의하세요.

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

상속 튜플에 하나 이상의 클래스가 등록되어 있을 경우, 이것을 다중 상속이라고 부릅니다.

슈퍼 클래스의 tell() 메소드에서 print 문 뒤에 붙어 있는 쉼표는 그 다음에 출력될 내용을 새로운 줄에 출력하지 말고 그 줄에 이어서 출력하라는 의미입니다. 이것은 print가 \n (줄바꿈) 문자를 마지막에 입력하지 않게 하는 것입니다.


14.7 요약

지금까지 클래스와 객체의 다양한 속성과 통용되는 용어에 대해 살펴봤습니다. 또한 객체 지향 프로그래밍을 사용할 때의 장점과 주의해야 할 점도 알아봤습니다. 파이썬은 고수준의 객체 지향 언어이기 때문에 이러한 개념을 잘 익혀두면 파이썬 프로그래머로 성장하는 데 큰 도움이 될 것입니다.

다음에는 파이썬에서 입/출력을 다루는 법과 파일을 읽고 쓰는 법을 살펴봅니다.