아 한 2억만 상속받고 싶다

- 리덕토 - 

 

Python과 매우 친해지기 그 3번째 시간은 클래스의 상속(Inherit)이다. 상속은 객체지향에서 빼놓을 수 없는 중요한 기능이고, 약간의 이해도가 필요하기에 별도의 포스팅을 작성하였다.


상속(Inherit)의 정의와 사용

우리의 친구 위키백과에서 상속이 무엇인지 정의를 가져와 보자

객체 지향 프로그래밍(OOP)에서, 상속(inheritance)은 객체들 간의 관계를 구축하는 방법이다

- 출처 : 위키백과(ko.wikipedia.org/wiki/%EC%83%81%EC%86%8D_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D))

으흠; 조금 모호한 부분이 있는 거 같다. 객체들 간의 관계를 상속만으로 구축되는 것은 아니지만; 위키가 필자보다 몇만 배는 똑똑하기 때문에 쟤가 저러면 그런 거다. 정의는 저렇고 상속은 "다른 객체로부터 새로운 객체를 재생성하는 것"으로 이해해주면 될 거 같다. 

 

이런 상황을 가정하자 우리는 객체 지향 프로그램의 방식으로 프로그램을 만들고 있는 도중인데, 요리사와 소방관의 클래스를 만들어야 된다. 둘은 사람이라는 기본 전제를 가지고 있다. 따라서 사람으로 가질 수 있는 모든 속성이나 공통된 행동이 있을 것이다. 상속은 이러한 아이디어를 정확하게 표현해준다.

여기서 상속을 하는 클래스를 부모(Parent) 클래스라고 하며, 상속을 받는 Fire Fighter이나 Cook을 자식(Sub) 클래스, 같은 레벨에서 상속을 받은 Fire Fighter와 Cook은 서로를 형제(Sibling) 클래스라고 지칭한다. 이렇게 실행한 상속은 다음과 같은 특징을 가진다.

 

1. 자식은 부모의 클래스 변수, 매서드에 접근할 수 있다.
2. 부모는 자식의 클래스변수, 메서드에 접근할 수 없다.
3. 여러 개의 부모를 상속받을 수 있다.(여기서 다이아몬드형 상속이라는 문제가 발생한다. 궁금한 인원은 더보기를 확인)
더보기

위와 같은 형태가 다이아몬드 상속이다. 물론 상속받은 객체를 상속받는 것은 문제가 되지 않으나, Fire Fighter가 human에게 상속받은 some_function()와 Cook이 human에게 상속받은 some_function() 중 GoodMan이 사용해야 하는 매서드가 무엇인지 정할 수 없는 문재가 발생한다.

 

상속은 오로지 조금 더 구체적인 자식으로 단방향으로만 진행된다. 아래의 코드를 확인하면서 Python에서 상속의 사용법을 익혀보자

class human :
    
    name = ""

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

    def introduce(self) :
        print("hello im human "+self.name)

    def job(self) :
        print("I have no jobs")


class firefighter(human) :      ## firefighter은 human을 상속받음

    def __init__(self,name) :
        super().__init__(name)  ## 부모의 생성자를 호출

    def job(self) :             ## job은 firefighter가 오버라이딩
        print("my job is firefighter")

    def new_introduce(self) :
        super().introduce()     ## super를 이용, 부모의 매서드 호출
    
    def extinguish(self) :
        print("you off fire!")

        
if __name__ == "__main__" :

    cheolsu = human("cheolsu")
    younghee = firefighter("younghee")

    cheolsu.introduce()
    cheolsu.job()

    younghee.extinguish()
    younghee.introduce()        ## 부모의 매서드인 introduce사용가능
    younghee.job()
    
    

우선 human 클래스는 우리가 저번 시간에서 모두 배운 내용으로 이루어져 있다.(생성자, 매머드의 정의, 클래스 변수 name 등) 제일 먼저 눈에 띄는 것은 상속을 할 때 class [클래스명](상속받을 클래스명) :으로 정의한 점이다.(여러 개를 상속받으려 먼 상속받을 클래스명을 콤마로 나누어 적어주면 된다.)

 

프로그램의 main을 보면 human객체 cheosufirefighter객체 younghee가 있다. 주석에 적어두었듯 younghee 또한 부모의 매서드인 introduce를 사용하고 있다.

 

자식에서 부모를 호출할 때는 앞에 super()를 붙이는데 super() 매더스는 자신의 부모를 반환한다. 따라서 younghee의 super(). introduce()는 human.introduce()로 해석되며, firefighter 생성자의 super().__init__(name)은 부모 생성자를 호출하는 것과 같다.

 

오버 라이딩(Overriding)

부모에서 정의된 메서드를 자식에서 재정의하는 것을 오버 라이딩(Overriding)이라고 한다. 코드에서는 human에서 정의된 job 함수가 firefighter에서 재정의되며 오버 라이딩(Overriding)된 것을 확인할 수 있다.

* 혹시 다른 언어에서 넘어와서 오버 로딩(Overloading)의 개념을 찾으시는 분이 계시다면 아쉽게도 Python은 오버 로딩을 허용하지 않는다는 비보를 전한다. 단, Pythoin은 파라미터를 받을 수 있는 방법을 상당히 다양하게 제공한다.)


이번 시간에는 클래스와 상속에 대해서 알아보았다. 어떤가, 이제 객체 지향과 클래스에 대해서 조금은 익숙해 느낌이 드는가? 다음 시간에는 우리의 프로그램을 조금 더 안정적으로 만들 수 있는 예외처리에 대해서 알아보도록 하자

지난 시간 우리는 객체 지향 프로그램이 무엇이고, 클래스가 무엇인지, 그리고 간단하게 클래스 사용의 예시를 살펴보았다. 이번 시간에는 Python에서 클래스를 사용하는 방법을 자세하게 알아보도록 하자


클래스(Class)의 정의와 사용

우선 다음의 코드를 타이핑하고 이야기를 나누어보자. 주석은 각 코드에 대한 설명이니 한번씩 읽어보길 바란다.

### class_example.py ###

class dog :					## class의 이름을 dog로 지정하였다.
    dog_name = ""				## dog의 멤버변수 dog_name을 정의하고 ""로 초기화

    def __init__(self,name) :			## 생생자, name을 파라미터로 받아 dog_name대입
        self.dog_name = name

    def bark(self) :				## 생성된 객체에서 호출하는 bark
        print("WOW "+self.dog_name)

    def self_bark() :				## class에서 호출하는 bark
        print("BOW")

if __name__ == "__main__" :
    my_dog = dog("puppy")			## my_dog(객체)를 dog(클래스)로 생성
    my_dog.bark()				## 생성된 my_dog의 bark메서드 호출

    dog.self_bark()				## dog(클래스)에서 직접 self_bark메서드 호출
    
    
출력 :
WOW puppy
BOW

self

일단 이 self 예약어는 어떤 의미가 있는 것일까?

Python에서 객체를 생성을 하면 자기자신을 지칭할 때 self라고 한다. 재미있는 점은 class내부에서 메서드를 정의할 때  생성된 객체가 이 메서드를 사용하려면 암묵적으로 self를 첫 번째 파라미터로 넘겨야 한다는 점이다.(실제 사용 시에는 이 self는 없는 것으로 취급한다. 예를 들어 some_function(self)가 정의되어있으면 사용할 때는 some_funtion()으로 사용하면 된다는 것이다.)

 

비교를 위해서 코드에서 2가지 함수를 다음과 같이 정의해보았다.

1. bark(self) : self를 인자로 받는 메서드이다. self를 인자로 받기에, dog에서 객체를 직접 넘겨주거나 dog로 생성이된 객체에서만(위 예시에서는 my_dog)이 bark를 사용할 수 있다.

2. self_bark() : self를 인자로 받지 않는 메서드이다. 따라서 생성이 된 객체는 사용할 수 없으며(생성된 객체는 함수호출시 묵시적으로 첫번째파라미터로 자기 자신을 전달한다. self_bark는 self파라미터를 받지 않는다.) dog클래스에서 직접 호출하면 사용 가능하다.

 

생성자

처음 보는 메서드로 __init__(self, name)이라는 메서드가 눈에 뜨인다. 이 __init__메서드는 이미 정의된 것으로 객체가 생성되는 순간(위 코드에서는 my_dog = dog("puppy")가 처리되는 순간)에서 자동적으로 동작하는 함수를 의미한다. 객체가 생성되는 순간 동작하는 함수이기에 "생성자"라고 부른다. 이 생성자에도 파라미터를 넘길 수 있는데, 위 예제에서는 객체 생성에 사용될 메서드이니 self와 이름인 name을 파라미터로 전달하고 있다. 전달받은 name변수는 dog_name에 저장한다. Python에 생성자처럼 사전 정의된 다음과 같은 것들도 있다.

1. __del__(self) : 소멸자, 객체가 사라질 때 행동을 정의한다.

2. __repr__(self) : 프린팅 지시자, print로 객체 자신이 호출될 때 return 값으로 무엇을 출력할지 지시한다.

3. __add__(self,other) : 연산자 "+"가 객체에 취해질 때 행동을 정의한다.

4. __cmp__(self,other) : 비교 연산자를 객체에 사용할 수 있게 행동을 정의한다.

 

클래스 변수

아래 코드에서 dog클래스 바로 아래 dog_name, travel처럼 정의된 변수를 클래스 변수라고 한다.

객체는 자신의 변수를 접근할 때 self. [변수명]을 붙인다. 코드에서 new_puppy = dog("happy")를 했다면 my_dog.dog_name은 "puppy", new_puppy.dog_name은 "happy"가 된다.(dog.dog_name은 ""로 출력이 된다)

 

이 클래스 변수는 재미있는 특징이 있다. 다음의 코드 main을 보자

class dog :
    dog_name = ""
    travel = list()

    def __init__(self,name) :
        self.dog_name = name

    def bark(self) :
        print("WOW "+self.dog_name)

    def self_bark() :
        print("BOW")

    def add_travel(self, value) :
        self.travel.append(value)

if __name__ == "__main__" :
    my_dog = dog("puppy")
    new_dog = dog("happy")

    my_dog.add_travel("home")
    

가장 마지막의 my_dog.add_travel("home")을 통해서 my_dog의 travel list에 "home"을 추가해 주었다. 그런데 웬걸?

>> new_dog.travel
["home"]
>> dog.travel
["home"]

이상하게 다른 객체인 new_dog와 객체의 틀인 dog클래스의 travel list에도 "home"이 추가된 걸 확인할 수 있다.

 

이는 클래스가 언제 객체로 바뀌느냐(이를 인스턴스화라고 한다.)의 문제이다.

 

Python에서 리터럴을 제외한 자료구조와 변수들은 class 바로 아래 정의를 했다면 객체끼리 공유된다. 이는 생성자에 자료구조를 정의하는 것으로 해결된다. 위에 코드로 해결책을 예를 들면 생성자에 self.travel = list()로 정의한다.(이러면 객체가 생성(인스턴스화)이 되고 self.travel이라는 list가 생성이 된 것이니 객체끼리 공유되지 않고, 생성된 객체 고유의 자료구조로 본다.)


이번 시간에는 객체 지향 프로그래밍을 하기 위한 가장 기본적인 방법인 클래스 생성과 사용에 대해서 알아보았다. 다음 시간에는 객체 지향 프로그래밍에서 빼놓은 수 없는 개념인 상속과 python에서의 상속 사용을 알아보도록 하자

Python과 친해지는 것을 넘어서서 매우 친해지려는 여러분들을 환영한다. 아직 Python의 흥미를 잃지 않았기를 바라며, 더욱 재미있고 심화적인 내용을 준비해 보았다. 그 첫 번째 시간은 객체지향과 클래스이다.

 


객체 지향 프로그래밍(Object Object Programming) 

객체 지향 프로그램이은 프로그래밍은 프로그램을 만들 때 사용된는 하나의 이론이다. 프로그램 그 자체를 명령의 집합으로 딱딱하게 보는 것이 아니라, 객체(Object)의 단위로서 이해를 하며, 객체와 객체의 상호작용으로 프로그램은 동작한다는 느낌이다. 현재는 하나의 Dictionary students를 가지고 있는 우리의 학생관리 프로그램을 객체 지행 프로그래밍으로 설계를 했으면 아래처럼 설계를 했을 것이다.

즉 학생이 등록된 학생DB를 하나의 객체로 보고, 여러 개의 학생 개체들이 상호작용을 하며 프로그램이 동작했을 것이다. 

 

 

객체 지향 프로그래밍의 장점

굳이 객체 지향으로 프로그램을 만들어야하는가 하는 의문을 가진 사람들도 있을 것이다. 물론 합당한 이유고, 필자도 객체 지향으로 몇 번 프로그래밍하기 전까지는 그 장점을 알 수 없었다. 일단 다음과 같은 장점이 객체 지향 프로그래밍을 의미 있게 한다.

 

① 프로그래밍 코드의 재사용성 향상

여러번 코드 안쳐도 된다는 거다. 학생 객체를 생성해두면 이걸 그대로 교수 객체, 노동자 객체로 만들 수 있다. 상속을 통해서 더욱 이 장점이 부각되는데, 상속이 어떤 것인지는 추후에 배우게 된다.

 

② 코드의 높은 응집력(Strong Cohesion과 낮은 결속력(Weak coupling)구현

응집력이란 한 모듈의 내부의 처리 요소들이 얼마나 기능적으로 연결되었는지를 의미하고, 결속력은 한 모듈과 다른 모듈 간의 상호 의존성을 의미한다. 따라서 "높은 응집력과 낮은 결속력"이 의미하는 것은 객체들이 얼마나 본인의 역할을 충실히 하면서 다른 모듈간의 의존을 하지 않는가를 의미한다.(유지보수 측면에서 이게 또 기가 막힌다. 장애 발생 시 고장이 난 모듈만 고치면 된다.)

 

③ 일상생활의 높은 수준으로 추상화를 통한 구현 용이성

자연어로 기술 시 나오는 컴포넌트들이 그대로 객체지향을 구현될 수 있다는 특징이다.  예를 들면 위의 설계도를 짜기 위해서 필자가 처음 생각한 것은 "학생 DB에 학생을 넣어 관리할 수 있는 프로그램"이었다. 그래서 학생 DB와 학생을 객체로 만들에 다이어그램을 만든 것이고, ver3 학생관리 프로그램은 실제로 저렇게 구현을 할 것이다.

 

 

클래스(Class)

클래스(clss)는 객체 지향 프로그래밍(OOP)에서 특정 객체를 생성하기 위해 변수와 메서드를 정의하는 일종의 틀.
객체를 정의하기 위한 상태(멤버 변수)와 메서드(함수)로 구성된다

- 출처 : 위키백과(ko.wikipedia.org/wiki/%ED%81%B4%EB%9E%98%EC%8A%A4_(%EC%BB%B4%ED%93%A8%ED%84%B0_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D)) - 

위는 위키백과에서 정의한 클래스이다. 무엇이든 뒤에 붙은 말은 핵심인데(붕어빵!), 첫 문장의 끝인 "일종의 틀"이라는 것이 눈에 들어온다. 

우선 아래와 같은 클래스 사용의 간단한 예시문을 통해서 이해를 높여보자 (* 문법은 지금 몰라도 무관하다.)

코드 : 
class student :
    name = ""

    def print_info(self) :
        print("im student "+self.name)

if __name__ == "__main__" :
    cheolsu = student()
    cheolsu.name = "cheolsu"
    cheolsu.print_info()

    younghee = student()
    younghee.name = "younghee"
    younghee.print_info()


출력 :
im studentcheolsu
im studentyounghee

우리는 class인 student를 만들었다. 그리고 student에는 name이라는 상태(멤버 변수)와, 자신의 정보를 출력하는 메서드(함수)를 정의했다. 그 후 학생 2명 철수와 영희를 student 클래스를 통해서 만들었다. 이제 클래스가 왜 "일종의 틀"인지 알 거 같다. 즉 정의된 class를 통해서 객체(Object)를 찍어내는(생성하는)것이다.

 

* if __name__ == "__main__"의 의미

더보기

* if __name__=="__main__" : 을 처음 보는 사람이 분명 있을 것이다.

위 조건문은 딱 읽으면 __name__이라는 변수가 __main__일 때 실행하는 조건문인데, 이 __name__이라는 변수는 프로그램의 내장 변수로 현재의 모듈의 이름을 담고 있는 내장 변수이다.

 

이때 직접 실행된 모듈은 __name__변수가 __main__이다. 따라서 저 조건문을 작성한 행위는 직접 실행 코드에 대해서 실행할 코드를 지정하는 일이다. 만약 다른 모듈에 if __name__=="__main__" 이 있더라도 직접 실행하지 않고, import 하면 그 모듈의 if __name__=="__main__" 코드 블록은 실행되지 않는다.

 

내장 변수에 대해서는 추후에 자세한 글을 포스팅할 예정이다.

 


맨 처음 advanced 한 tutorial시간이었는데, 잘 따라와 줘서 감사할 따름이다. 다음 시간에는 이 class가 어떻게 Python에서 동작하는지 깊게 이해하는 시간을 가져보도록 하자.

+ Recent posts