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. 클래스 상속 사용하기
- 클래스 상속은 물려받은 기능을 유지한채로 다른 기능을 추가할 때 사용하는 기능이다.
- 여기서 기능을 물려주는 클래스를 기반 클래스, 상속을 받아 새롭게 만드는 클래스를 파생 클래스라고 한다.
- 클래스 상속은 생물 분류를 떠올리면 이해하기 쉽다.
- 예를 들어 조류, 어류는 공통된 조상인 척추동물로부터 물려받은 특성을 공유하면서 각자 고유한 특성을 가진다.
- 척추를 가졌다는 특성은 변함이 없지만 날개를 가졌으면 조류, 물속에 살면 어류인 식이다.
- 즉, 같은 계통으로 특성을 공유하며 전혀 상관없이 어류가 꽃식물의 특성을 가지지는 않는다.
- 마찬가지로 클래스 상속도 기반 클래스의 능력을 그대로 활용하면서 새로운 클래스를 만들 때 사용한다.
- 만약 상속 개념을 적용하지 않는 경우, 새로운 기능이 필요할 때마다 계속 클래스를 만든다면 중복되는 부분을 반복해서 만들어야 한다.
- 이럴 때 상속을 사용하면 중복되는 기능을 만들지 않아도 되기 때문에 상속은 기존 기능을 재사용할 수 있어서 효율적이다.
3. 사람 클래스로 학생 클래스 만들기
- 클래스 상속은 다음과 같이 클래스를 만들 때
()
(괄호)를 붙이고 안에 기반 클래스 이름을 넣는다.
- 간단하게 사람 클래스를 만들고 사람 클래스를 상속받아 학생 클래스를 만들어보자.
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
메서드를 호출할 수 있다.
- 그리고
Student
클래스에 추가한 새로운 메서드인study
를 호출했다.
Person
클래스와Student
클래스의 관계를 그림으로 나타내면 다음과 같은 모양이 된다.
- 이처럼 클래스 상속은 기반 클래스의 기능을 유지하면서 새로운 기능을 추가할 수 있다.
- 특히 클래스 상속은 연관되면서 동등한 기능일 때 사용한다.
상속 관계 확인하기
- 클래스의 상속 관계를 확인하고 싶을 때는
issubclass
함수를 사용한다.
>>> class Person:
... pass
...
>>> class Student(Person):
... pass
...
>>> issubclass(Student, Person)
True
Student
가Person
의 파생 클래스이므로issubclass
는True
가 나온다.
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)
- 여기서는 상속을 사용하지 않고 속성에 인스턴스를 넣어서 관리하므로
PersonList
가Person
을 포함하고 있다. - 이러면 사람 목록
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()
뒤에.
(점)을 붙여서 메서드를 호출한다.
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
를 찾는 과정을 그림으로 나타내면 다음과 같은 모양이 된다.
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
를 넣어서 현재 클래스가 어떤 클래스인지 명확하게 표시하는 방법도 있다.
6. 메서드 오버라이딩 사용하기
- 이번에는 파생 클래스에서 기반 클래스의 메서드를 새로 정의하는 메서드 오버라이딩에 대해 알아보자.
- 다음과 같이
Person
의greeting
메서드가 있는 상태에서Student
에도greeting
메서드를 만든다.
class Person:
def greeting(self):
print("안녕하세요.")
class Student(Person):
def greeting(self):
print("안녕하세요. 저는 학생입니다.")
james = Student()
james.greeting() # 안녕하세요. 저는 학생입니다.
james.greeting()
처럼Student
의greeting
메서드를 호출하니"안녕하세요. 저는 학생입니다."
가 출력되었다.- 오버라이딩은 무시하다, 우선하다라는 뜻을 가지고 있는데 말 그대로 기반 클래스의 메서드를 무시하고 새로운 메서드를 만든다는 뜻이다.
- 여기서
Person
클래스의greeting
메서드를 무시하고Student
클래스에서 새로운greeting
메서드를 만들었다. - 보통 프로그램에서 어떤 기능이 같은 메서드 이름으로 계속 사용되어야 할 때 메서드 오버라이딩을 활용한다.
- 다시
Person
클래스의greeting
메서드와Student
클래스의greeting
메서드를 보면 "안녕하세요."라는 문구가 중복된다.
- 이럴 때는 기반 클래스의 메서드를 재활용하면 중복을 줄일 수 있다.
- 다음과 같이 오버라이딩된 메서드에서
super()
로 기반 클래스의 메서드를 호출해 본다.
class Person:
def greeting(self):
print("안녕하세요.")
class Student(Person):
def greeting(self):
super(Student, self).greeting()
print("저는 학생입니다.")
james = Student()
james.greeting()
# 안녕하세요.
# 저는 학생입니다.
Student
의greeting
에서super().greeting()
으로Person
의greeting
을 호출했다.- 즉, 중복되는 기능은 파생 클래스에서 다시 만들지 않고, 기반 클래스의 기능을 사용하면 된다.
- 이처럼 메서드 오버라이딩은 원래 기능을 유지하면서 새로운 기능을 덧붙일 때 사용한다.
7. 다중 상속 사용하기
- 다중 상속은 여러 기반 클래스로부터 상속을 받아서 파생 클래스를 만드는 방법이다.
- 다음과 같이 클래스를 만들 때
()
(괄호) 안에 클래스 이름을,
(콤마)로 구분해서 넣는다.
- 그럼 사람 클래스와 대학교 클래스를 만든 뒤 다중 상속으로 대학생 클래스를 만들어보자.
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() # 공부하기
- 먼저 기반 클래스
Person
과University
를 만들었다. - 그다음에 파생 클래스
Undergraduate
를 만들 때class Undergraduate(Person, University):
와 같이 괄호 안에Person
과University
를 콤마로 구분해서 넣었다. - 이렇게 하면 두 기반 클래스의 기능을 모두 상속받는다.
- 즉, 다음과 같이
Undergraduate
클래스의 인스턴스로Person
의greeting
과University
의manage_credit
을 호출할 수 있다.
james = Undergraduate()
james.greeting() # 안녕하세요.
james.manage_credit() # 학점 관리
james.study() # 공부하기
Person
,University
,Undergraduate
클래스의 관계를 그림으로 나타내면 다음과 같은 모양이 된다.
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
,C
는A
를 상속받는다. - 그리고 다시
D
는B
,C
를 상속받는다.
- 이 관계를 그림으로 나타내면 다음과 같은 모양이 된다.
- 객체지향 프로그래밍에서는 이런 상속 관계를 다이아몬드 상속이라고 부른다.
- 여기서는 클래스
A
를 상속받아서B
,C
를 만들고, 클래스B
와C
를 상속받아서D
를 만들었다. - 그리고
A
,B
,C
모두greeting
이라는 같은 메서드를 가지고 있기 때문에, 이런 경우D
가 호출해야 하는 클래스의 메서드를 알기 어렵다.
2) 메서드 탐색 순서 확인하기
- 많은 프로그래밍 언어들이 다이아몬드 상속에 대한 해결책을 제시하고 있는데, 파이썬에서는 메서드 탐색 순서(MRO)를 따른다.
- 다음과 같이 클래스
D
에 메서드mro
를 사용해 보면 메서드 탐색 순서가 나온다.
>> D.mro()
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
- MRO에 따르면
D
의 메서드 호출 순서는 자기자신D
, 그다음이B
이다. - 따라서
D
로 인스턴스를 만들고greeting
을 호출하면D
의greeting
은 없으므로B
의greeting
이 호출된다.
- 파이썬은 다중 상속을 한다면
class D(B, C):
의 클래스 목록 중 왼쪽에서 오른쪽 순서로 메서드를 찾는다. - 그러므로 같은 메서드가 있다면
B
가 우선한다. - 만약 상속 관계가 복잡하게 얽혀 있다면 MRO를 살펴보는 것이 편리하다.
object
클래스
- 파이썬에서
object
는 모든 클래스의 조상이다. - 그래서 다음과 같이
int
의 MRO를 출력해 보면int
자기자신과object
가 출력된다.
- 파이썬 3에서 모든 클래스는
object
클래스를 상속받으므로 기본적으로object
를 생략한다.
8. 추상 클래스 사용하기
- 추상 클래스는 메서드의 목록만 가진 클래스이며 상속받는 클래스에서 메서드 구현을 강제하기 위해 사용한다.
- 먼저 추상 클래스를 만들려면
import
로abc
모듈을 가져와야 하는데, 여기서abc
는 Abstract Base Class의 약자이다. - 그리고 클래스의
()
(괄호) 안에metaclass=ABCMeta
를 지정하고, 메서드를 만들 때 위에@abstractmethod
를 붙여서 추상 메서드로 지정한다.
- 여기서는
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
에서는 추상 메서드로study
와go_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
만 넣어서 빈 메서드로 만든 것이다. - 왜냐하면 추상 클래스로 인스턴스를 만들 수 없으니 추상 메서드도 호출할 일이 없기 때문이다.
- 추상 클래스는 인스턴스로 만들 때는 사용하지 않으며 오로지 상속에만 사용한다.
- 그리고 파생 클래스에서 반드시 구현해야 할 메서드를 정해 줄 때 사용한다.