객체지향 프로그래밍
지금까지 작성한 모든 프로그램에서는 프로그램을 함수, 즉 데이터를 조작하는 문장 블록 위주로 설계했습니다. 이를 절차지향(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.
동작 원리
예제 코드가 길지만 클래스 변수와 객체 변수의 특성을 잘 보여줍니다. 여기서 population
은 Robot
클래스에 속하고, 따라서 클래스 변수에 해당합니다. name
변수는 객체에 속하고(self
를 이용해 할당됨), 따라서 객체 변수입니다.
그러므로 self.population
이 아닌 Robot.population
으로 population
클래스 변수를 참조합니다. name
객체 변수는 객체의 메서드 내에서 self.name
표기법을 이용해 참조합니다. 클래스 변수와 객체 변수의 이 같은 차이점을 기억해 두세요. 또한 클래스 변수와 이름이 같은 객체 변수는 클래스 변수에 의해 숨겨진다는 점도 알아두세요!
Robot.population
대신 self.__class__.population
을 사용할 수도 있는데, 모든 객체는 자기 자신의 클래스를 self.__class__
속성을 통해 참조하기 때문입니다.
사실 how_many
는 객체가 아닌 클래스에 속하는 메서드입니다. 이것은 필드나 메서드가 어느 클래스에 속하는지 알 필요가 있느냐에 따라 해당 메서드를 classmethod
나 staticmethod
로 정의할 수 있다는 뜻입니다. 여기서는 클래스 변수를 참조하고 있으므로 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)라 합니다. 반면 Teacher
와 Student
클래스는 파생 클래스(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__
메서드를 명시적으로 호출합니다. 이것은 매우 중요하므로 기억해 두세요. 예제에서는 Teacher
와 Student
하위 클래스에서 __init__
메서드를 정의하고 있어 파이썬이 자동으로 기반 클래스인 SchoolMember
의 생성자를 호출하지 않기 때문에 직접 __init__
메서드를 명시적으로 호출해야 합니다.
이와 달리 하위 클래스에서 __init__
메서드를 정의하지 않았다면 파이썬이 자동으로 기반 클래스의 생성자를 호출할 것입니다.
Teacher
나 Student
의 인스턴스를 SchoolMember
의 인스턴스로 취급해서 Teacher.tell
이나 Student.tell
로 SchoolMember
의 tell
메서드에 접근할 수 있음에도 각 하위 클래스에 맞춰 tell
메서드를 각 하위 클래스에 별도로 정의합니다(메서드 안에서 SchoolMember
의 tell
메서드를 사용해). 여기서 이렇게 했기 때문에 Teacher.tell
이라고 작성했을 때 파이썬이 상위 클래스가 아닌 해당 하위 클래스의 tell
메서드를 사용합니다. 하위 클래스에서 tell
메서드를 정의하지 않았다면 파이썬은 상위 클래스의 tell
메서드를 사용할 것입니다. 파이썬은 언제나 실제 하위 클래스 타입에서 먼저 메서드를 찾고, 만약 하위 클래스에서 아무것도 찾지 못하면 클래스 정의에서 튜플에 지정된 순서대로 하나씩(여기서는 기반 클래스가 하나밖에 없지만 기반 클래스를 여러 개 둘 수도 있습니다) 기반 클래스에서 메서드를 찾기 시작합니다.
용어 노트: 상속 튜플에 클래스를 2개 이상 지정한 경우 이를 다중 상속(multiple inheritance)이라 합니다.
상위 클래스의 tell()
메서드에서는 end
매개변수를 사용해 한 줄을 출력하고 다음 출력을 같은 줄에 계속 이어지게 합니다. 이렇게 하면 print
가 출력 결과의 끝에 \n
(개행) 기호를 출력하지 않습니다.
정리
지금까지 클래스와 객체의 다양한 측면을 비롯해 이와 연관된 다양한 용어를 살펴봤습니다. 또한 객체지향 프로그래밍의 이점과 함정도 살펴봤습니다. 파이썬은 대단히 객체지향적이고 이러한 개념을 신경 써서 이해한다면 장기적으로 크게 도움이 될 것입니다.
다음 장에서는 파이썬에서 입력/출력을 처리하고 파일에 접근하는 방법을 배우겠습니다.