객체지향 프로그래밍

지금까지 작성한 모든 프로그램에서는 프로그램을 함수, 즉 데이터를 조작하는 문장 블록 위주로 설계했습니다. 이를 절차지향(procedure-oriented) 방식의 프로그래밍이라고 합니다. 데이터와 기능을 결합해서 그것을 객체(object)라고 하는 것 안에 감싸는 식으로 프로그램을 조직화하는 또 다른 방법이 있습니다. 이를 객체지향(object-oriented) 프로그래밍 패러다임이라고 합니다. 대부분의 경우 절차적 프로그래밍을 이용할 수 있지만 규모가 큰 프로그램을 작성하거나 이 방법에 더 적합한 문제가 있을 경우 객체지향 프로그래밍 기법을 이용할 수 있습니다.

클래스와 객체는 객체지향 프로그래밍의 두 가지 주요 측면입니다. 클래스(class)는 새로운 타입(type)을 생성하며, 이때 객체(object)는 해당 클래스의 인스턴스(instance)입니다. 이를 int 타입의 변수에 비유하자면 정수를 저장하는 변수는 int 클래스의 인스턴스(객체)에 해당하는 변수라고 할 수 있습니다.

정적 언어 프로그래머를 위한 메모

심지어 정수도 객체(int 클래스의)로 간주된다는 점에 유의합니다. 이는 정수가 원시 네이티브 타입(primitive native type)에 해당하는 C++나 자바(1.5 버전 이전의)와는 다른 점입니다.

이 클래스에 대한 자세한 사항은 help(int)를 참고하세요.

C#과 자바 1.5 프로그래머는 이것이 박싱(boxing)과 언박싱(unboxing)의 개념과 비슷하다고 느낄 것입니다.

객체에는 해당 객체에 속하는(belong) 평범한 변수를 이용해 데이터를 저장할 수 있습니다. 어떤 객체나 클래스에 속하는 변수를 필드(field)라고 합니다. 객체는 클래스에 속하는 함수를 이용해 기능을 가질 수도 있습니다. 이러한 함수를 해당 클래스의 메서드(method)라고 합니다. 이 같은 용어는 중요한데, 클래스나 객체에 속하는 함수 및 변수를 클래스나 객체와 상관없이 독립적인 함수 및 변수와 구분하는 데 도움이 되기 때문입니다. 필드와 메서드를 통틀어서 해당 클래스의 속성(attribute)이라고 할 수 있습니다.

필드에는 두 가지 유형이 있습니다. 클래스의 각 인스턴스/객체에 속할 수도 있고 클래스 자체에 속할 수도 있습니다. 이를 각각 인스턴스 변수(instance variable)클래스 변수(class variable)라고 합니다.

클래스는 class 키워드를 이용해 생성합니다. 해당 클래스의 필드와 메서드는 들여쓰기한 블록 안에 나열합니다.

self

클래스의 메서드는 일반 함수와 한 가지 차이점만 있습니다. 즉, 매개변수 목록의 맨 앞에 반드시 별도의 이름을 첫 번째로 추가해야 하지만 해당 메서드를 호출할 때는 이 매개변수에 값을 지정해서는 안 되고, 파이썬이 값을 제공한다는 것입니다. 이처럼 특별한 변수는 해당 객체 자체를 가리키고, 관례상 self라는 이름을 줍니다.

이 매개변수에 다른 이름을 줄 수도 있지만 self라는 이름을 사용하길 적극 권장합니다. 다른 이름을 지정하는 것은 그다지 바람직하지 않습니다. 표준화된 이름을 사용하는 데는 여러 이점이 있습니다. 즉, self를 사용할 경우 프로그램을 읽는 사람이 이를 곧바로 인식할 수 있고 전문적인 IDE(Integrated Development Environments)에서는 이를 활용해 코드 작성에 도움을 줄 수 있습니다.

C++/자바/C# 프로그래머를 위한 메모

파이썬의 self는 C++에서의 this 포인터, 자바와 C#에서의 this 참조에 해당합니다.

파이썬이 어떻게 self의 값을 지정하고 왜 거기에 값을 줄 필요가 없는지 분명 궁금할 것입니다. 예제를 살펴보면 명확하게 이해할 수 있을 것입니다. MyClass라는 클래스가 있고, 이 클래스의 인스턴스를 myobject라고 해봅시다. myobject.method(arg1, arg2)로 이 객체의 메서드를 호출하면 이는 파이썬에 의해 자동으로 MyClass.method(myobject, arg1, arg2)으로 변환되고, 이것이 바로 특별한 self의 정체입니다.

또한 이것은 어떤 메서드가 인자를 받지 않더라도 적어도 self는 인자로 지정해야 한다는 것을 의미합니다.

클래스

가장 간단한 클래스의 예를 다음 예제에서 볼 수 있습니다(oop_simplestclass.py).

class Person:
    pass  # 빈 블록

p = Person()
print(p)

출력 결과:

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

동작 원리

class 문과 클래스의 이름을 이용해 새 클래스를 만듭니다. 다음으로 이 클래스의 본문을 구성하는 들여쓰기된 문장 블록이 나옵니다. 이 경우 pass 문을 이용해 빈 블록을 만듭니다.

다음으로, 클래스명 다음에 괄호 쌍을 지정해 이 클래스의 객체/인스턴스를 생성합니다. (인스턴스화에 대한 더 자세한 내용은 다음 절에서 배웁니다). 확인 차원에서 변수를 출력해 변수의 타입을 확인합니다. 출력 결과를 통해 __main__ 모듈의 Person 클래스의 인스턴스임을 확인할 수 있습니다.

객체가 저장된 컴퓨터 메모리의 주소도 출력된다는 점에 유의합니다. 이 주소는 파이썬이 공간을 찾아야 객체를 저장할 수 있기 때문에 컴퓨터마다 값이 달라집니다.

메서드

이미 앞에서 클래스/객체는 별도의 self 변수를 갖는다는 점만 제외하면 함수와 비슷한 메서드를 가질 수 있다는 것을 살펴봤습니다. 이제 예제를 하나 보겠습니다(oop_method.py).

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

p = Person()
p.say_hi()
# 위의 두 줄은 Person('Swaroop').say_hi()라고 작성해도 됩니다

출력 결과:

$ python oop_method.py
Hello, how are you?

동작 원리

여기서 self를 실제로 볼 수 있습니다. 참고로 say_hi 메서드는 매개변수를 받지 않는데도 함수 정의에는 self가 있습니다.

__init__ 메서드

파이썬 클래스에는 특별한 의미를 가진 메서드가 많습니다. 이번에는 __init__ 메서드의 의미를 살펴보겠습니다.

__init__ 메서드는 클래스의 객체가 인스턴스화(즉, 생성)되자마자 실행됩니다. 이 메서드는 객체를 이용해 하고 싶은 초기화(initialization)(즉, 객체의 초깃값을 전달)를 수행하는 데 유용합니다. 여기서 메서드명 앞뒤로 밑줄을 두 개 사용한다는 데 주목하세요.

예제(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()
# 위의 두 줄은 Person('Swaroop').say_hi()라고 작성해도 됩니다

출력 결과:

$ python oop_init.py
Hello, my name is Swaroop

동작 원리

여기서는 name이라는 매개변수(늘 그렇듯이 self와 함께)를 받는 __init__ 메서드를 정의합니다. 또한 name이라는 필드도 새로 하나 생성합니다. 두 변수 모두 이름이 'name'이지만 서로 다른 변수라는 점에 주목하세요. 여기서 self.name이라는 점 표기법은 "name"이라는 것이 "self"라는 객체의 일부이고 다른 name은 지역 변수이기 때문에 아무런 문제가 없습니다. 예제에서는 명시적으로 어느 이름을 참조하는지 나타내고 있기 때문에 파이썬이 혼동할 여지가 없습니다.

Person 클래스의 새 인스턴스인 p를 생성할 때는 p = Person('Swaroop')처럼 클래스명 다음에 나오는 괄호 안에 인수를 넣어서 인스턴스를 생성합니다.

보다시피 __init__ 메서드를 명시적으로 호출하지 않습니다. 이것이 바로 이 메서드의 특별한 점입니다.

이제 메서드 내에서 self.name 필드를 이용할 수 있게 됩니다. 이를 say_hi 메서드에서 확인할 수 있습니다.

클래스와 객체 변수

이미 앞에서 클래스와 객체의 기능부(즉, 메서드)를 살펴봤으므로 이제 데이터부에 관해 배워봅시다. 데이터부, 즉 필드는 클래스와 객체의 네임스페이스(namespace)묶인(bound) 평범한 변수에 불과합니다. 이것은 이러한 이름들이 해당 클래스와 객체의 문맥 내에서만 유효하다는 것을 의미합니다. 이러한 이유로 그것들을 네임 스페이스(name space)라고 부르는 것입니다.

필드에는 클래스 변수와 객체 변수로 두 가지 유형이 있습니다. 두 유형은 클래스 또는 객체 중 어느 것이 해당 변수를 소유(own)하느냐에 따라 각기 구분됩니다.

클래스 변수(class variable)는 공유되어 클래스의 모든 인스턴스에서 접근할 수 있습니다. 클래스 변수는 단 하나의 사본만이 존재하고, 한 객체가 클래스 변수를 변경하면 해당 변경 사항은 다른 모든 인스턴스에 반영됩니다.

객체 변수(object variable)는 클래스의 각 개별 객체/인스턴스에서 소유합니다. 이 경우 각 객체는 자신만의 필드 사본을 가집니다. 즉, 공유되지 않으며, 서로 다른 인스턴스에서 이름은 같아도 필드와 아무런 관련이 없습니다. 다음은 이를 쉽게 이해할 수 있는 예제입니다(oop_objvar.py).

class Robot:
    """이름을 가진 로봇을 나타냅니다."""

    # 로봇의 수를 세는 클래스 변수
    population = 0

    def __init__(self, name):
        """데이터를 초기화합니다."""
        self.name = name
        print("(Initializing {})".format(self.name))

        # 이 로봇이 생성되면 로봇 수에 더해집니다.
        Robot.population += 1

    def die(self):
        """저는 죽어가고 있습니다."""
        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):
        """로봇이 인사합니다.

        로봇은 인사할 수 있습니다."""
        print("Greetings, my masters call me {}.".format(self.name))

    @classmethod
    def how_many(cls):
        """현재 로봇 수를 출력합니다."""
        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()

출력 결과:

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

동작 원리

예제 코드가 길지만 클래스 변수와 객체 변수의 특성을 잘 보여줍니다. 여기서 populationRobot 클래스에 속하고, 따라서 클래스 변수에 해당합니다. name 변수는 객체에 속하고(self를 이용해 할당됨), 따라서 객체 변수입니다.

그러므로 self.population이 아닌 Robot.population으로 population 클래스 변수를 참조합니다. name 객체 변수는 객체의 메서드 내에서 self.name 표기법을 이용해 참조합니다. 클래스 변수와 객체 변수의 이 같은 차이점을 기억해 두세요. 또한 클래스 변수와 이름이 같은 객체 변수는 클래스 변수에 의해 숨겨진다는 점도 알아두세요!

Robot.population 대신 self.__class__.population을 사용할 수도 있는데, 모든 객체는 자기 자신의 클래스를 self.__class__ 속성을 통해 참조하기 때문입니다.

사실 how_many는 객체가 아닌 클래스에 속하는 메서드입니다. 이것은 필드나 메서드가 어느 클래스에 속하는지 알 필요가 있느냐에 따라 해당 메서드를 classmethodstaticmethod로 정의할 수 있다는 뜻입니다. 여기서는 클래스 변수를 참조하고 있으므로 classmethod를 사용하겠습니다.

예제에서는 데코레이터(decorator)를 이용해 how_many 메서드를 클래스 메서드로 표시했습니다.

데코레이터는 래퍼 함수(즉, 내부 함수의 앞뒤로 뭔가를 수행할 수 있도록 다른 함수를 "감싸는" 함수)를 호출하는 축약 표현으로 생각할 수 있으므로 @classmethod 데코레이터를 적용하는 것은 다음과 같이 호출하는 것과 같습니다.

how_many = classmethod(how_many)

__init__ 메서드로 Robot 인스턴스에 이름을 전달해 초기화하는 것을 눈여겨봅시다. 이 과정에서 로봇이 하나 더 추가되기 때문에 __init__ 메서드에서는 population의 값을 1만큼 증가시킵니다. 또한 self.name의 값이 각 객체에 따라 다른데, 이는 객체 변수의 특성을 나타냅니다.

한 가지 기억해야 할 것은 반드시 self만을 이용해 동일 객체의 변수와 메서드를 참조해야 한다는 것입니다. 이를 속성 참조(attribute reference)라 합니다.

이 프로그램에서 클래스와 메서드에 독스트링을 사용하는 것을 볼 수 있습니다. 클래스 독스트링은 런타임에 Robot.__doc__으로, 메서드 독스트링은 Robot.say_hi.__doc__으로 접근할 수 있습니다.

die 메서드에서는 Robot.population 값을 1만큼 줄입니다.

모든 클래스 멤버는 외부에 공개됩니다. 한 가지 예외가 있는데, __privatevar처럼 이름이 밑줄 2개로 시작하는 데이터 멤버는 파이썬이 네임 맹글링(name-mangling)을 이용해 사실상 비공개 변수로 만듭니다.

따라서 클래스나 객체 내에서만 사용되는 변수의 이름은 밑줄로 시작하는 것이 관례이고, 다른 모든 이름들은 공개되어 다른 클래스/객체에서 사용할 수 있게 됩니다. 다만 이것은 관례일 뿐 파이썬에 의해 강제되지 않는다는 점을 기억하세요(이름이 밑줄 2개로 시작하는 경우를 제외하고).

C++/자바/C# 프로그래머를 위한 메모

파이썬에서 모든 클래스 멤버(데이터 멤버를 포함해서)는 공개(public)되고 모든 메서드는 가상(virtual) 메서드입니다.

상속

객체지향 프로그래밍의 주된 이점 중 하나는 코드의 재사용(reuse)이고, 이렇게 하는 한 가지 방법은 상속(inheritance) 메커니즘을 이용하는 것입니다. 상속은 클래스 간의 타입과 하위 타입 관계를 구현하는 것이라고 생각하면 됩니다.

학교에서 교사와 학생을 관리해야 하는 프로그램을 작성한다고 해봅시다. 교사와 학생은 이름이나 나이, 주소와 같은 공통적인 특성이 있습니다. 또한 교사에게는 급여, 수업, 휴가 같은 특징이, 학생에게는 점수와 수업료 같은 특성이 있습니다.

이 경우 각 타입에 대해 서로 독립적인 두 개의 클래스를 만들어서 처리할 수도 있지만 새로운 공통적인 특성을 추가하려면 이러한 독립적인 클래스에 모두 추가해야 할 것입니다. 이렇게 되면 코드 관리가 금방 어려워집니다.

더 나은 방법은 SchoolMember라는 공통 클래스를 만들고 교사와 학생 클래스가 이 클래스로부터 상속받는(inherit) 것입니다. 즉, 교사와 학생 클래스가 이 타입(클래스)의 하위 타입이 되고 이러한 하위 타입에만 특화된 특성을 추가할 수 있습니다.

이 방법에는 여러 이점이 있습니다. SchoolMember의 기능을 추가하거나 변경할 경우 하위 타입에도 이러한 변화가 자동으로 반영됩니다. 예를 들어, SchoolMember 클래스에 간단히 새로운 ID 카드 필드를 추가하기만 해도 교사와 학생에 대해 모두 새로운 ID 카드 필드가 추가됩니다. 하지만 하위 타입에서 발생한 변경 사항은 다른 타입에 영향을 주지 않습니다. 또 한 가지 이점은 교사나 학생 객체를 SchoolMember 객체로 참조할 수 있다는 것인데, 이는 학교 구성원의 수를 셀 때처럼 특정 상황에 유용할 수 있습니다. 이를 다형성(polymorphism)이라 하며, 상위 타입을 기대하는 상황, 즉 어떤 객체를 상위 클래스의 인스턴스로 취급할 수 있는 상황에서 하위 타입을 상위 타입으로 대체할 수 있습니다.

또한 상위 클래스의 코드를 재사용해서 다른 클래스에서 코드를 반복해서 작성할 필요가 없다는 점도 눈여겨볼 필요가 있습니다. 서로 독립적인 클래스를 사용했다면 코드를 반복해서 작성해야 했을 것입니다.

이 같은 상황에서 SchoolMember 클래스를 기반 클래스(base class) 또는 상위 클래스(superclass)라 합니다. 반면 TeacherStudent 클래스는 파생 클래스(derived classes) 또는 하위 클래스(subclasses)라 합니다.

이제 이 예제를 프로그램으로 살펴봅시다(oop_subclass.py).

class SchoolMember:
    '''학교 구성원을 나타냅니다.'''
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print('(Initialized SchoolMember: {})'.format(self.name))

    def tell(self):
        '''세부사항을 출력합니다.'''
        print('Name:"{}" Age:"{}"'.format(self.name, self.age), end=" ")


class Teacher(SchoolMember):
    '''교사를 나타냅니다.'''
    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):
    '''학생을 나타냅니다.'''
    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)

# 빈 줄을 출력합니다
print()

members = [t, s]
for member in members:
    # Teacher와 Student에 대해 모두 동작합니다
    member.tell()

출력 결과:

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

동작 원리

상속을 사용하기 위해 클래스 정의에서 클래스명 다음에 나오는 튜플에 기반 클래스의 이름을 지정합니다(예: class Teacher(SchoolMember)). 다음으로, 하위 클래스에서 인스턴스의 기반 클래스 부분을 초기화할 수 있도록 self 변수를 이용해 기반 클래스의 __init__ 메서드를 명시적으로 호출합니다. 이것은 매우 중요하므로 기억해 두세요. 예제에서는 TeacherStudent 하위 클래스에서 __init__ 메서드를 정의하고 있어 파이썬이 자동으로 기반 클래스인 SchoolMember의 생성자를 호출하지 않기 때문에 직접 __init__ 메서드를 명시적으로 호출해야 합니다.

이와 달리 하위 클래스에서 __init__ 메서드를 정의하지 않았다면 파이썬이 자동으로 기반 클래스의 생성자를 호출할 것입니다.

TeacherStudent의 인스턴스를 SchoolMember의 인스턴스로 취급해서 Teacher.tell이나 Student.tellSchoolMembertell 메서드에 접근할 수 있음에도 각 하위 클래스에 맞춰 tell 메서드를 각 하위 클래스에 별도로 정의합니다(메서드 안에서 SchoolMembertell 메서드를 사용해). 여기서 이렇게 했기 때문에 Teacher.tell이라고 작성했을 때 파이썬이 상위 클래스가 아닌 해당 하위 클래스의 tell 메서드를 사용합니다. 하위 클래스에서 tell 메서드를 정의하지 않았다면 파이썬은 상위 클래스의 tell 메서드를 사용할 것입니다. 파이썬은 언제나 실제 하위 클래스 타입에서 먼저 메서드를 찾고, 만약 하위 클래스에서 아무것도 찾지 못하면 클래스 정의에서 튜플에 지정된 순서대로 하나씩(여기서는 기반 클래스가 하나밖에 없지만 기반 클래스를 여러 개 둘 수도 있습니다) 기반 클래스에서 메서드를 찾기 시작합니다.

용어 노트: 상속 튜플에 클래스를 2개 이상 지정한 경우 이를 다중 상속(multiple inheritance)이라 합니다.

상위 클래스의 tell() 메서드에서는 end 매개변수를 사용해 한 줄을 출력하고 다음 출력을 같은 줄에 계속 이어지게 합니다. 이렇게 하면 print가 출력 결과의 끝에 \n(개행) 기호를 출력하지 않습니다.

정리

지금까지 클래스와 객체의 다양한 측면을 비롯해 이와 연관된 다양한 용어를 살펴봤습니다. 또한 객체지향 프로그래밍의 이점과 함정도 살펴봤습니다. 파이썬은 대단히 객체지향적이고 이러한 개념을 신경 써서 이해한다면 장기적으로 크게 도움이 될 것입니다.

다음 장에서는 파이썬에서 입력/출력을 처리하고 파일에 접근하는 방법을 배우겠습니다.