파이썬 강좌 – 클래스와 객체 ~ 변수를 사람으로 진화시키기

  1. 프롤로그
  2. 개발 첫걸음
    1. 컴퓨터 구성요소 - 컴퓨터는 어떤 걸 할 수 있나?
    2. 개발과 관련된 용어
    3. 파이썬의 선택 - 왜 파이썬인가?
    4. 파이썬 설치 - Hello World 출력하기
    5. Visual Studio Code 의 편리한 기능
    6. REPL과 콘솔 창 - 파이썬 동작시키기
  3. 파이썬 기초
    1. 기초 입출력 - 소통하기
    2. 변수와 대입 - 기억하기
    3. 연산자 - 계산하기
    4. 조건문 - 분기를 만들기
    5. 반복문 - 비슷한 작업을 반복하기
    6. 변수와 리스트 - 비슷한 변수들을 묶기
    7. for, range - 리스트의 항목을 다루기
    8. 파이선 기초 꿀팁
      1. 함수와 메소드의 호출 - 편리한 기능 이용하기
      2. 모듈 설치와 사용 - 유용한 기능 끌어다 쓰기
      3. 문자열 - 텍스트 다루기
  4. 파이썬 중급
    1. 정규표현식 - 문자열을 검색하고 치환하기(작성중)
    2. 함수를 직접 만들기 - 자주 쓰는 기능을 묶기
    3. 딕셔너리, 튜플, 세트 - 변수를 다양한 방법으로 묶기
    4. 클래스와 객체 - 변수를 사람으로 진화시키기
    5. 상속 - 클래스를 확장하기
    6. 파이썬 중급 꿀팁
      1. 코드를 작성하는 사람의 의도(작성중)
      2. 정체성과 동질성 - 객체의 성질(작성중)
      3. 명령문, 표현식 - 문법을 이루는 것들 (작성중)
      4. 슬라이싱 - 리스트를 갖고 놀기
  5. 파이썬 고급
    1. 예외와 에러 - 예상치 못한 상황에 대응하기
    2. 특별 메소드와 연산자 - 파이썬의 내부 작동방식 이해하기
    3. 다양한 함수 인수 - 유연한 함수 만들기
    4. 시퀀스와 반복자 - 반복과 순회를 자유자재로 다루기
    5. 변수의 범위 - 이름 검색의 범위
  6. 파이썬 심화
    1. 제너레이터와 코루틴 -
    2. async와 await
    3. 데코레이터 - 함수의 기능을 강화하기
    4. 객체로서의 클래스 - 클래스를 동적으로 정의하기
  7. 파이썬 프로젝트 실습
    1. 원카드 게임 만들기 (1)
    2. 원카드 게임 만들기 (2)
  8. 실전 (파이썬 외적인 것들)
    1. 유니코드 - 컴퓨터에서 문자를 표기하는 방법
    2. html, css, 인터넷 - 자동화 첫 걸음 내딛기
    3. 네트워크 - 인터넷으로 통신하는 방법
    4. 문서 - 문맥을 읽어보기

많아지는 변수와 함수들

신조어 중 클라스 라는 말이 종종 등장하죠. 격이 다르다, 차원이 다르다 라는 의미로 "크~ 클라스 오지구연"이라고 내뱉곤 합니다. 여기서의 클라스는 우리가 지금부터 이야기하려는 클래스가 맞습니다. 교실의 classroom까지 떠올릴 필요는 없습니다. 😂

어찌 되었건 우리는 클래스라는 새로운 개념을 배울 예정입니다. 본격적으로 배우기 전에 클래스가 왜 필요한지 가상의 예시를 통해 천천히 알아가보죠.

슈퍼파워 게임 회사는 현재 RPG 게임을 만들고 있다. 지금은 적 몬스터를 만들고자 한다. 적 몬스터의 스펙은 다음과 같다.

  1. 적 몬스터는 저마다 공격력, 체력을 가지고 있다.
  2. 적 몬스터가 아군을 공격하면 몬스터의 공격력 만큼 우리가 피해를 입는다.
  3. 아군이 적 몬스터를 공격하면 그만큼 피해를 준다.
  4. 적 몬스터의 체력이 다 되면 몬스터는 사망한다.

위 상황을 파이썬 코드로 만들어봅시다. 실제로 싸우는 상황도 코드로 묘사하여 봅시다.

attack = 10 # 공격력
health = 125 # 체력

# 몬스터가 아군을 공격할 때
def monsterAttack():
    print("몬스터가 아군에게 " + str(attack) + "만큼 공격했다!")
    print("-----------------")

# 아군이 몬스터를 공격할 때
def monsterHit(damage):
    global health
    print("아군이 몬스터에게 " + str(damage) + "의 데미지를 주었다!")
    if health < 0:
        print("몬스터는 이미 시체이다.....")
    else:
        health -= damage
        if health < 0:
            print("몬스터가 죽었다!")
        else :
            print("현재 몬스터의 체력은 " + str(health) + "이다.")
    print("-----------------")

monsterAttack()
monsterHit(30)
monsterHit(90)
monsterAttack()
monsterHit(20)
monsterHit(5)
몬스터가 아군에게 10만큼 공격했다!
-----------------
아군이 몬스터에게 30의 데미지를 주었다!
현재 몬스터의 체력은 95이다.
-----------------
아군이 몬스터에게 90의 데미지를 주었다!
현재 몬스터의 체력은 5이다.
-----------------
몬스터가 아군에게 10만큼 공격했다!
-----------------
아군이 몬스터에게 20의 데미지를 주었다!
몬스터가 죽었다!
-----------------
아군이 몬스터에게 5의 데미지를 주었다!
몬스터는 이미 시체이다.....
-----------------

global 키워드는 해당 함수의 바깥에 전역으로 정의되어 있는 변수를 이용하겠다는 선언입니다. 자세한 내용은 다음을 참조하세요. (추가 예정)

간단한 로직인 것 같으면서도 생각보다 긴 코드가 만들어졌습니다. 자 첫번째 몬스터 구상을 완성했군요. 하지만 게임에는 몬스터가 단 한마리만 등장하는 법은 없죠. 3마리까지 추가한다고 칩시다. 같은 변수명을 쓰면 겹치게 되니 변수명 뒤에 숫자를 붙여서 몬스터를 구분할 수 있도록 해요. 예감이 되시나요? 코드가 아주 길어질 게 분명합니다.. 일단 해 봅시다.

attack1 = 10
health1 = 125
attack2 = 20
health2 = 50
attack3 = 40
health3 = 300

def monsterAttack1():
    print("몬스터가 아군에게 " + str(attack1) + "만큼 공격했다!")
    print("-----------------")

def monsterHit1(damage):
    global health1
    print("아군이 몬스터에게 " + str(damage) + "의 데미지를 주었다!")
    if health1 < 0:
        print("몬스터는 이미 시체이다.....")
    else:
        health1 -= damage
        if health1 < 0:
            print("몬스터가 죽었다!")
        else :
            print("현재 몬스터의 체력은 " + str(health1) + "이다.")
    print("-----------------")

def monsterAttack2():
    print("몬스터가 아군에게 " + str(attack2) + "만큼 공격했다!")
    print("-----------------")

def monsterHit2(damage):
    global health2
    print("아군이 몬스터에게 " + str(damage) + "의 데미지를 주었다!")
    if health2 < 0:
        print("몬스터는 이미 시체이다.....")
    else:
        health2 -= damage
        if health2 < 0:
            print("몬스터가 죽었다!")
        else :
            print("현재 몬스터의 체력은 " + str(health2) + "이다.")
    print("-----------------")

def monsterAttack3():
    print("몬스터가 아군에게 " + str(attack3) + "만큼 공격했다!")
    print("-----------------")

def monsterHit3(damage):
    global health3
    print("아군이 몬스터에게 " + str(damage) + "의 데미지를 주었다!")
    if health3 < 0:
        print("몬스터는 이미 시체이다.....")
    else:
        health3 -= damage
        if health3 < 0:
            print("몬스터가 죽었다!")
        else :
            print("현재 몬스터의 체력은 " + str(health3) + "이다.")
    print("-----------------")

monsterAttack1()
monsterHit1(30)
monsterHit1(90)
monsterAttack1()
monsterHit2(30)
print("필살기!! 모든 몬스터에게 1000의 공격!!")
monsterHit1(1000)
monsterHit2(1000)
monsterHit3(1000)
스터가 아군에게 10만큼 공격했다!
-----------------
아군이 몬스터에게 30의 데미지를 주었다!
현재 몬스터의 체력은 95이다.
-----------------
아군이 몬스터에게 90의 데미지를 주었다!
현재 몬스터의 체력은 5이다.
-----------------
몬스터가 아군에게 10만큼 공격했다!
-----------------
아군이 몬스터에게 30의 데미지를 주었다!
현재 몬스터의 체력은 20이다.
-----------------
모든 몬스터에게 1000의 공격!!
아군이 몬스터에게 1000의 데미지를 주었다!
몬스터가 죽었다!
-----------------
아군이 몬스터에게 1000의 데미지를 주었다!
몬스터가 죽었다!
-----------------
아군이 몬스터에게 1000의 데미지를 주었다!
몬스터가 죽었다!
-----------------

핫하.. 귀찮아서 필살기를 써서 모든 몬스터를 죽여버렸습니다.

어찌어찌 해결이 되었나요? 근데 단 3개의 몬스터를 만드는데도 60줄 가까이 코드를 작성했습니다. 하지만 몬스터가 10마리가 필요하다면? 그 뿐만 아니라 몬스터에게 이름이 주어질 수도 있고, 방어력이 생길 수도 있습니다. 어쩌면 몬스터가 마법을 쓸 수도 있죠! 골치가 아파집니다. 모든 몬스터에게 일일히 변수와 함수를 추가할 생각을 하니 머리가 지끈거리지 않나요?


클래스 등장

이제 클래스가 등장합니다. 예시를 우선 봅시다. ##1## 등의 번호를 누르면 해당 설명으로 곧장 이동합니다.

class Monster: ##a_1##
    def __init__(self, attack, health): ##a_2##
        self.attack = attack ##a_3##
        self.health = health
    
    def doAttack(self): ##a_4##
        print("몬스터가 아군에게 " + str(self.attack) + "만큼 공격했다!") ##a_5##
        print("-----------------")

    def hit(self, damage): ##a_6##
        print("아군이 몬스터에게 " + str(damage) + "의 데미지를 주었다!")
        if self.health <= 0:
            print("몬스터는 이미 시체이다.....")
        else:
            self.health -= damage
            if self.health <= 0:
                print("몬스터가 죽었다!")
            else :
                print("현재 몬스터의 체력은 " + str(self.health) + "이다.")
        print("-----------------")

mon1 = Monster(10, 125) ##a_7##
mon2 = Monster(20, 50)
mon3 = Monster(40, 300)

mon1.doAttack() ##a_8##
mon1.hit(30)
mon1.hit(90)
mon1.doAttack()
mon2.hit(30)
print("필살기!! 모든 몬스터에게 1000의 공격!!")
for mon in [mon1, mon2, mon3]: ##a_9##
    mon.hit(1000)

천천히 하나하나 설명할게요.

a_1(1.) 클래스 정의

class Monster:

Monster라는 이름의 클래스(class) 를 정의합니다. class라는 새로운 키워드가 등장했고, 함수 정의와 마찬가지로 콜론(:)으로 하여 블록을 만들어 Monster 클래스의 자세한 내용을 써넣을 수 있도록 합니다.

a_2(2.) 생성자 정의

    def __init__(self, attack, health):

__init__이라는 이름의 메소드(method) 를 정의합니다. 메소드는 객체(object) 의 동작을 만들 때 이용합니다. 이 요상하게 생긴 메소드의 이름은 언더바(_) 두개, init, 다시 언더바 두개로 이루어집니다.

메소드를 정의하는 방법은 함수와 완전히 동일하나, 첫번째 인수를 무조건 self 로 지정합니다. 계속해서 설명하겠지만, 메소드들이 호출될 때 제일 앞에 있는 self 인수는 자동으로 채워집니다. 그래서 만약 인수를 하나도 받지 않는 메소드를 정의하고 싶다면 self 하나만 작성하고, 인수를 위 예제처럼 attack, health 두 개를 받아 이용하고 싶다면 self를 먼저 적은 후 attack, health를 적어서 총 3개의 인수를 받도록 정의합니다.

self는 특별한 존재입니다. Monster 클래스를 통해서 어떤 객체가 만들어지면, 그 객체의 자기 자신을 뜻하게 됩니다.

__init__ 메소드는 생성자(Constructor) 라는 특별 메소드(Special Method) 입니다. 특별 메소드는 생성자 말고도 다양한 종류가 있는데요, 차차 알아보면 됩니다. 생성자를 특히 먼저 배우는 이유는, 객체가 생성될 때 객체가 사용할 속성(Attribute) 을 초기화하는 역할을 맡은 메소드이기 때문입니다.

클래스를 정의할 때 __init__ 메소드를 필수로 정의해야 하는 건 아닙니다.

a_3(3.) 속성 초기화

        self.attack = attack

self.attack 이라는 표현이 등장합니다. 여기서 점(.)은 왼쪽에 있는 객체가 소유하고 있는, 혹은 포함하고 있는 무언가를 가져오겠다는 뜻입니다. 즉 여기서는 self라는 객체가 가지고 있는, attack이라는 속성에 함수의 인수로 들어온 attack을 대입하겠다는 뜻이지요. 두 attack은 서로 다르다는 점을 유념해주시기 바랍니다.

우리가 변수를 정의할 때 단지 대입문을 통하여 정의했던 것과 마찬가지로 속성 정의도 대입문으로 진행됩니다. 간단하게 self.attack = attack으로 이 객체는 attack이라는 속성을 가질 수 있게 되었습니다.

a_4(4.) 인수가 없는 일반 메소드 정의

    def doAttack(self):

doAttack 이라는 이름을 가진 메소드를 정의합니다. 이 메소드는 언더바가 있지 않으므로 특별 메소드에 포함되지 않습니다. 인수는 self 하나를 가지고 있습니다.

a_5(5.) 속성 접근

        print("몬스터가 아군에게 " + str(self.attack) + "만큼 공격했다!")

self.attack을 통해 이 객체가 갖고 있는 속성에 접근합니다.

a_6(6.) 인수가 있는 일반 메소드 정의

    def hit(self, damage):

hit이라는 이름을 가진 메소드를 정의합니다. 마찬가지로 특별 메소드가 아니지요. 이 메소드는 self, damage와 같이 두 개의 인수를 받고 있습니다. 이 메소드의 행동은 다음과 같습니다. 객체의 health 속성이 0 이하일 경우 이미 죽어있다는 메시지를 출력하고, health가 남아있다면 damage만큼 차감시켜 여전히 살아있는지 판단합니다. 아직 살아있다면 남은 health를 출력합니다.

a_7(7.) 객체 생성

mon1 = Monster(10, 125)
mon2 = Monster(20, 50)
mon3 = Monster(40, 300)

우리가 클래스를 만들었다고 해서 곧바로 클래스의 기능을 쓸 수 있는 것은 아닙니다. 클래스를 정의해놓고, 그 클래스를 이용해서 객체를 만들어야 비로소 클래스가 가지고 있는 메소드와 속성을 이용할 수 있습니다. 클래스는 일종의 입니다. 틀을 통해 객체를 생성해내는 것이지요. (우리가 함수를 일단 정의해놓고 나중에 쓰고 싶을 때 함수를 호출하는 것과 비슷한 원리입니다.)

Monster(10, 125)를 호출하는 순간 Monster 클래스의 __init__ 메소드, 즉 생성자가 호출됩니다. 생성자는 앞서 살펴보았던 것처럼 return 문이 별도로 없지만, 특별하게 작동하여 Monster 객체를 만들어 반환(return)합니다. mon1Monster 클래스의 인스턴스(instance) 입니다.

앞서 생성자를 정의할 때 self, attack, health와 같이 세 개의 인수를 받았습니다. 하지만 Monster(10, 125)에서는 두 개의 인수만을 이용했습니다. 메소드는 함수와는 다르게 호출할 때 첫번째 인수는 무조건 자동으로 채워집니다. 바로 ‘자기 자신’으로요.

mon1, mon2, mon3이 각각 호출하는 생성자는 동일한 메소드입니다. 메모리에 오직 하나로 존재하는 완전히 같은 함수를 호출하고 있습니다. 하지만 호출할 때마다 self 인수에는 각각의 객체로 자동으로 채워집니다. 그리하여 생성자 내부에서의 표현은 self.attack으로 동일했지만, attack 속성은 mon1, mon2, mon3 각각 개별적 및 독립적으로 존재할 수 있게 됩니다.

a_8(8.) 메소드 호출

mon1.doAttack()

mon1 이 지니고 있는 doAttack 메소드를 호출합니다. doAttack 메소드 내부에서의 selfmon1을 가리키게 되어 mon1attack 속성을 출력하게 됩니다.

a_9(9.) 한 번에 메소드 접근

for mon in [mon1, mon2, mon3]:
    mon.hit(1000)

몬스터들은 모두 같은 Monster 클래스로 생성하였으므로 지니고 있는 메소드명과 속성명 또한 동일합니다. 이를 인터페이스(Interface)가 같다 라고도 표현할 수 있는데요, 이 또한 뒤에 계속해서 설명할 수 있도록 하겠습니다.

mon은 차례로 mon1, mon2, mon3을 가리키게 되고, 몬스터 모두 hit이라는 메소드를 가지고 있으므로 문제 없이 작동합니다. 몬스터가 더 많아진다 하더라도 리스트에 넣어 관리한다면 편리하게 일괄적으로 공격받게 할 수 있겠지요.

결과는 동일합니다.

몬스터가 아군에게 10만큼 공격했다!
-----------------
아군이 몬스터에게 30의 데미지를 주었다!
현재 몬스터의 체력은 95이다.
-----------------
아군이 몬스터에게 90의 데미지를 주었다!
현재 몬스터의 체력은 5이다.
-----------------
몬스터가 아군에게 10만큼 공격했다!
-----------------
아군이 몬스터에게 30의 데미지를 주었다!
현재 몬스터의 체력은 20이다.
-----------------
필살기!! 모든 몬스터에게 1000의 공격!!
아군이 몬스터에게 1000의 데미지를 주었다!
몬스터가 죽었다!
-----------------
아군이 몬스터에게 1000의 데미지를 주었다!
몬스터가 죽었다!
-----------------
아군이 몬스터에게 1000의 데미지를 주었다!
몬스터가 죽었다!
-----------------

패러다임 분리

클래스의 장점은 명확합니다. 똑같이 동작하는 부분과 다르게 동작하는 부분을 명확하게 분리시켜서 코드 작성을 훨씬 손쉽게 할 수 있습니다. 위 Monster 클래스를 이용하여 세 개의 인스턴스를 만들어 보았는데요, 각각 몬스터의 같은 점과 다른 점은 다음과 같습니다.

같은 점 다른 점
데미지 받는 동작, 공격을 주는 동작, attackhealth라는 속성의 용도 각 몬스터의 attack, health 실제 값

냉혹한 프로그래밍의 세계에서는, 중복으로 코딩하는 것을 손쉽게 허용하지 않습니다. 작성할 때의 귀찮음도 크겠지만, 무엇보다도 수정해야 할 사항이 있을 때 일일히 변경해야 한다는 게 너무 번거롭기 때문입니다. 클래스의 장점이 조금은 이해가 되셨나요?


이제 용어를 정리할 시간입니다. 위 예제를 설명하면서 수많은 용어가 등장했습니다.

꼭 기억해요!

  1. 모든 객체(Object) 에게는 저마다의 클래스(Class) 가 존재합니다. 다른 말로 표현하면 클래스는 일종의 틀이고, 클래스를 통해 객체를 찍어낼(생성할) 수 있습니다. 클래스를 다른 말로 타입(Type) 이라고도 합니다.
  2. 메소드(Method) 란, 클래스에서 정의되는 함수로, 객체의 동작을 표현합니다.
  3. 속성(Attribute) 이란, 클래스에서 정의되는 변수로, 각 객체의 고유값을 나타냅니다.
  4. 모든 메소드는 반드시 첫번째 인수로 self 를 가집니다. 메소드를 호출할 때에 self는 ‘자기 자신’으로 자동으로 채워지므로 self가 없다 상정하고 메소드를 호출합니다.
  5. 특별 메소드(Special Method) 란, 파이썬에서 특별하게 취급되는 메소드를 말합니다. 종류는 굉장히 많지만 명확히 정해져 있고, 각자 특별한 역할이 있습니다. 이름이 앞 뒤로 언더바 두개(__)로 구성되어 다른 메소드와 구별됩니다. 특별 메소드와 관련해서는 다음을 참조하세요. (추가 예정)
  6. 생성자(Constructor) 란, 특별 메소드 중 하나입니다. 객체를 새로이 생성할 때 사용되는 메소드이며, 객체를 초기화하는 역할을 가지고 있습니다. 별도의 return 문은 없습니다. 클래스명(인수1, 인수2, ...)와 같이 호출합니다.
  7. 본래 인스턴스(Instance) 란, 클래스가 설계도 라는 개념으로 이야기할 때 그 반대급부인 구현된 실체 라는 대비되는 개념으로 사용됩니다. 파이썬에서는 객체와 거의 동일한 뜻으로 사용되므로 혼용하여도 무방합니다.

용어 정리를 간단하게 했습니다. 이 정도면 간단한 겁니다. 😉 이제 다시 위의 몬스터 예제를 읽어내려서 이해가 가지 않는 지점이 혹시나 있는지 체크해 주세요. 이해가 다 되었다면 다음으로 진행해주시기 바랍니다.

세상에는 용도와 목적에 따라 아주 다양한 프로그래밍 언어가 생겨났습니다. 하지만 더 효율적이고 범용적인 프로그래밍 언어 사용을 위해 언어가 어떤 특성을 지니고 있는지, 어떤 개념을 이용하는지에 대해서 정리해야 할 필요성이 생겼습니다. 그리하여 프로그래밍 언어 패러다임이 어느정도 정립되는데요, 그 중 객체 지향 프로그래밍(OOP) 은 아주 유서깊은 패러다임입니다. OOP는 객체라는 추상적인 존재를 클래스와 클래스의 복잡한 관계를 통해 구현하고자 합니다. OOP에 대항하는 또 다른 패러다임도 존재하지만, 이미 OOP는 프로그래머와 프로그램 그 자체에 깊숙히 자리잡고 있습니다. OOP가 등장하고 한참 뒤에 태어난 파이썬은, 물론 개발에 있어 새로운 패러다임을 창조해내기도 하지만, 기존의 강력한 패러다임을 구현할 의무도 지니게 된 셈입니다.


개념 추가 정리

핵심 개념과 더불어서, 파이썬에서 클래스를 사용할 때의 유의점과 참고할 만한 사항을 정리합니다. 선 요약하면 다음과 같습니다.

  1. 클래스의 이름은 대문자로, 메소드와 속성의 이름은 소문자로 시작하도록 합니다.
  2. 생성자를 정의했다면 거기에 맞춰 객체를 생성해야 합니다.
  3. 메소드와 속성은 이름 중복이 허용되지 않습니다.
  4. 인터페이스(Interface)가 같다 라는 말은 파이썬에서는 쉽게 얘기하여 서로 다른 객체가 이름이 같고 기능이 비슷한 메소드를 가지고 있다라고 말할 수 있습니다.
  5. 파이썬의 세계에서 모든 변수(variable)는 객체(Object) 입니다.

클래스의 이름은 대문자로, 메소드와 속성의 이름은 소문자로 시작하도록 합니다.

스타일에 관한 내용입니다. 개발자 조상님들은 대대로 클래스 명에는 대문자로, 메소드와 속성 명에는 소문자로 시작하도록 해왔습니다. 유명한 파이썬 모듈도 모두 그렇게 해왔습니다. 프로그래머간의 약속입니다.

예외가 있습니다. 파이썬에서 기초 자료형이라 불리우는 것들은 소문자로 시작하는 클래스입니다. int, float, str, list 등이 있습니다.

power라는 가상의 모듈을 불러와 사용하는, 김 부장님이 쓴 코드를 맥락없이 마주하게 되었다고 가정합시다.

import power
a = power.man.Data()

우리가 알 수 있는 정보는 극히 적습니다. power.man이 뭔지는 모릅니다. 하지만 이 함수는 power.man이라는 것 안에 있는 Data()라는 메소드를 호출한 것이 아닌, power.man이라는 것 안에 정의되어 있는 Data라는 클래스의 생성자를 호출하여 새 객체를 만들었음을 시사하고 있습니다. (클래스의 정의는 어디서든지 가능합니다.) 우리가 a를 어떻게 사용하는지 파악하려면, Data 클래스가 어떤 속성과 메소드를 정의해놓았는지 찾아보면 됩니다.

또 다른 예제를 봅시다. 이름 모를 나그네가 소리소문없이 코드만 쓰고 떠나갔습니다.

# This function returns Data.
def getSomething(index=0 ... 후략
... (중략)

함수 정의 앞에 간단하게 주석으로 This function returns Data.라고 설명해놓았네요. 근데 대소문자 구분이 뭔가 보이시나요? 단순히 data가 아닌 Data를 반환한다고 합니다. 이게 어떤 의미일까요? 바로 Data라는 클래스를 기반으로 만들어진 객체를 반환한다는 사실을 함축적으로 표현하고 있습니다.

코드 작성에서 ‘다른 사람’이라 함은 과거의 나, 미래의 나가 될 수 있다는 사실을 항상 유념해주세요. 옛날에 쓴 코드를 왜 이렇게 썼는지 현재의 내가 이해할 수 없을 수도 있고, 현재의 내가 당연하게 썼던 코드를 미래의 내가 왜 이렇게 썼는지 도무지 이해할 수 없을 수도 있습니다. 상식을 고수하는 건 스스로에게도 좋습니다.


생성자를 정의했다면 거기에 맞춰 객체를 생성해야 합니다.

class Car:
    def __init__(self, name):
        self.name = name

kiss = Car()
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/test.py", line 5, in <module>
    kiss = Car()
TypeError: __init__() missing 1 required positional argument: 'name'

에러가 발생합니다. 생성자가 1개의 인수를 필요로 하지만 위 예제에서는 생성자에 아무런 인수도 넘겨주지 않아 에러가 발생했습니다.


class Car:
    def __init__(self, name):
        self.name = name

kiss = Car('power', 31)
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/test.py", line 5, in <module>
    kiss = Car('power', 31)
TypeError: __init__() takes 2 positional arguments but 3 were given

클래스는 동일하고 이번에는 인수를 2개 넘겨주었는데요, 마찬가지로 개수가 맞지 않아 에러가 발생했습니다.


메소드와 속성은 이름 중복이 허용되지 않습니다.

class Gun:
    def __init__(self, bang):
        self.bang = bang

    def bang(self):
        print("빵야빵야!")

kiss = Gun('뱅뱅')
kiss.bang()
  File "c:/Users/tooth/Desktop/test.py", line 9, in <module>
    kiss.bang()
TypeError: 'str' object is not callable

str 객체를 호출할 수 없다고 에러가 뜹니다. 메소드와 속성의 이름이 bang으로 같으므로 생성자가 불리우는 순간 메소드였던 self.bang은 사라지고 속성으로서 덮어쓰여집니다. kiss.bang 은 메소드가 아니라 속성인 셈이죠. 속성을 호출하려고 하니 에러가 뜰 수 밖에요.


인터페이스가 같다 라는 말은 파이썬에서는 쉽게 얘기하여 서로 다른 객체가 이름이 같고 기능이 비슷한 메소드를 가지고 있다라고 말할 수 있습니다.

간단히 예제를 보고 설명합시다.

class Woman:
    def cry(self):
        print("훌쩍훌쩍..")

class Man:
    def cry(self):
        print("으흐흑..")
        
class Baby:
    def cry(self):
        print("응애응애!!")

people = []
people.append(Woman())
people.append(Man())
people.append(Baby())
people.append(Baby())
people.append(Man())
people.append(Woman())
people.append(Woman())
for person in people:
    person.cry()
훌쩍훌쩍..
으흐흑..
응애응애!!
응애응애!!
으흐흑..
훌쩍훌쩍..
훌쩍훌쩍..

Woman, Man, Baby라는 3개의 클래스를 정의했습니다. 이 클래스들은 완전히 다르지만, 오로지 공통점이라고는 cry라는 메소드를 정의해놓은 것 뿐입니다. 그러고는 people이라는 리스트를 만들고 거기에 Woman, Man, Baby 객체를 생성하여 직접 넣어주고 있습니다. for 문에서는 people 안에 있는 요소를 순회하며 cry 메소드를 호출하고 있습니다.

한번 더 강조하지만 세 개의 클래스들은 완전히 독립된 클래스입니다. 피가 하나도 섞이지 않은 완전 남이나 마찬가지인 클래스들이죠. 하지만 이 클래스를 이용하는 방법은 우연찮게 같았습니다. 이러한 상황 속에서 우리는 두 가지 새삼스러운 사실을 다시 한번 확인할 수 있습니다.

첫째, 서로 다른 객체가 이름이 같고 기능이 비슷한 메소드를 가지고 있다면 이 객체를 사용하는 방법은 동일하다는 점입니다. 아까 전에 이를 인터페이스가 같다 고 이야기하였죠. 반대로 이야기하면 이 객체들이 운다(cry)는 행위의 인터페이스를 제공한다고도 말할 수 있습니다. 어떤 객체가 인수를 하나도 받지 않고 우는 소리를 출력하는 메소드를 가지고 있다면 그 객체는 울 수 있는 객체이며, 사용법과 용도는 같기 때문에 코드의 사용자 입장에서는 cry 메소드 내부가 어떻게 되어 있는지 신경쓰지 않고 코드를 사용할 수 있습니다. 어떤 메소드의 내부가 어떻게 구현되어 있는지 신경쓰지 않아도 된다는 점은 객체와 클래스를 다룰 때 중요한 특징 중 하나입니다.

둘째, 인터페이스가 같다고 판단하는 것은 또 다른 차원의 문제라는 점입니다. 위 코드에서는 단순히 for 문을 돌려서 cry 메소드를 호출했습니다. 객체가 cry 메소드를 가지고 있는지 체크하는 작업은 없었지요. 만약 Baby 클래스의 cry 메소드가 아래와 같이 정의되어 있었다면 어떨까요?

(중략)
class Baby:
    def cry(self, isHungry):
        if isHungry:
            print("밥줘!!")
        else
            print("응애응애!!")
(후략)

결과는 다음과 같습니다.

훌쩍훌쩍..
으흐흑..
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/coding-class/oh/191201/test.py", line 25, in <module>
    person.cry()
TypeError: cry() missing 1 required positional argument: 'isHungry'

Baby 클래스의 cry 메소드만 취하는 인수가 달라졌습니다. 이제 더이상 인터페이스가 같다 라고 말할 수 없습니다. 왜냐하면 사용하는 방법이 다르기 때문이지요. 이처럼 인터페이스가 다르다면 에러가 발생하는 상황은 꽤 빈번하게 발생합니다.

이러한 문제를 해결하고자 하는 두 가지 접근법이 있습니다. 하나는 그 메소드가 실제로 존재하는지 실행하기 전에 체크해보자. 이고, 또 다른 하나는 일단 그 메소드가 존재한다고 가정하고 실행해보고, 에러가 발생하면 따로 처리하자. 입니다. 파이썬은 명확하게 후자의 길을 걷고 있습니다. 파이썬은 에러 처리에 관한 기능을 강력하게 제공해주고 있지요. 이것은 예외에서 자세히 살펴보도록 합시다. (추가 예정)


파이썬의 세계에서 모든 변수는 객체 입니다.

이것은 좀 충격적인 사실입니다. class Something:으로 클래스를 정의하고 생성자를 Something() 이렇게 호출해야 비로소 객체가 생성되는 것이 아닌가요? 그냥 단순히 number = 10으로 해도 number가 객체가 된다니요? 그걸 어떻게 증명하죠?

파이썬에서는 type이라는 클래스를 제공합니다. 생성자 안에 무엇이든 넣으면 그것에 관한 클래스(타입)에 대한 정보를 알 수가 있어요. 시험 삼아 다음 코드를 실행해볼까요?

list1 = [[1,2,3],'4',5,(6,7),{8,9},{10:11, 12:13}, 14.15]
for item in list1:
    print(type(item))

import math
class Duck:
    def makeNoise(self):
        print("꽥꽥")
yellow = Duck()


list2 = [math, math.pow(2,0.5), yellow, yellow.makeNoise, yellow.makeNoise()]
for item in list2:
    print(type(item))
<class 'list'>
<class 'str'>
<class 'int'>
<class 'tuple'>
<class 'set'>
<class 'dict'>
<class 'float'>
꽥꽥
<class 'module'>
<class 'float'>
<class '__main__.Duck'>
<class 'method'>
<class 'NoneType'>

결과가 나왔습니다. 죄다 class 라고 합니다. list(리스트), str(문자열), tuple(튜플), int, set(세트), dict(딕셔너리), float 모두 클래스네요.

자주 쓰는 math 모듈 또한 module 클래스 기반이고, 우리가 직접 만든 Duck 클래스의 인스턴스인 yellow는 물론 객체겠지만, 메소드를 호출하지 않고 메소드 그 자체를 가르킨 yellow.makeNoise 또한 method라는 클래스의 객체랍니다. 더욱 더 놀라운 점은 makeNoise 메소드는 아무것도 반환하지 않아서 yellow.makeNoise()와 같이 호출하여도 아무것도 남지 않는데, 아무것도 아닌 것조차 NoneType이라는 클래스의 객체라는 점입니다.

이것이 시사하는 바는 도대체 무엇일까요?

그것은 바로 변수들을 객체로서 설명 가능하다는 사실입니다. 클래스가 같은 객체나 값은 결국 같은 용도의 메소드와 속성을 가지고 있으며, 오직 다른 것은 속성에 존재하는 값임을 알 수 있습니다. 또한 객체의 일반적인 성질을 이용할 수 있다는 것인데요, 이는 다음과 같습니다.

  1. 프로그램이 실행하는 도중에 생성할 수 있습니다.
  2. 이름으로써 접근할 수 있습니다. (변수명을 떠올리세요)
  3. 함수의 인수로 전달할 수 있습니다.
  4. 함수의 결과로 반환할 수 있습니다.

즉, 어떤 클래스를 정의해 놓았다 해도 특정 메소드를 프로그램 실행 도중에 추가하거나 삭제할 수도 있으며, 리스트나 딕셔너리에 메소드를 직접 저장할 수도 있는 등 무궁무진한 활용법을 기대해볼 수 있다는 것이죠. 심지어 메소드의 인수나 반환 값으로 함수를 받을 수도 있구요.

함수가 값이다? 무슨 말인지 알쏭달쏭할 것 같습니다. 이러한 활용법은 꽤 고급 활용법에 해당하므로 직접 구현할 일은 앞으로도 없을 수 있지만, 실제 유용한 모듈이나 라이브러리가 이러한 객체적 특성을 이용하여 편리한 기능을 제공하고 있다는 사실을 알고 있어야 제대로 된 활용을 할 수 있을 것입니다.

더 나아가기

  • 클래스의 계층 구조 – 상속 참조
  • 객체로서의 함수 (일급 함수) (추가 예정)
  • 객체로서의 클래스 (메타프로그래밍) (추가 예정)

연습 문제

  • 객체와 클래스는 무엇인가? 서로의 관계로 설명해보라.
  • 메소드는 무엇인가?
  • 속성은 무엇인가?
  • 메소드를 정의할 때 반드시 지켜야 할 규칙은 무엇인가?
  • 특별 메소드란 무엇인가?
  • 생성자의 역할은 무엇인가?
  • 인스턴스라는 말은 객체와 혼용하여도 무방한가?
  • 클래스의 이름은 어떻게 시작하는 게 좋은가?
  • 메소드와 속성의 이름 중복은 허용되는가?
  • 인터페이스가 같다 라는 말을 다르게 이야기하면 무엇인가?
  • 객체의 일반적인 성질 4가지는 무엇인가?

프로그래밍 문제

  1. 다음 코드의 결과를 예측하세요.

    class Lady:
        def __init__(self, name):
            self.name = name + "양"
    
    merry = Lady('메리')
    print(merry.name)
    
    
  2. 다음 클래스를 만드세요. Cup 클래스는 용량을 나타내는 capacity 속성을 가지려 합니다. Cup 객체가 생성될 때, 생성자를 통해 capacity를 받아들이고자 합니다.

  3. 방금 만든 Cup 클래스를 보강하세요. Cup 클래스는 현재 물의 양을 나타내는 now 속성도 만들고자 합니다. 하지만 이 now는 객체가 생성될 때 0으로 초기화됩니다.

  4. Cup 클래스를 계속해서 보강하고자 합니다. Cup 객체가 생성될 때 ~~ 용량 만큼의 컵이 생성되었습니다라는 메세지를 print 하고자 합니다. 제대로 동작하는지 테스트까지 진행하세요.

  5. Cup 클래스에 fill 메소드를 추가하고자 합니다. 이 메소드는 물의 양을 인수로 받으며, 그 만큼 now 속성에 더하도록 합니다. 만약 nowcapacity보다 커지게 된다면 물이 가득찼습니다 메세지를 출력합니다. 별 다른 반환값은 없습니다. 제대로 동작하는지 테스트까지 진행하세요.

  6. Cup 클래스에 pour 메소드를 추가하고자 합니다. 이 메소드는 ~~ 만큼의 물을 부어냅니다라는 메세지를 출력하며 현재 가진 물의 양인 now을 모두 반환합니다. 동시에 now0으로 만듭니다.

  7. Cup 클래스를 완성시킨 뒤 다음 코드를 작동시켰을 때 결과가 어떤지 예측해봅시다.

    cup1 = Cup(30)
    cup2 = Cup(50)
    
    cup1.fill(20)
    cup2.fill(40)
    cup2.fill(cup1.pour())
    print(cup1.now)
    print(cup2.now)
    

프로그래밍 문제 정답

  1. 메리양

  2. 코드입니다.

    class Cup:
        def __init__(self, capacity):
            self.capacity = capacity
    
  3. 코드입니다.

    class Cup:
        def __init__(self, capacity):
            self.capacity = capacity
            self.now = 0
    
  4. 코드입니다.

    class Cup:
        def __init__(self, capacity):
            self.capacity = capacity
            self.now = 0
            print(f'용량이 {capacity} 만큼의 컵을 생성했습니다.')
    
  5. 코드입니다.

    class Cup:
        def __init__(self, capacity):
            self.capacity = capacity
            self.now = 0
            print(f'용량이 {capacity} 만큼의 컵을 생성했습니다.')
    
        def fill(self, water):
            self.now += water
            if self.now > self.capacity:
                self.now = self.capacity
                print('물이 가득찼습니다.')
    
  6. 코드입니다.

    class Cup:
        def __init__(self, capacity):
            self.capacity = capacity
            self.now = 0
            print(f'용량이 {capacity} 만큼의 컵을 생성했습니다.')
    
        def fill(self, water):
            self.now += water
            if self.now > self.capacity:
                self.now = self.capacity
                print('물이 가득찼습니다.')
    
        def pour(self):
            print(f'물을 {self.now} 만큼 부어냅니다.')
            outgoing = self.now
            self.now = 0
            return outgoing
    
  7. 결과입니다.

    용량이 30 만큼의 컵을 생성했습니다.
    용량이 50 만큼의 컵을 생성했습니다.
    물을 20 만큼 부어냅니다.
    물이 가득찼습니다.
    0
    50
    

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다

Scroll to top