흔한말 (collection)/Python

물려줄 것이라고는 속성과 메소드 뿐이구나..파이썬 클래스 상속 알아보기

hnanmal 2022. 8. 30. 22:10
300x250

객체를 왜 만들고, 왜 상속을 하는가?

객체란?

객체는 정보의 덩어리를 의미합니다. 눈앞에 있는 가장 간단한 사물을 봐도, 단 한가지 종류의 정보 만으로는 기술할 수 없다는 것을 잘 느낄 수 가 있습니다. 지우개 하나 만 보더라도, 물체가 가진 형상의 치수와 경도, 그리고 구입한 날짜와 현재 사용 정도 등 정량적으로 기술 할 수 있는 항목만 나열해봐도 손가락 10개가 빠듯해지죠. 그리고 우리가 세상을 인식하는 기본 틀이기도 합니다.

 

컴퓨터는 결국에는 순차적으로 내리는 명령을 수행할 뿐이지만, 코드를 작성하는 단계에서는 다양한 상황에 다양한 기능을 수행하는 복잡한 프로그램을 구상하려면 절차적 프로그래밍으로는 곧(저의 경우 1분만에) 한계에 봉착합니다. 가공할 천재라면 절차적 프로그래밍으로도 얼마든지 시스템의 복잡도를 케어할 수 있겠지만, 혼자서 모든 걸 만들어야 할테고, 다른 사람들은 손도 못댈 가능성이 높겠죠?

 

그래서 함께 묶여서 연관된 기능을 함께 수행하게 될 데이터들을 보자기로 싸매 버리는 방식을 규정하는 것이 객체(클래스)이고, 클래스를 통해 생성해낸 것이 특정 데이터 조합이 바로 개체(인스턴스)라고 생각하면 편합니다.

 

(설명에 사용된 용어는 엄밀하지 않을 수 있습니다.)

 

객체는 범주론과도 어느 정도 닿아있는 면이 있는데, 우리가 사물을 인식할 때, 추상이라는 사고과정을 통해 공통적인 측면들을 모아 자연스럽게 상위의 범주로 인식하게 됩니다. 예를 들어 사람이 있고, 이 사람의 부분 집합으로서 운동선수, 학생, 교수, 사업가 등이 있다고 인식하듯이 말이죠. 프로그래밍에서는 객체를 이용한 코딩을 통해 이러한 개념상의 위계질서를 잘 표현하고 복잡도를 제어할 수 있게 됩니다.

 

그래서 이번에는 객체(클래스)전반에 관한 내용을 다루기 보다는 중복코드 축약과 기능 확장 대응에 편리한 상속에 대해서 좀 알아보려고 합니다. 파이썬으로 알아봅니다~.

 

 

상속 방식과 효과

기본 개념

상속은 이미 많이 들어본 말이죠? 가진 것을 물려준다라는 뜻입니다. 코딩에서도 하나의 클래스가 자신이 가진 모든 것을 다른 클래스에게 물려줄 수 있습니다. 현실에서는 부모가 자식에게 무언가 상속하려고 하면 정말 많은 절차가 필요합니다. 서류는 아래와 같은 것들이 필요하다고 합니다.

1.제적등본

2.전제적 등본(돌아가신 분의 전제적(할아버지제적) -1통

3.말소자 초본 (과거주소이력 다 나오게 발급) -1통

4.기본증명서 -1통

5.가족관계증명서 -1통

6.입양관계증명서 -1통

7.친양자 입양관계증명서 -1통

 

 

하지만 파이썬에서 부모 클래스가 자식 클래스에게 상속을 하는 과정은 간단합니다.

class 부모클라스:
    pass

class 자식클라스(부모클라스):
    pass

상속받을 클래스를 정의할 때, 클래스 명 뒤에 괄호를 붙이고 상속 받아올 대상 클래스 이름을 적어주기만 하면 끝입니다.

이번에는 상속을 이용해서 클래스로 만든 객체가 생성되자 마자 자신의 정체성을 외치는 코드를 한번 짜 볼까요?

class 부모클라스:
    def __init__(self):
        print("부모!!!")

class 자식클라스(부모클라스):
    def __init__(self):
        부모클라스.__init__(self)
        print("자식!!!")


부모개체 = 부모클라스()
print("\n")
자식개체 = 자식클라스()

---------<<결과>>----------
부모!!!

부모!!!
자식!!!

부모클라스에서는 객체를 생성하자마자 “부모!!!”라고 외치게 만들었고, 이를 상속받은 자식클라스에서는 부모의 멘트 이후에 자신의 멘트인 “자식!!!” 까지 덧붙이도록 짠 코드입니다. 상속의 느낌을 느끼셨죠?

 

 

소꿉놀이봇으로 알아보는 상속

이번에는 소꿉놀이봇을 한번 디자인해보면서 상속을 어떻게 활용할 수 있는지 알아봅시다.

class RolePlayer:
    """Parent Class"""
    name = "성명"
    role = "역할"

    def tellName(self):
        print(f"내 이름은 {self.name} 이야..")

먼저 소꿉놀이의 원형, 프로토타입이 될 RolePlayer라는 클래스를 아주 간단하게 선언해봤습니다. 내부 속성으로 name과 role을 갖고 있고, 생성된 인스턴스가 tellName이라는 메소드를 호출받게되면 자신의 이름을 말하게 되는 클래스입니다.

 

 

 

소꿉놀이는 역시 의사와 환자 놀이가 제일 재미있었던 어릴적 기억이 있기 때문에 먼저 RolePlayer클래스를 상속해서 의사 클래스를 만들어 보겠습니다. 이따 업그레이드 할 예정이므로 일단 ‘version 0’이라고 이름뒤에 적어둘게요.

## Class Declaration Code

class Doctor_v0(RolePlayer):
    """Child Class"""
    def __init__(self, name):
        self.name = name
        self.role = "의사"

    def introduce(self):
        print(f"나는 {self.role} 야.. 잘 부탁해.")
## Client Code

의사0 = Doctor_v0("김익남")
의사0.tellName()
의사0.introduce()

---------<<결과>>----------
내 이름은 김익남 이야..
나는 의사 야.. 잘 부탁해.

의사 클래스를 이처럼 정의하고 나니, 부모클래스의 tellName 메소드는 물론이고, 자신이 추가로 정의한 introduce 메소드를 이용해서 역할까지도 말하게 된 것을 볼 수 있네요.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

물려받은 것이 마음에 들지 않을 때 자식이 하는 짓

의사 클래스를 만들었으니, 환자 클래스도 한 번 만들어볼까요?

 

Patient 클래스를 선언할 때는 의사와는 다른 점이 하나 있습니다. 어디가 아픈지 이야기 해야하기 때문에, 인스턴스 생성시에 인자로 아픈곳을 같이 전달해 줘야 한다는 점이죠. 그리고 자신의 이름을 말할 때 어디가 아픈지도 같이 말해줘야 원활한 진료가 이루어 질 수 있을 겁니다.

 

하지만 부모클래스에서 정의된 tellName메소드는 이름만 딱 말하고 끝나거든요. Patient클래스 입장에서는 물려받은 유산이 시원치 않아서 마음에 들지 않는 상황이 온겁니다. 이럴 때는 어떻게 하면 될까요?

## Class Declaration Code

class Patient(RolePlayer):
    """Child Class"""
    def __init__(self, name, problem):
        self.name = name
        self.role = "환자"
        self.problem = problem

    def introduce(self):
        print(f"나는 {self.role} 야.. 잘 부탁해.")

    def tellName(self):
        print(f"내 이름은 {self.name} 이고, {self.problem} (이)가 아파서 왔어..")

맘에 들지 않으면 자기가 직접 바꿔버리면 됩니다! 메소드 이름은 동일하게 유지한 채로, 클래스 내에서 새롭게 내용을 작성해서 선언해 버리면 되는 거죠. 이러한 방법을 메소드 오버라이딩이라고 합니다.

 

위의 코드에서 보듯이, tellName 메소드를 Patient 클래스 내부에서 재정의 해버린 것을 볼 수 있습니다.

 

이제 정의한 클래스를 활용해서 코드를 짜봅시다.

## Client Code

의사 = Doctor_v0("익준")
환자 = Patient("청명", "허리")
playerList = [의사, 환자]

for i in playerList:
    i.introduce()
    i.tellName()
    print("\n")

---------<<결과>>----------
나는 의사 야.. 잘 부탁해.
내 이름은 익준 이야..

나는 환자 야.. 잘 부탁해.
내 이름은 청명 이고, 허리 (이)가 아파서 왔어..

환자 하나를 생성하고, playerList라는 변수에 리스트를 만들어 의사와 환자를 한꺼번에 담아둡시다. 그런 다음에 for문을 통해 소꿉놀이에 참여하는 사람들을 한명씩 역할소개와 이름소개를 하게 하면, 위 코드처럼 아름다운 의사 환자 소꿉놀이 광경이 펼쳐집니다. 재밌죠?

 

 

회진을 도는 의사봇을 만들어 보자

이제 의사 클래스를 좀 업그레이드 해서 여러명의 환자를 회진하는 의사클래스 v1을 한번 만들어볼까요?!

## Class Declaration Code

class Doctor_v1(Doctor_v0):
    def matchPatient(self, *args):
        self.patientList = args

    def diagnosis(self):
        for i in self.patientList:
            print(f"{i.name}님, {i.problem}가 안좋으시군요. 약 처방할께요.")

새로운 버전의 의사 클래스는 Doctor_v0를 상속하고 있습니다. 이중으로 상속이 된 것을 볼 수 있죠? RolePlayer > Doctor_v0 > Doctor_v1 의 순서로 상속되고 있습니다. 다른 코드는 건드리지 않고, Doctor_v1클래스에서는 딱 2가지의 메소드만 추가하려고 합니다.

 

바로 matchPatient 메소드와 diagnosis 메소드가 그것이죠.

 

이 두 메소드의 역할은

각각 의사가 담당하게 될 환자들을 지정해주는 역할,

그리고 담당 환자들의 이름과 병세를 말해주는 회진 역할이 되겠습니다.

 

코드 중에서 * 표가 들어간 부분이 있는데, 이 부분이 잘 이해가 가지 않더라도 크게 신경쓰지 마세요. Asterisk(*)는 파이썬에서 함수인자의 개수를 한정하고 싶지 않을 때 사용하게 되는 표현입니다.

그래서 위의 matchPatient 메소드는 그 인자로 환자 5명이 들어오든 7명이 들어오든 개의치 않고 작업을 수행할 수 있게 됩니다. 이 Asterisk에 대해서는 추후에 더 자세히 포스팅 하겠습니다.

 

 

## Client Code

환자1 = Patient("청명", "허리")
환자2 = Patient("조걸", "경추")
환자3 = Patient("철용", "팔꿈치")

newdoctor = Doctor_v1("화타")
newdoctor.matchPatient(환자1, 환자2, 환자3)
newdoctor.diagnosis()

---------<<결과>>----------
청명님, 허리가 안좋으시군요. 약 처방할께요.
조걸님, 경추가 안좋으시군요. 약 처방할께요.
철용님, 팔꿈치가 안좋으시군요. 약 처방할께요.

먼저 환자 3명을 만들어보겠습니다. 환자1, 2, 3 변수에 각각 이름과 병세를 적어서 환자 인스턴스를 생성했습니다.

 

그 다음에는 화타라는 이름의 의사객체도 새로 만들고, matchPatient 메소드를 이용해 아까 만들어 둔 환자 3명을 매칭시켜 줍니다.

그리고 나서 최종적으로 회진을 돌게하는 명령인 diagnosis 메소드를 실행시키면 결과와 같이 모든 환자들을 세심하게 신경쓰며 회진을 도는 화타 의사의 역할극이 출력되게 됩니다!

 

결론

여기서는 사실 설명을 위해서 조금은 우스꽝스러운 방식으로 클래스들을 생성하고 서로 상속받도록 했습니다만, 실제로 효율적이고 합리적인 코드를 짜려면 디자인 패턴을 고려하여 클래스 구조를 만드는 것이 좋습니다.

 

다만 이 포스트에서는 상속이라는 방식을 이용하여 어떻게 점차 기능이 추가되는 클래스를 만들고 관리할 수 있는지 대략적인 느낌만 받으셔도 성공이라고 생각합니다.

반응형