파이썬 강좌 – 상속 ~ 클래스 확장하기

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

우선 상속에 알아보기 전에 새로운 클래스 객체와 클래스 메서드를 알아봅시다.

클래스 속성

지난 시간에 했던 예제를 불러와 보겠습니다.

class Monster:

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

mon1 = Monster(10, 100)
print(mon1.attack, mon1.health)

mon2 = Monster(30, 100)
print(mon2.attack, mon2.health)

10 100
30 100

앞서 클래스를 배울 때 사용했던 코드와 거의 유사합니다. 이해하시는 데는 무리가 없으시지요? __init__doAttack 두 개의 메소드가 존재하고, attackhealth 두 개의 속성이 있습니다. 이 속성들은 객체마다 분리되어 있습니다. 그림으로 표현하면 다음과 같습니다.

Monster 클래스 그림 Monster 클래스의 모습

이제, 매번 attackhealth를 지정해주기 귀찮으니 함수에 기본 값(default)을 설정해보도록 합시다. 다음과 같이 함수의 인수에 =을 붙여주면 해당 인수는 선택사항이 됩니다. 정상적으로 인수로 값이 들어온다면 그 값으로 되고, 값이 들어오지 않는다면 기본값으로 작동합니다. 아래 예제는 __init__mon1, mon2 변수를 만드는 과정만 살짝 수정했습니다.

class Monster:

    def __init__(self, attack = 10, health = 100):
        self.attack = attack
        self.health = health
    
    def doAttack(self):
        print("몬스터가 아군에게 " + str(self.attack) + "만큼 공격했다!")
        print("-----------------")

mon1 = Monster()
print(mon1.attack, mon1.health)

mon2 = Monster(30)
print(mon2.attack, mon2.health)
10 100
30 100

우리의 의도대로 잘 작동합니다.


그런데 Monster 클래스를 생성했던 시점과 달리, 기본 값 그 자체를 바꾸고 싶습니다. 왜냐하면 공격력이 30인 몬스터를 수십 마리 다시 만들고 싶은데, 그럴려면 일일히 Monster(30)이라고 코딩해야 하기 때문이죠. 머리를 굴려봅시다. 기본 값을 어떤 변수로 두어서, 이 변수를 필요할 때마다 수정한다면 어떨까요? 한 번 시도해 봅시다.

defaultAttack = 10 # 새로 추가하였습니다.
defaultHealth = 100 # 새로 추가하였습니다.

class Monster:

    def __init__(self, attack = None, health = None):
        if attack is None : 
            attack = defaultAttack
        if health is None : 
            health = defaultHealth
        self.attack = attack
        self.health = health

    def doAttack(self):
        print("몬스터가 아군에게 " + str(self.attack) + "만큼 공격했다!")
        print("-----------------")
    
mon1 = Monster()
print(mon1.attack, mon1.health)

defaultAttack = 30

mon2 = Monster()
print(mon2.attack, mon2.health)
10 100
30 100

Monster 생성자에서 attackhealth의 기본 값은 None이 되었습니다. 일단은 기본값이 존재하기 때문에 함수를 호출할 때 attackhealth의 내용을 채워넣지 않아도 되지만, 그렇게 되면 이 인수들은 None이라는 의미 없는 값을 가지게 됩니다. 이 때, if를 이용하여 외부 변수를 대입하여 줍니다.


하지만 다시 문제가 발생했습니다. defaultAttackdefaultHealth는 너무 흔한 이름이라 다른 클래스가 이 변수에 접근할 가능성이 있습니다. 우리는 이 변수들을 Monster에서만 사용하고 싶습니다. 그렇다고 만약에, defaultMonsterAttack 라고 변수 이름을 수정하고자 하니 너무 길어지는 문제가 있네요. 이 때 클래스 속성(Class Attribute) 이 등장합니다. 사용법은 간단합니다. 변수를 클래스 내부에 정의하면 됩니다! 그리고 해당 속성을 사용하려면 클래스명.속성 으로 하면 됩니다. 아래 예제를 보십시다.

class Monster:

    defaultAttack = 10
    defaultHealth = 100

    def __init__(self, attack = None, health = None):
        if attack is None : 
            attack = Monster.defaultAttack
        if health is None : 
            health = Monster.defaultHealth
        self.attack = attack
        self.health = health

    def doAttack(self):
        print("몬스터가 아군에게 " + str(self.attack) + "만큼 공격했다!")
        print("-----------------")
    
mon1 = Monster()
print(mon1.attack, mon1.health)

Monster.defaultAttack = 30

mon2 = Monster()
print(mon2.attack, mon2.health)
10 100
30 100

그림은 다음과 같습니다.

Monster 클래스 그림 (클래스 속성 추가) Monster 클래스의 모습 (클래스 속성 추가)

클래스 속성이란, 인스턴스 하나하나에 대응되는 속성이 아니라, 클래스 그 자체에 대응이 되는 속성입니다. 즉 객체가 아무리 많다 하더라도 접근되는 속성은 단 하나입니다. 클래스 속성은 다음과 같은 상황에서 유용하게 사용될 수 있으며, 그러한 상황이라는 의도를 내포하고 있습니다.

  • 인스턴스 간 정보를 공유하고자 할 때 (예: Monster 객체의 개수)
  • 클래스 자체의 정보를 제공하고자 할 때 (예: Monster 클래스의 설명)

클래스 속성은 인스턴스로도 접근이 가능합니다. 즉 Monster.defaultAttack 이 아닌 mon1.defaultAttack 으로도 접근할 수가 있습니다. 단, 이렇게 인스턴스를 통해서 접근을 한다면 "클래스 속성"이 아니라 "그 객체의 속성"으로 접근하겠다는 오해를 불러 일으킬 수 있습니다. 컴퓨터가 오해하는 게 아니라, 이 코드를 보는 다른 사용자가 그렇습니다. 그러므로 Monster.defaultAttack 이렇게 클래스 속성을 사용하겠다고 명시해줍시다.


클래스 메소드

클래스 속성과 비슷하게, 클래스 자체에 대한 동작을 넣고자 할 때 사용됩니다. 객체나 인스턴스가 필요없는 동작이라고 생각하시면 되겠습니다. 일반적으로 유틸리티성 메소드를 정의할 때 많이 사용됩니다.

class Monster:

    defaultAttack = 10
    defaultHealth = 100

    @classmethod ##a_1##
    def setDefault(cls, attack, health):
        cls.defaultAttack = attack
        cls.defaultHealth = health

    def __init__(self, attack = None, health = None):
        if attack is None : attack = Monster.defaultAttack
        if health is None : health = Monster.defaultHealth
        self.attack = attack
        self.health = health

    def doAttack(self):
        print("몬스터가 아군에게 " + str(self.attack) + "만큼 공격했다!")
        print("-----------------")
    
mon1 = Monster()
print(mon1.attack, mon1.health)

Monster.setDefault(30, 50)

mon2 = Monster()
print(mon2.attack, mon2.health)
10 100
30 50

a_1(1.) 클래스 메소드 정의하기

클래스 메소드를 정의하는데, 새로운 문법이 등장했습니다. 바로 @classmethod 인데요, 이는 데코레이터(decorator)라고 하여, 바로 아래에 등장하는 함수를 데코해주는 역할입니다. 여기서는 바로 아래의 setDefault 함수를 클래스 메소드로 사용하겠다는 뜻으로 풀이될 수 있습니다.

클래스 메소드의 큰 특징은, 첫 번째 인수로 객체가 넘어오는 게 아니라, 클래스가 넘어오게 됩니다. 즉 위에서의 cls 인수는 Monster 그 자체가 되어 cls.defaultAttack과 같이 클래스 속성에 접근할 수 있게 되는 것입니다. 이 메소드를 호출할 때에는 속성과 마찬가지로 .을 이용하여 클래스명.메소드명(인수...) 라고 작성합니다.

어차피 객체 하나하나에 구애되지 않는 메소드이니까 self를 굳이 쓰지 않아도 될 것 같습니다. 그런데 왜 클래스 정보를 받아오는 것일까요? 클래스의 상속에서 더 알아보도록 합시다.


정적 메소드 (static method)

클래스에 연결되어서 각 객체와는 독립적으로 작동하는 메소드를, 다른 프로그래밍 언어에서는 통상적으로 정적 메소드(static method)라고 불립니다. 자바와 C++에서는 static이라는 키워드를 곧장 이용하여 구현하곤 합니다.


상속의 핵심

기능을 확장하고자 한다

초심으로 돌아가서 원래의 예제를 봅시다. Monster 클래스를 한번 만들어보았습니다. 이제 Monster와 비슷하게 작동하는 Human, Dog 클래스를 새롭게 만들어서 기능을 조금씩 수정해보고자 합니다. 변경할 내용은 다음과 같습니다.

  • 사람에게는 이 사람의 출신 나라 정보가 포함될 수 있습니다. 속성을 추가해 줍시다.
  • 사람은 똑똑해서 조금 약하게 한번 더 때립니다. doAttack 메소드를 수정해줍시다.
  • 개는 짖을 줄 압니다. bark 메소드를 추가해줍니다.

그리하여 수정한 결과는 다음과 같습니다.

class Monster:

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

class Human:

    def __init__(self, attack, health, country):
        self.attack = attack
        self.health = health
        self.country = country
    
    def doAttack(self):
        print("사람이 아군에게 " + str(self.attack) + "만큼 공격했다!")
        print("사람은 똑똑해서 한번 더 " + str(self.attack * 0.5) +"만큼 공격했다!")
        print("-----------------")

class Dog:

    def __init__(self, attack, health):
        self.attack = attack
        self.health = health
    
    def doAttack(self):
        print("개가 아군에게 " + str(self.attack) + "만큼 공격했다!")
        print("-----------------")

    def bark(self):
        print("왈왈!!")


dog = Dog(20, 30)
human = Human(10, 100, "한국")
monster = Monster(5, 100)

dog.doAttack()
dog.bark()
human.doAttack()
monster.doAttack()
개가 아군에게 20만큼 공격했다!
-----------------
왈왈!!
사람이 아군에게 10만큼 공격했다!
사람은 똑똑해서 한번 더 5.0만큼 공격했다!
-----------------
몬스터가 아군에게 5만큼 공격했다!

우리의 의도대로 잘 작동합니다. 하지만 이제 시작입니다. 할 일은 점점 늘어납니다. 만약 Monster, Human, Dog 모두에게 방어력을 가지고 있는 속성 defence와 방어한다는 동작인 doDefense 메소드를 넣고 싶다면요? 뿐만 아니라 Duck, Dragon 클래스를 추가하고 싶다면요? 복사 붙여넣기 신공이 절실하다면 이제 다른 방법을 찾아볼 때 입니다. 지금은 바로 상속(inheritance)이란 것을 이용해보도록 합시다!


기본적인 사용 방법

상속(inheritance) 이란 부모-자식 개념을 클래스 개념에 추가하는 것입니다. 그래서 부모의 기능을 자식에게 상속하여 코드 중복을 없애고 유지보수를 더 원활하게 할 수 있게 됩니다. 기본적인 사용 방법은 클래스 정의 시 이름 바로 뒤에 소괄호(()) 내에 부모 클래스를 넣어주면 됩니다. 예시는 다음과 같습니다.

class 부모클래스:
    내용

class 자식클래스(부모클래스):
    내용

주요 특징은 다음과 같습니다.

  • 자식 클래스는 일단 부모 클래스의 속성과 메소드를 상속받습니다.
  • 자식 클래스는 부모 클래스에 영향을 주지 않고 자기만의 속성과 메소드를 정의할 수 잇습니다.
  • 자식 클래스는 부모 클래스의 속성 및 메소드를 덮어써서 구현할 수 있습니다. (오버라이딩)
  • 자식 클래스는 부모 클래스의 속성 및 메소드에 접근 및 호출할 수 있습니다.
  • 자동으로 자식 클래스의 메소드가 호출됩니다. (다형성)

이 두 클래스의 관계를 이야기할 때 다양한 용어가 쓰이는데요, 다음과 같습니다.

  • 부모 클래스(parent class)와 자식 클래스(child class)
  • 상위 클래스(super class)와 하위 클래스(sub class)
  • 기본 클래스(base class)와 파생 클래스(derived class)

구현 예

그래서 위 Monster, Human, Dog 클래스의 상위 클래스인 Unit 클래스를 만들어서 공통적인 부분을 묶어봅시다.

class Unit:
    typeString = "유닛"
    def __init__(self, attack, health):
        self.attack = attack
        self.health = health
    
    def doAttack(self):
        print(f"{self.typeString}이(가) 아군에게 {self.attack} 만큼 공격했다!")
        print("-----------------")
        

class Monster(Unit):
    typeString = "몬스터"


class Human(Unit):
    typeString = "사람"

    def __init__(self, attack, health, country):
        super().__init__(attack, health)
        self.country = country
    
    def doAttack(self):
        print(f"{self.typeString}이 아군에게 {self.attack}만큼 공격했다!")
        print(f"{self.typeString}은 똑똑해서 한번 더 {self.attack * 0.5}만큼 공격했다!")
        print("-----------------")

class Dog(Unit):
    typeString = "개"
    def bark(self):
        print("왈왈!!")


dog = Dog(20, 30)
human = Human(10, 100, "한국")
monster = Monster(5, 100)

dog.doAttack()
dog.bark()
human.doAttack()
monster.doAttack()
개이(가) 아군에게 20만큼 공격했다!
-----------------
왈왈!!
사람이 아군에게 10만큼 공격했다!
사람은 똑똑해서 한번 더 5.0만큼 공격했다!
-----------------
몬스터이(가) 아군에게 5만큼 공격했다!

원래의 의도와 부합하여 아주 잘 작동한다는 것을 보실 수 있습니다.


상속의 특징

속성과 메소드의 상속

예제를 보면서 설명하도록 하겠습니다.

class Base:
    name = '베이스'           # 클래스 속성

    @classmethod              # 클래스 메소드
    def tellMeName(cls):
        print('이름은', cls.name, '입니다.')

    def __init__(self, age) : # 생성자(메소드)
        self.age = age        # 속성

class Sub(Base):
    pass

Sub.tellMeName()              # 클래스 메소드 호출
instance = Sub(13)            # Sub 객체 생성
print(instance.age)           # 객체 속성 접근
이름은 베이스 입니다.
13

Base 클래스를 상속받는 Sub 클래스는 Base의 속성과 메소드를 빠짐없이 상속받습니다. Sub 클래스의 정의는 단 하나도 없는데도, 클래스 메소드를 이용할 수 있고, 생성자로서 하나의 인수를 넣어야 하며, 객체 내의 속성도 잘 살아있음을 확인할 수 있습니다.


속성 및 메소드 확장

class Base:
    def __init__(self, age) :
        self.age = age        

class Sub(Base):
    def afterYears(self, year): # 메소드를 추가하였습니다.
        print(f"{year}년 뒤 나이는 {year+self.age}살 입니다...")
        self.future = 'Sad..' # 속성을 추가하였습니다.

instance = Sub(13)
instance.afterYears(10) # 추가된 메소드를 호출
print(instance.future) # 추가된 속성을 출력
10년 뒤 나이는 23살 입니다...
Sad..

상속받는 클래스는 물론 자기 자신만의 속성과 메소드를 구현할 수 있습니다.


오버라이딩 (overriding)

오버라이딩이란, 상위 클래스로부터 상속받은 메소드나 속성을 재정의한다는 뜻입니다. 쉽게 말해 덮어 쓴다는 뜻입니다. 그렇다면 왜 재정의를 하는 걸까요? 하위 클래스에서 기능을 수정 및 확장하기 위해서 입니다. 생성자를 예로 들어보겠습니다.

class Base:
    def __init__(self, age) :
        self.age = age
    
class Sub(Base):
    def __init__(self, age, country):
        self.age = age
        self.country = country

ins1 = Sub(13, '한국')
print(ins1.country)
ins2 = Sub(13)
한국
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/test2.py", line 12, in <module>
    ins2 = Sub(13)
TypeError: __init__() missing 1 required positional argument: 'country'

Sub 클래스의 생성자(__init__)를 우리가 새롭게 정의했습니다. 그렇게 되면 완전히 새로운 생성자로 대체됩니다. 그래서 Sub(13, '한국')과 같이 쓰는 건 옳지만 기존 Base 클래스의 생성자를 고려하여 인수를 하나만 쓰게 된다면 에러가 나게 됩니다.

오버라이딩은 이름만 똑같이 쓴다면 자동으로 오버라이딩이 됩니다. 아쉽게도 파이썬의 자체 기능은 오버라이드의 여부를 확인할 수 없습니다. 그러니까, 어떤 메소드나 속성을 정의할 때 이게 완전히 새롭게 정의되는 건지, 이미 상위 클래스에서 정의되었던 것을 오버라이드하는지 구분할 수 없다는 이야기이지요. 대부분의 상황에서는 오버라이드 해도 별 문제는 없지만 간혹 오버라이드 하면 안 되는 것들도 있을 수 있기 때문에 살짝 조심하셔야겠습니다.

그런데, 데코레이터(추가 예정)를 이용한 방법(링크)도 있으니 혹시 관심이 있으시다면 참조해주세요.

오버로딩? (overloading) (심화)

파이썬에는 상관 없는 이야기이므로 다른 언어를 접하지 않으셨다면 이 부분은 가볍게 패스하세요.

c++나 자바처럼 컴파일 타임에 많은 것이 결정되는 언어에서는 호출할 인수의 타입이나 개수에 따라서 호출될 함수가 미리 정해집니다. 그렇게 된다면 오직 하나의 함수 시그니처만 이용하게 되는 것일까요? 아닙니다. 바로 오버로딩(overloading)을 통해 이름은 같되 리턴형, 인수의 개수, 인수의 타입이 다른 메소드를 여러 번 정의함으로써 여러 함수 시그니처를 이용할 수 있게 됩니다.

파이썬에서는 오버로딩이라는 개념이 없습니다. 파이썬에서는 애초에 리턴형을 지정해줄 필요가 없을 뿐더러 인수의 타입에도 제한이 없고, 인수의 기본 값이나 가변 인수를 활용하여 다양한 인수 조합을 만들어낼 수 있습니다.


부모 클래스의 속성 및 메소드 접근

오버라이딩을 했을 때의 특징은 기존의 함수 내용은 모두 사라진다는 점입니다. 이는 의도한 행동일 수도 있고, 문제가 될 수도 있습니다. 왜냐하면 기존 상위 클래스의 메소드에다가 살짝의 부가 기능을 써넣고 싶은데, 오버라이드하게 되면 중복되는 부분을 전부 복사하여 붙여넣어야 하기 때문이죠. 이를 방지하기 위해 우리는 부모 클래스의 속성이나 메소드에 접근하여 호출할 수 있습니다.

아래 예제에서 print("뭔가 많은 행동을 하고 있습니다. 복붙하기엔 많아요.") 이 부분이 아주 길다고 상상해보세요. Sub에서 __init__을 오버라이딩 한다면 이 아주 긴 부분을 복사 붙여넣어야 할 판입니다. 하지만 이를 아주 쉽게 해결할 수 있습니다.

class Base:
    def __init__(self, age) :
        print("Base의 생성자가 시작됩니다.")
        self.age = age
        print("뭔가 많은 행동을 하고 있습니다. 복붙하기엔 많아요.")
        print("Base의 생성자가 끝났습니다.")
    
class Sub(Base):
    def __init__(self, age, country):
        print("Sub의 생성자가 시작됩니다.")
        super().__init__(age) # 상위 클래스 접근
        self.country = country
        print("Sub의 생성자가 끝났습니다.")

ins1 = Sub(13, '한국')
Sub의 생성자가 시작됩니다.
Base의 생성자가 시작됩니다.
뭔가 많은 행동을 하고 있습니다. 복붙하기엔 많아요.
Base의 생성자가 끝났습니다.
Sub의 생성자가 끝났습니다.

위 예제에서는 Sub의 생성자에서 상위 클래스를 super() 를 통해 접근하여, __init__ 메소드 즉 생성자를 명시적으로 호출해서 Base의 생성자를 불러냈습니다. 출력된 메세지를 보시면 Sub의 생성자가 시작되고 끝나는 사이에 Base의 생성자도 호출되었다는 사실을 알 수 있습니다.


다형성

다형성이란, 겉으로 봤을 때 같은 코드라도 적절하게 다른 메소드가 호출된다는 뜻입니다. 파이썬에서는 self라는 인스턴스를 메소드의 첫 번째 인수로 강제함으로써 다소 직관적으로 다형성을 이해해볼 수 있습니다. 다음 코드에서는 ##1## 부분의 self.printA()가 다형성이 적용되는 구간입니다. 다음 코드를 살펴봐주세요.

class Base:
    def printA(self):
        print('Base A')
    
    def printAandB(self):
        self.printA() ##b_1##
        print('Base B')

class Sub(Base):
    def printA(self):
        print('Sub A')
    
ins = Sub()
ins.printAandB()
Sub A
Base B

b_1(1.) 적절하게 printA 메소드가 선택된다.

코드의 하단에서 Sub 클래스의 인스턴스로 ins를 두었습니다. ins의 타입은 Sub가 되는 것이지요. Sub에는 printAandB 메소드를 구현하지 않았으므로 상속받은 Base의 것을 사용하게 됩니다. 이윽고 self.printA()에 다다랐을 때에 파이썬 인터프리터는 어떤 클래스에 정의되어있는 printA 메소드를 실행할지 결정해야 합니다.

파이썬 인터프리터는 본래의 클래스(Sub) 에 그 메소드가 정의되어 있을 경우 그 메소드를 실행합니다. 본래의 클래스가 없다면 차례로 상위 클래스로 가면서 발견되는 메소드를 실행합니다. 여기서의 printA 메소드 호출은 결국 Sub의 것으로 실행됩니다. printA 메소드의 호출 과정을 순서도로 표현하면 다음과 같습니다.

graph TD a1["self.printA()<br>를 만난다"] --> a2{"self가 지니고<br>있는 printA<br>메소드가<br>몇 개인가?"} a2 --> |아무 것도 없다|a3["AttributeError<br>에러 발생"] a2 --> |한 개 이상|a4["self 객체의<br>클래스에서부터<br>검색해본다."] a4 --> a6{"해당 클래스에<br>printA 메소드가<br>있는가?"} a6 --> |yes|a7["그 메소드를 실행시킨다."] a6 --> |no|a8["그 다음 상위 클래스를 검색해본다."] a8 --> a6

printA 메소드 호출 시 탐색 과정


연습 문제

  • 클래스 속성을 정의하는 방법은 무엇인가?
  • 클래스 메소드를 정의하는 방법은 무엇인가?
  • 일반 메소드와 달리 클래스 메소드의 가장 큰 특징은 무엇인가?
  • 상속에서 주요 특징 다섯 가지는 무엇인가?

뒷 이야기

메소드의 기본 값은 메소드 정의시 결정됩니다.

이 말인 즉슨, 메소드 기본 값을 설정할 때 즉각 변수로 사용한다 하더라도, 변수 시점의 값으로 고정된다는 뜻입니다. 아래와 같은 코드는 defaultAttack 값을 수정한다 하더라도 메소드의 기본 값이 변경되지 않습니다.

defaultAttack = 10
defaultHealth = 100

class Monster:

    def __init__(self, attack = defaultAttack, health = defaultHealth):
        self.attack = attack
        self.health = health
    
mon1 = Monster()
print(mon1.attack, mon1.health)

defaultAttack = 30 # defaultAttack 값을 변경했습니다.

mon2 = Monster()
print(mon2.attack, mon2.health) # 하지만 결과는 똑같습니다.
10 100
10 100

구현 강제

부모 클래스는 자식 클래스로 하여금 특정 메소드의 구현을 강제할 수 있습니다. 바로 NotImplementedError 예외를 이용하면 됩니다. 이 예외에 대한 내용은 문서를 참조해주세요.

class Base:
    def bark(self):
        raise NotImplementedError("bark 메소드가 구현되어야 합니다.")

class Sub(Base):
    pass

a = Sub()
a.bark()
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/test2.py", line 9, in <module>
    a.bark()
  File "c:/Users/tooth/Desktop/test2.py", line 3, in bark
    raise NotImplementedError("bark 메소드가 구현되어야 합니다.")
NotImplementedError: bark 메소드가 구현되어야 합니다.

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

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Scroll to top