파이썬 – OOP Part 2. 클래스와 인스턴스(Class and Instance)

11 분 소요

이번 강좌에서는 클래스와 인스턴스에 대해서 배우도록 하겠습니다.

“파이썬은 객체 지향적 프로그래밍 언어입니다. 파이썬의 모든 것은 오브젝트입니다. 문자열, 리스트, 함수, 심지어 모듈 또한 오브젝트입니다…”라고 하는 얘기는 귀가 아프도록 들으셨을 겁니다. 그런데 도데체 오브젝트가 무엇일까요? 오브젝트란 속성과 같은 여러가지의 데이터와 함수(오브젝트 안에서는 메소드라고 부릅니다.)를 포함한 하나의 데이터 구조를 말합니다. 또한 파이썬에서 이 오브젝트들은 변수에 할당될 수도 있고, 함수의 인자로 전달될 수도 있는 퍼스트 클래스 오브젝트입니다. 퍼스트 클래스 오브젝트에 대해서는 이전 강좌인 퍼스트클래스 함수를 참고하여 주십시오.

오브젝트란?

오브젝트란 데이터를 조금 더 쉽게 다루기 위해서 “네임스페이스”라는 것을 이용하여 만든 논리적인 집합입니다. 학교에서 학생들을 관리하기 위해서 학년을 나누고 반을 나누는 것처럼요. 파이썬 사전을 만드는 것과 클래스와 모듈로 데이터 집합을 만드는 것 모두 데이터를 손쉽게 저장, 변경 또는 엑세스 할 수 있도록 오브젝트를 만드는 것 입니다.

네임스페이스에 대해서는 모듈을 다룰 때 자세히 설명 드리겠습니다.

다음의 코드는 형식만 다를뿐 모두 논리적인 데이터 집합인 오브젝트를 만들어 필요한 데이터에 엑세스하는 방법을 보여주고 있습니다.

원하시는 디렉터리에 oop_2.py라는 이름의 파이썬 파일을 만들고 다음의 코드를 저장하여 주십시오.

사전을 사용하는 경우

oop_2.py
student = {'name': '이상희', 'year': 2, 'class': 3, 'student_id': 35}
 
print('{}, {}학년 {}{}번'.format(student['name'], student['year'], student['class'], student['student_id']))

터미널이나 커맨드창을 여시고 oop_2.py가 저장된 디렉터리로 이동하신 후, 프로그램을 실행하여 주십시오.

실행 결과
이상희, 2학년 3 35

클래스를 사용하는 경우

oop_2.py
class Student:
def __init__(self, name, year, class_num, student_id):  # 파이썬 키워드인 class는 인수 이름으로 사용하지 못 합니다.
self.name = name
self.year = year
self.class_num = class_num
self.student_id = student_id
 
    def introduce_myself(self):
        return '{}, {}학년 {}{}번'.format(self.name, self.year, self.class_num, self.student_id)
 
 
student = Student('이상희', 2, 3, 35)
print(student.introduce_myself())
실행 결과
이상희, 2학년 3 35

모듈을 사용하는 경우 같은 폴더안에 student.py 파일을 하나 만든 후 아래 코드를 입력합니다.

student.py
name = '이상희'
year = 2
class_id = 3
student_id = 35
oop_2.py
import student
 
print('{}, {}학년 {}{}번'.format(student.name, student.year, student.class_id, student.student_id))
실행 결과
이상희, 2학년 3 35

위의 세가지 예 모두 형식과 방법이 조금 다를뿐 오브젝트라는 논리적 집합을 사용하여 똑같은 데이터를 출력하는 것을 알 수 있습니다.

그럼 이번에는 파이썬의 모든 것들이 정말 오브젝트인지 그리고 그 오브젝트 안에는 뭐가 있는지 확인해 볼까요? 먼저 문자열이 정말 오브젝트인지 확인해 보죠.

oop_2.py
text = 'string'
print(dir(text))

기억하세요~!

dir()는 파이썬의 표준 내장 함수입니다. 이 함수는 인자가 없을 경우에는 모듈 레벨의 지역변수를, 인자가 있을 경우에는 인자(오브젝트)의 모든 속성과 메소드를 보여줍니다. 이 함수는 디버깅을 할 때 아주 많이 쓰이는 중요한 함수입니다.

실행 결과
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

text라는 변수에 “string”이라는 6 글자만 할당하였을 뿐인데 뭐가 이렇게 많이 출력 되나요?!?

그 이유는 이렇습니다. textstr이라는 데이터타입이 만들어낸 오브젝트이며 str 데이터타입에 정의된 모든 속성과 메소드를 상속 받았기 때문입니다.

예제를 통해서 몇가지만 확인해 보겠습니다.

oop_2.py
text = 'string'
 
print('# text의 클래스 확인')
print(text.__class__)  # <type 'str'>
 
print('\n# text가 str의 인스턴스 오브젝트인지 확인')
print(isinstance(text, str))  # True
 
print('\n# str 오브젝트의 메소드 확인')
print(text.upper())  # STRING
실행 결과
# text의 클래스 확인
<class 'str'>
 
# text가 str의 인스턴스 오브젝트인지 확인
True
 
# 메소드 확인
STRING

text의 클래스도 확인을 해봤고 메소드도 호출해 봤습니다. 함수나 모듈도 오브젝트라고 했는데 진짜인지 확인해 보도록 하죠.

oop_2.py
def my_function():
'''my_function에 대한 설명입니다~!'''
pass
 
print('# my_function의 속성 확인')
print(dir(my_function), '\n')
 
print('# my_function의 docstring 출력')
print(my_function.__doc__, '\n')
 
print('# my_function에 새로운 속성 추가\n')
my_function.new_variable = '새로운 변수입니다.'
 
print('# 추가된 속성 확인')
print(dir(my_function), '\n')
 
print('# 추가한 속성값 출력')
print(my_function.new_variable, '\n')
실행 결과
# my_function의 속성 확인
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__ha
sh__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subcl
asshook__']
 
# my_function의 docstring 출력
my_function에 대한 설명입니다~!
 
# my_function에 새로운 속성 추가
 
# 추가된 속성 확인
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__ha
sh__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subcl
asshook__', 'new_variable']
 
# 추가한 속성값 출력
새로운 변수입니다.

정말 함수도 많은 속성을 가지고 있다는 것과 임의로 속성을 추가할 수도 있다는 것까지 확인하였습니다.

이제 어느정도 감을 잡으셨으면 클래스를 사용하여 새로운 데이터타입을 만들고 그 데이터타입의 인스턴스 오브젝트를 만들어 보죠.

회사에서 직원들의 인사 데이터를 관리하기 위한 클래스를 만들어 보겠습니다.

oop_2.py
class Employee:
pass
 
emp_1 = Employee()
emp_2 = Employee()
 
print('# emp_1과 emp_2는 다른 메모리 주소값을 가진 별개의 오브젝트입니다.')
print(id(emp_1))
print(id(emp_2))
print()
 
print('# emp_1과 emp_2는 같은 클래스의 인스턴스인 것을 확인합니다.')
class_of_emp_1 = emp_1.__class__
class_of_emp_2 = emp_2.__class__
print(id(class_of_emp_1))
print(id(class_of_emp_2))
실행 결과
# emp_1과 emp_2는 다른 메모리 주소값을 가진 별개의 오브젝트입니다.
3095355928096
3095355928048
 
# emp_1과 emp_2는 같은 클래스의 인스턴스인 것을 확인합니다.
3095348371152
3095348371152

Employee라는 클래스를 정의하고 emp_1, emp_2라는 인스턴스를 만들었습니다. 그리고 id()함수를 이용하여 emp_1과 emp_2가 다른 메모리 주소값을 가진 별개의 오브젝트라는 것을 확인하였습니다. 그리고 둘다 같은 클래스의 인스턴스라는 것도 확인하였습니다.

이번에는 emp_1, emp_2 인스턴스에 변수를 추가하여 데이터를 저장해 보겠습니다.

oop_2.py
class Employee:
pass
 
emp_1 = Employee()
emp_2 = Employee()
 
# 인스턴스 변수에 데이터 저장
emp_1.first = 'Sanghee'
emp_1.last = 'Lee'
emp_1.email = 'sanghee.lee@schoolofweb.net'
emp_1.pay = 50000
 
emp_2.first = 'Minjung'
emp_2.last = 'Kim'
emp_2.email = 'minjung.kim@schoolofweb.net'
emp_2.pay = 60000
 
# 인스턴스 변수 데이터에 엑세스
print(emp_1.email)
print(emp_2.email)
실행 결과
sanghee.lee@schoolofweb.net
minjung.kim@schoolofweb.net

인스턴스에 데이터를 저장하고 엑세스해 보았습니다. 그런데 위의 코드는 잘 못된 코드입니다. 위의 코드처럼 인스턴스 변수를 하나 하나 수동으로 할당하면 클래스를 사용하는 의미가 없습니다. init 메소드를 사용하여 인스턴스를 생성할 때 필요한 데이터를 할당하겠습니다.

노트

init 메소드는 “이니셜라이져”라고도 부르고 다른 언어에서는 “컨스트럭터”라고도 부릅니다. 이 메소드는 인스턴스가 생성될때 자동으로 호출되며 호출되는 순간 자동으로 인스턴스 오브젝트를 self라는 인자로 받습니다. 그리고 이 이니셜라이져를 사용해서 인스턴스 생성시에 여러가지 데이터를 인자로 전달하여 오브젝트안에 초기 데이터로서 저장할 수가 있습니다.

oop_2.py
 
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'
 
 
emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)
 
print(emp_1.email)
print(emp_2.email)
 
# emp_1의 풀네임 출력
print('{} {}'.format(emp_1.first, emp_1.last))
실행 결과
sanghee.lee@schoolofweb.net
minjung.kim@schoolofweb.net
Sanghee Lee
클래스의 장점을 살려서 간결한 코드가 만들어졌습니다.
 
그런데 마지막줄에 풀네임을 출력한 코드를 주십시오. 코드는 회사에서 어떤 직원의 이름을 물어 때, “이름이 뭔가요? 퍼스트네임, 라스트네임의 순서로 말해주세요.”라고 요청하는 것과 같습니다. 100명의 직원의 이름을 물어 보려면 아프겠습니다. 모든 직원들이 “이름이 뭔가요?”라는 질문을 받으면 어떤 형식으로 대답해야 하는지 알았으면 좋겠네요. 그렇게 만들어 보죠.
oop_2.py
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'
 
    def full_name(self):
        return '{} {}'.format(self.first, self.last)
 
 
emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)
 
# emp_1의 풀네임 출력
print(emp_1.full_name())
실행 결과
Sanghee Lee

처음 클래스를 사용하는 사람들이 아주 많이 하는 실수가 있습니다. 그게 뭐냐면, 메소드를 정의할 때 self 인수를 잊어버리는 것입니다. 그럼 어떻게 될까요? 한번 보죠.

oop_2.py
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'
 
    def full_name():  # <--- self가 없습니다.
        return '{} {}'.format(self.first, self.last)
 
 
emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)
 
# emp_1의 풀네임 출력
print(emp_1.full_name())
실행 결과
Traceback (most recent call last):
File "C:\Users\CURTIS\Dev\Python\oop_2.py", line 16, in <module>
print(emp_1.full_name())
TypeError: full_name() takes 0 positional arguments but 1 was given
“TypeError: full_name() takes 0 positional arguments but 1 was given”

옹… 파이썬 인터프레터가 뭔가 알 수 없는 얘기를 하는데 무슨 뜻일까요? full_name 메소드는 인자를 안 받는데 왜 1 개를 줬냐고하네요. emp_1.full_name() 이렇게 아무런 인자 없이 호출을 했는데 말이죠. 뭘까요?!? 그건 위에서도 설명했듯이 인스턴스의 메소드를 호출하면 인스턴스 자기 자신인 self가 첫번째 인자로 자동 전달되기 때문입니다. 다음 예제를 보시면 조금 이해가 쉬울 겁니다.

oop-2.py
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'
 
    def full_name(self):
        return '{} {}'.format(self.first, self.last)
 
 
emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)
 
# 클래스를 통해서 full_name 메소드 호출
print(Employee.full_name(emp_1))
실행 결과
Sanghee Lee

마지막행의 코드를 보시면 클래스를 통해서 메소드를 실행하였는데, 이런 경우에는 클래스는 어떤 인스턴스의 메소드를 호출해야 하는지 모르기 때문에 대상이 될 인스턴스를 인자로 전달해야 합니다. 사실 emp_1.full_name()를 실행하면 백그라운드에서는 Employee.full_name(emp_1)가 실행되는 것입니다.

이번 강좌는 여기서 마치고 다음 강좌에서 “클래스 변수”를 공부하며 클래스와 인스턴스의 차이점에 대해서 더 공부해 보겠습니다.