Skip to content

25. 클래스 상속(Inheritance)


1. 키워드

  • 상속(Inheritance)
  • 기반 클래스(Base Class), 부모 클래스(Parent Class), 슈퍼 클래스(Superclass)
  • 파생 클래스(Derived Class), 자식 클래스(Child Class), 서브 클래스(Subclass)
  • 오버라이딩(Overriding)
  • 다중 상속(Multiple Inheritance)
  • 다이아몬드 상속(Diamond Inheritance)과 메서드 탐색 순서(Method Resolution Order)
  • 추상 클래스(Abstract Class)


2. 클래스 상속 사용하기

  • 클래스 상속은 물려받은 기능을 유지한채로 다른 기능을 추가할 때 사용하는 기능이다.
  • 여기서 기능을 물려주는 클래스를 기반 클래스, 상속을 받아 새롭게 만드는 클래스를 파생 클래스라고 한다.


001


  • 클래스 상속은 생물 분류를 떠올리면 이해하기 쉽다.
  • 예를 들어 조류, 어류는 공통된 조상인 척추동물로부터 물려받은 특성을 공유하면서 각자 고유한 특성을 가진다.
  • 척추를 가졌다는 특성은 변함이 없지만 날개를 가졌으면 조류, 물속에 살면 어류인 식이다.
  • 즉, 같은 계통으로 특성을 공유하며 전혀 상관없이 어류가 꽃식물의 특성을 가지지는 않는다.
  • 마찬가지로 클래스 상속도 기반 클래스의 능력을 그대로 활용하면서 새로운 클래스를 만들 때 사용한다.


002


  • 만약 상속 개념을 적용하지 않는 경우, 새로운 기능이 필요할 때마다 계속 클래스를 만든다면 중복되는 부분을 반복해서 만들어야 한다.
  • 이럴 때 상속을 사용하면 중복되는 기능을 만들지 않아도 되기 때문에 상속은 기존 기능을 재사용할 수 있어서 효율적이다.


3. 사람 클래스로 학생 클래스 만들기

  • 클래스 상속은 다음과 같이 클래스를 만들 때 ()(괄호)를 붙이고 안에 기반 클래스 이름을 넣는다.


class 기반클래스이름:
    코드

class 파생클래스이름(기반클래스이름):
    코드


  • 간단하게 사람 클래스를 만들고 사람 클래스를 상속받아 학생 클래스를 만들어보자.


class Person:
    def greeting(self):
        print("안녕하세요.")

class Student(Person):
    def study(self):
        print("공부하기")

james = Student()

james.greeting() # 안녕하세요.
james.study() # 공부하기


  • Student 클래스를 만들 때 class Student(Person):과 같이 ()(괄호) 안에 기반 클래스인 Person 클래스를 넣었다.
  • 이렇게 하면 Person 클래스의 기능을 물려받은 Student 클래스가 된다.


  • Student 클래스에는 greeting 메서드가 없지만, Person 클래스에서 상속받았으므로 다음과 같이 greeting 메서드를 호출할 수 있다.


james = Student()

james.greeting() # 안녕하세요.


  • 그리고 Student 클래스에 추가한 새로운 메서드인 study를 호출했다.


james.study() # 공부하기


  • Person 클래스와 Student 클래스의 관계를 그림으로 나타내면 다음과 같은 모양이 된다.


003


  • 이처럼 클래스 상속은 기반 클래스의 기능을 유지하면서 새로운 기능을 추가할 수 있다.
  • 특히 클래스 상속은 연관되면서 동등한 기능일 때 사용한다.


상속 관계 확인하기

  • 클래스의 상속 관계를 확인하고 싶을 때는 issubclass 함수를 사용한다.


issubclass(파생클래스, 기반클래스)
>>> class Person:
...     pass
...

>>> class Student(Person):
...     pass
...

>>> issubclass(Student, Person)
True


  • StudentPerson의 파생 클래스이므로 issubclassTrue가 나온다.


4. 상속 관계와 포함 관계 알아보기

  • 클래스 상속은 정확히 어디에 사용해야 하는지 알아보자.


1) 상속 관계

  • 앞에서 만든 Student 클래스는 Person 클래스를 상속받아서 만들었다.


class Person:
    def greeting(self):
        print("안녕하세요.")

class Student(Person):
    def study(self):
        print("공부하기")


  • 여기서 학생 Student는 사람 Person이므로 같은 종류이다.
  • 이처럼 상속은 명확하게 같은 종류이며 동등한 관계일 때 사용한다.
  • 즉, "학생은 사람이다."라고 했을 때 말이 되면 동등한 관계라고 한다.
  • 그래서 상속 관계를 영어로 is-a 관계라고 부른다.


2) 포함 관계

  • 학생 클래스가 아니라 사람 목록을 관리하는 클래스를 만들려면 다음과 같이 리스트 속성에 Person 인스턴스를 넣어서 관리하면 된다.


class Person:
    def greeting(self):
        print("안녕하세요.")

class PersonList:
    def __init__(self):
        self.person_list = []

    def append_person(self, person):
        self.person_list.append(person)


  • 여기서는 상속을 사용하지 않고 속성에 인스턴스를 넣어서 관리하므로 PersonListPerson을 포함하고 있다.
  • 이러면 사람 목록 PersonList와 사람 Person은 동등한 관계가 아니라 포함 관계이다.
  • 즉, "사람 목록은 사람을 가지고 있다."라고 말할 수 있다.
  • 그래서 포함 관계를 영어로 has-a 관계라고 부른다.


5. 기반 클래스의 속성 사용하기

  • 다음과 같이 Person 클래스에 hello 속성이 있고, Person 클래스를 상속받아 Student 클래스를 만든다.
  • 그다음에 Student로 인스턴스를 만들고 hello 속성에 접근해 본다.


class Person:
    def __init__(self):
        print("Person __init__")
        self.hello = "안녕하세요."

class Student(Person):
    def __init__(self):
        print("Student __init__")
        self.school = "파이썬 코딩 도장"

james = Student()

print(james.school)
print(james.hello)

# Student __init__
# 파이썬 코딩 도장
# AttributeError: 'Student' object has no attribute 'hello'


  • 실행을 해보면 에러가 발생한다.
  • 왜냐하면 기반 클래스 Person__init__ 메서드가 호출되지 않았기 때문이다.
  • 즉, Person__init__ 메서드가 호출되지 않으면, self.hello = "안녕하세요."도 실행되지 않아서 속성이 만들어지지 않는다.


1) super()로 기반 클래스 초기화하기

  • 이때는 super()를 사용해서 기반 클래스의 __init__ 메서드를 호출해 준다.


  • 다음과 같이 super() 뒤에 .(점)을 붙여서 메서드를 호출한다.


super().메서드()
class Person:
    def __init__(self):
        print("Person __init__")
        self.hello = "안녕하세요."

class Student(Person):
    def __init__(self):
        print("Student __init__")
        super().__init__()
        self.school = "파이썬 코딩 도장"

james = Student()

print(james.school)
print(james.hello)

# Student __init__
# person __init__
# 파이썬 코딩 도장
# 안녕하세요.


  • 실행을 해보면 기반 클래스 Person의 속성인 hello가 잘 출력된다.
  • super().__init__()과 같이 기반 클래스 Person__init__ 메서드를 호출해 주면 기반 클래스가 초기화되어서 속성이 만들어진다.
  • 실행 결과를 보면 "Student __init__""Person __init__"이 모두 출력되었다.


  • 기반 클래스 Person의 속성 hello를 찾는 과정을 그림으로 나타내면 다음과 같은 모양이 된다.


004


2) 기반 클래스를 초기화하지 않아도 되는 경우

  • 만약 파생 클래스에서 __init__ 메서드를 생략한다면 기반 클래스의 __init__이 자동으로 호출되므로 super()는 사용하지 않아도 된다.


class Person:
    def __init__(self):
        print("Person __init__")
        self.hello = "안녕하세요."

class Student(Person):
    pass

james = Student()

print(james.hello)

# Person __init__
# 안녕하세요.


  • 이처럼 파생 클래스에 __init__ 메서드가 없다면 기반 클래스의 __init__이 자동으로 호출되므로 기반 클래스의 속성을 사용할 수 있다.


좀 더 명확하게 super 사용하기

  • super는 다음과 같이 파생 클래스와 self를 넣어서 현재 클래스가 어떤 클래스인지 명확하게 표시하는 방법도 있다.


super(파생클래스, self).메서드
class Student(Person):
    def __init__(self):
        print("Student __init__")
        super(Student, self).__init__()
        self.school = "파이썬 코딩 도장"


6. 메서드 오버라이딩 사용하기

  • 이번에는 파생 클래스에서 기반 클래스의 메서드를 새로 정의하는 메서드 오버라이딩에 대해 알아보자.


  • 다음과 같이 Persongreeting 메서드가 있는 상태에서 Student에도 greeting 메서드를 만든다.


class Person:
    def greeting(self):
        print("안녕하세요.")

class Student(Person):
    def greeting(self):
        print("안녕하세요. 저는 학생입니다.")

james = Student()

james.greeting() # 안녕하세요. 저는 학생입니다.


  • james.greeting()처럼 Studentgreeting 메서드를 호출하니 "안녕하세요. 저는 학생입니다."가 출력되었다.
  • 오버라이딩은 무시하다, 우선하다라는 뜻을 가지고 있는데 말 그대로 기반 클래스의 메서드를 무시하고 새로운 메서드를 만든다는 뜻이다.
  • 여기서 Person 클래스의 greeting 메서드를 무시하고 Student 클래스에서 새로운 greeting 메서드를 만들었다.
  • 보통 프로그램에서 어떤 기능이 같은 메서드 이름으로 계속 사용되어야 할 때 메서드 오버라이딩을 활용한다.


  • 다시 Person 클래스의 greeting 메서드와 Student 클래스의 greeting 메서드를 보면 "안녕하세요."라는 문구가 중복된다.


    def greeting(self):
        print("안녕하세요.")
    def greeting(self):
        print("안녕하세요. 저는 학생입니다.")


  • 이럴 때는 기반 클래스의 메서드를 재활용하면 중복을 줄일 수 있다.


  • 다음과 같이 오버라이딩된 메서드에서 super()로 기반 클래스의 메서드를 호출해 본다.


class Person:
    def greeting(self):
        print("안녕하세요.")

class Student(Person):
    def greeting(self):
        super(Student, self).greeting()
        print("저는 학생입니다.")

james = Student()

james.greeting()

# 안녕하세요.
# 저는 학생입니다.


  • Studentgreeting에서 super().greeting()으로 Persongreeting을 호출했다.
  • 즉, 중복되는 기능은 파생 클래스에서 다시 만들지 않고, 기반 클래스의 기능을 사용하면 된다.
  • 이처럼 메서드 오버라이딩은 원래 기능을 유지하면서 새로운 기능을 덧붙일 때 사용한다.


7. 다중 상속 사용하기

  • 다중 상속은 여러 기반 클래스로부터 상속을 받아서 파생 클래스를 만드는 방법이다.


  • 다음과 같이 클래스를 만들 때 ()(괄호) 안에 클래스 이름을 ,(콤마)로 구분해서 넣는다.


class 기반클래스이름1:
    코드

class 기반클래스이름2:
    코드

class 파생클래스이름(기반클래스이름1, 기반클래스이름2):
    코드


  • 그럼 사람 클래스와 대학교 클래스를 만든 뒤 다중 상속으로 대학생 클래스를 만들어보자.


class Person:
    def greeting(self):
        print("안녕하세요.")

class University:
    def manage_credit(self):
        print("학점 관리")

class Undergraduate(Person, University):
    def study(self):
        print("공부하기")

james = Undergraduate()

james.greeting() # 안녕하세요.
james.manage_credit() # 학점 관리
james.study() # 공부하기


  • 먼저 기반 클래스 PersonUniversity를 만들었다.
  • 그다음에 파생 클래스 Undergraduate를 만들 때 class Undergraduate(Person, University):와 같이 괄호 안에 PersonUniversity를 콤마로 구분해서 넣었다.
  • 이렇게 하면 두 기반 클래스의 기능을 모두 상속받는다.


  • 즉, 다음과 같이 Undergraduate 클래스의 인스턴스로 PersongreetingUniversitymanage_credit을 호출할 수 있다.


james = Undergraduate()

james.greeting() # 안녕하세요.
james.manage_credit() # 학점 관리
james.study() # 공부하기


  • Person, University, Undergraduate 클래스의 관계를 그림으로 나타내면 다음과 같은 모양이 된다.


005


1) 다이아몬드 상속

  • 그럼 조금 복잡한 클래스 상속을 해보자.


class A:
    def greeting(self):
        print("안녕하세요. A입니다.")

class B(A):
    def greeting(self):
        print("안녕하세요. B입니다.")

class C(A):
    def greeting(self):
        print("안녕하세요. C입니다.")

class D(B, C):
    pass

x = D()

x.greeting() # 안녕하세요. B입니다.


  • 기반 클래스 A가 있고, B, CA를 상속받는다.
  • 그리고 다시 DB, C를 상속받는다.


  • 이 관계를 그림으로 나타내면 다음과 같은 모양이 된다.


006


  • 객체지향 프로그래밍에서는 이런 상속 관계를 다이아몬드 상속이라고 부른다.
  • 여기서는 클래스 A를 상속받아서 B, C를 만들고, 클래스 BC를 상속받아서 D를 만들었다.
  • 그리고 A, B, C 모두 greeting이라는 같은 메서드를 가지고 있기 때문에, 이런 경우 D가 호출해야 하는 클래스의 메서드를 알기 어렵다.


2) 메서드 탐색 순서 확인하기

  • 많은 프로그래밍 언어들이 다이아몬드 상속에 대한 해결책을 제시하고 있는데, 파이썬에서는 메서드 탐색 순서(MRO)를 따른다.


  • 다음과 같이 클래스 D에 메서드 mro를 사용해 보면 메서드 탐색 순서가 나온다.


클래스.mro()
>> D.mro()
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]


  • MRO에 따르면 D의 메서드 호출 순서는 자기자신 D, 그다음이 B이다.
  • 따라서 D로 인스턴스를 만들고 greeting을 호출하면 Dgreeting은 없으므로 Bgreeting이 호출된다.


x = D()

x.greeting() # 안녕하세요. B입니다.


  • 파이썬은 다중 상속을 한다면 class D(B, C):의 클래스 목록 중 왼쪽에서 오른쪽 순서로 메서드를 찾는다.
  • 그러므로 같은 메서드가 있다면 B가 우선한다.
  • 만약 상속 관계가 복잡하게 얽혀 있다면 MRO를 살펴보는 것이 편리하다.


object 클래스

  • 파이썬에서 object는 모든 클래스의 조상이다.
  • 그래서 다음과 같이 int의 MRO를 출력해 보면 int 자기자신과 object가 출력된다.


>>> int.mro()
[<class 'int'>, <class 'object'>]


  • 파이썬 3에서 모든 클래스는 object 클래스를 상속받으므로 기본적으로 object를 생략한다.


8. 추상 클래스 사용하기

  • 추상 클래스는 메서드의 목록만 가진 클래스이며 상속받는 클래스에서 메서드 구현을 강제하기 위해 사용한다.


  • 먼저 추상 클래스를 만들려면 importabc 모듈을 가져와야 하는데, 여기서 abc는 Abstract Base Class의 약자이다.
  • 그리고 클래스의 ()(괄호) 안에 metaclass=ABCMeta를 지정하고, 메서드를 만들 때 위에 @abstractmethod를 붙여서 추상 메서드로 지정한다.


from abc import *

class 추상클래스이름(metaclass=ABCMeta):
    @abstractmethod
    def 메서드이름(self):
        코드


  • 여기서는 from abc import *abc 모듈의 모든 클래스와 메서드를 가져왔다.


  • 그럼 학생 추상 클래스 StudentBase를 만들고, 이 추상 클래스를 상속받아 학생 클래스 Student를 만들어보자.


from abc import *

class StudentBase(metaclass=ABCMeta):
    @abstractmethod
    def study(self):
        pass

    @abstractmethod
    def go_to_school(self):
        pass

class Student(StudentBase):
    def study(self):
        print("공부하기")

james = Student()

james.study() # TypeError: Can't instantiate abstract class Student with abstract methods go_to_school


  • 실행을 해보면 에러가 발생한다.
  • 왜냐하면 추상 클래스 StudentBase에서는 추상 메서드로 studygo_to_school을 정의했다.
  • 하지만 StudentBase를 상속받은 Student에서는 study 메서드만 구현하고, go_to_school 메서드는 구현하지 않았으므로 에러가 발생한다.
  • 따라서 추상 클래스를 상속받았다면 @abstractmethod가 붙은 추상 메서드를 모두 구현해야 한다.


  • 다음과 같이 Student에서 go_to_school 메서드도 구현해 준다.


from abc import *

class StudentBase(metaclass=ABCMeta):
    @abstractmethod
    def study(self):
        pass

    @abstractmethod
    def go_to_school(self):
        pass

class Student(StudentBase):
    def study(self):
        print("공부하기")

    def go_to_school(self):
        print("학교가기")

james = Student()

james.study() # 공부하기
james.go_to_school() # 학교가기


  • 모든 추상 메서드를 구현하니 실행이 잘 된다.
  • 이처럼 추상 클래스는 파생 클래스가 반드시 구현해야 하는 메서드를 정해 줄 수 있다.
  • 참고로 추상 클래스의 추상 메서드를 모두 구현했는지 확인하는 시점은 파생 클래스로 인스턴스를 만들 때이다.
  • 따라서 james = Student()에서 확인한다.


1) 추상 메서드를 빈 메서드로 만드는 이유

  • 그리고 또 한 가지 중요한 점이 있는데 추상 클래스는 인스턴스로 만들 수가 없다.


  • 다음과 같이 추상 클래스 StudentBase로 인스턴스를 만들면 에러가 발생한다.


james = StudentBase() # TypeError: Can't instantiate abstract class StudentBase with abstract methods go_to_school, study


  • 그래서 지금까지 추상 메서드를 만들 때 pass만 넣어서 빈 메서드로 만든 것이다.
  • 왜냐하면 추상 클래스로 인스턴스를 만들 수 없으니 추상 메서드도 호출할 일이 없기 때문이다.
  • 추상 클래스는 인스턴스로 만들 때는 사용하지 않으며 오로지 상속에만 사용한다.
  • 그리고 파생 클래스에서 반드시 구현해야 할 메서드를 정해 줄 때 사용한다.

References