1. 상속(Inheritance)

 

오늘은 클래스의 상속(Inheritance)에 대해 알아보려고 합니다. 상속이란 무엇일까요? 우리가 알고 있는 상속은 사전적 정의에 따르면 "일정한 친족적 관계가 있는 사람 사이에 한 쪽이 사망하거나 법률상의 원인이 발생하였을 때 재산적 또는 친족적 권리와 의무를 계승하는 제도"와 같습니다. 그런데 이 상속이란 개념이 파이썬의 클래스에도 존재합니다. 쉽게 말해서, 부모의 유산을 자식이 물려 받듯이 부모 클래스와 자식 클래스라는 것이 존재하여 부모 클래스의 멤버를 자식 클래스가 물려받을 수 있습니다.

위 그림처럼 상속 관계에 있다고 가정해봅시다. 먼저 사람(Person) 클래스는 부모가 되는 클래스이니 부모 클래스라고 말하고, 근로자(Employee) 클래스는 자식이 되는 클래스이니 자식 클래스라 말합니다. Employee 클래스는 어찌보면 Person의 영역 안에 속하는 것이므로 상속을 통해 Person 클래스의 멤버(함수, 데이터)를 물려받아 Employee 클래스에서 그대로 사용할 수 있습니다. 아직까지 상속이란 녀석이 어떤 녀석인지 모르시겠다면 아래의 예제를 우선 보도록 합시다.

>>> class Person:
	def __init__(self, name, age, gender):
		self.Name = name
		self.Age = age
		self.Gender = gender
	def aboutMe(self):
		print("저의 이름은 " + self.Name + "이구요, 제 나이는 " + self.Age + "살 입니다.")
		
>>> class Employee(Person):
	def __init__(self, name, age, gender, salary, hiredate):
		Person.__init__(self, name, age, gender)
		self.Salary = salary
		self.Hiredate = hiredate
	def doWork(self):
		print("열심히 일을 합니다.")
	def aboutMe(self):
		Person.aboutMe(self)
		print("제 급여는 " + self.Salary + "원 이구요, 제 입사일은 " + self.Hiredate + " 입니다.")

>>> objectEmployee = Employee("김철수", "18", "남", "5000000", "2013년 10월 28일")
>>> objectEmployee.doWork()
열심히 일을 합니다.
>>> objectEmployee.aboutMe()
저의 이름은 김철수이구요, 제 나이는 18살 입니다.
제 급여는 5000000원 이구요, 제 입사일은 2013년 10월 28일 입니다.

위 예제를 보시면, Person이란 클래스와 Person 클래스를 상속받는 Employee 클래스가 정의되어 있습니다. 아직은 모르는 것 투성이지만, 차근차근 예제의 코드를 살펴보도록 합시다. 우선은 Person 클래스의 내부부터 보도록 합시다. Person 클래스의 생성자에서는 이름, 나이, 성별을 인자로 넘겨받고 self.Name, self.Age, self.Gender의 값을 초기화 시키는 것을 보실 수 있습니다. 또한 aboutMe라는 함수는 이름과 나이를 출력하는 함수입니다. 그 다음으로 Employee 클래스를 보도록 합시다.

>>> class Employee(Person):

이 부분을 자세히 보시면, 클래스 이름 뒤에 괄호가 등장하여 괄호 안에 클래스 이름이 또다시 등장하는 것을 보실 수 있습니다. 이것은 Employee 클래스가 Person 클래스를 상속받는다는 의미이며, 이는 아래와 같이 표현이 된다는 것을 알 수 있습니다.

class 클래스명(상속받을 클래스명):

그리고 Employee 클래스가 Person 클래스를 상속받으니, Employee 클래스는 멤버를 물려받는 자식 클래스라 말할 수 있으며 Person 클래스는 멤버를 물려주는 부모 클래스라고 말할 수 있습니다. 이어서 Employee 클래스의 생성자 부분을 보도록 합시다. 생성자 부분을 보시면 특이한 부분을 보실 수 있는데, 한번 같이 보도록 합시다.

...
	def __init__(self, name, age, gender, salary, hiredate):
		Person.__init__(self, name, age, gender)
		self.Salary = salary
		self.Hiredate = hiredate
...

위의 생성자에선 이름, 나이, 성별, 급여, 입사일을 인자로 넘겨받는다는 것을 보실 수 있습니다. 그런데 특이한건, 위 코드의 3행을 보시면 부모 클래스의 생성자를 호출하면서 넘겨받은 인자를 부모 클래스의 생성자로 넘겨주는데, 이는 명시적으로 Person 클래스의 생성자를 호출하는 방법입니다. (여기서는 'self'를 함께 전달해야 합니다) 이어서, Employee 클래스의 aboutMe 함수를 보도록 하겠습니다.

...
	def aboutMe(self):
		Person.aboutMe(self)
		print("제 급여는 " + self.Salary + "원 이구요, 제 입사일은 " + self.Hiredate + " 입니다.")
...

위의 aboutMe 함수를 살펴보시면 이 함수에서도 명시적으로 Person 클래스의 aboutMe 함수를 호출하는 것을 보실 수 있습니다. 부모 클래스의 aboutMe 함수가 호출되고 나서는 급여와 입사일을 출력합니다. 이렇게 보니, 참으로 상속이란 특징이 쓸모있다고 여겨지지 않나요? 이 상속이란 특징을 이용하면, 유지 보수가 쉬워지거나 중복되는 코드가 적어지는 등의 장점이 존재합니다. 위의 예제처럼 한개의 클래스를 상속받는 경우도 있지만, 두개 이상의 클래스를 상속 받는 경우도 있을 수 있습니다. 한번 보실까요?

 

2. 다중 상속(Multiple Inheritance)

 

다중 상속이란 두개 이상의 클래스를 상속받는 것을 말하는데, 이 경우에는 두 클래스의 모든 속성을 물려받게 됩니다. 이는 하나의 자식 클래스가 두개 이상의 부모 클래스를 가지는 것이라고 할 수 있습니다. 아래 그림처럼 말입니다.

다중 상속의 예제를 한번 보도록 합시다. 그리고 차근차근 예제의 코드를 살펴보도록 합시다.

>>> class ParentOne:
	def func(self):
		print("ParentOne의 함수 호출!")
	
>>> class ParentTwo:
	def func(self):
		print("ParentTwo의 함수 호출!")
	
>>> class Child(ParentOne, ParentTwo):
	def childFunc(self):
		ParentOne.func(self)
		ParentTwo.func(self)
		
>>> objectChild = Child()
>>> objectChild.childFunc()
ParentOne의 함수 호출!
ParentTwo의 함수 호출!
>>> objectChild.func()
ParentOne의 함수 호출!

위의 예제 코드에서는 부모 클래스인 ParentOne, ParentTwo라는 클래스가 정의되어 있으며, 자식 클래스인 Child라는 클래스도 정의되어 있습니다. 위의 코드에서 9행을 한번 보실까요?

>>> class Child(ParentOne, ParentTwo):

위 코드에서는 2개 이상의 클래스를 상속 받을때 콤마를 기준으로 상속받을 클래스의 이름을 나열하고 있는 것을 보실 수 있습니다. (여기서 상속받을 클래스의 나열 순서가 검색 결과에 영향을 끼칩니다) 그리고 주의깊게 보셔야 할 부분은, 18~19행의 부분인데 한번 보도록 합시다. 

>>> objectChild.func()
ParentOne의 함수 호출!

위 부분을 보시면 objectChild의 func 함수를 호출하는데, 여기서 ParentOne과 ParentTwo 중 어떤 클래스의 func 함수가 호출되는지 보았더니 ParentOne 클래스의 func 함수가 호출된 모습을 보실 수 있습니다. 이는, 우리가 상속받을 클래스의 이름을 나열할때 순서에 따라 이름을 찾기 때문입니다. 즉, ParentOne 클래스의 이름공간에서 func를 찾는다는 것입니다. 다중 상속에 대해 이해가 가시나요? 그런데 다중 상속을 이용할때도 주의하셔야 할 점이 있는데, 아래와 같은 다이아몬드 상속을 하는 경우 문제가 발생합니다.

위 그림을 보시면 B와 C 클래스가 A 클래스를 상속받고 있으며, 다시 D 클래스가 B와 C 클래스를 상속받고 있습니다. 위 처럼 다이아몬드 모양의 상속 구조에서는 아래와 같은 문제가 발생할 수 있습니다.

>>> class A:
	def __init__(self):
		print("A 클래스의 생성자 호출!")
		
>>> class B(A):
	def __init__(self):
		print("B 클래스의 생성자 호출!")
		A.__init__(self)
	
>>> class C(A):
	def __init__(self):
		print("C 클래스의 생성자 호출!")
		A.__init__(self)
	
>>> class D(B, C):
	def __init__(self):
		print("D 클래스의 생성자 호출!")
		B.__init__(self)
		C.__init__(self)
	
>>> objectD = D()
D 클래스의 생성자 호출!
B 클래스의 생성자 호출!
A 클래스의 생성자 호출!
C 클래스의 생성자 호출!
A 클래스의 생성자 호출!

위 예제를 보시면, B와 C에선 부모 클래스인 A의 생성자를 호출하고 있으며 D에서는 부모 클래스인 B와 C 클래스의 생성자를 호출하고 있는 모습을 보실 수 있습니다. 이 상태에서 D의 인스턴스 객체를 생성하게 되면, A 클래스의 생성자가 두번이나 호출되고 있는 모습을 보실 수 있습니다. 이 상황을 피하기 위해서 super라는 내장 함수를 사용할 수 있으며, 이 함수는 부모 클래스의 객체를 반환하게끔 되어 있습니다. 한번 내장 함수인 super 함수를 이용하여 위의 예제를 고쳐보도록 하겠습니다.

>>> class A:
	def __init__(self):
		print("A 클래스의 생성자 호출!")

>>> class B(A):
	def __init__(self):
		print("B 클래스의 생성자 호출!")
		super().__init__()

>>> class C(A):
	def __init__(self):
		print("C 클래스의 생성자 호출!")
		super().__init__()

>>> class D(B, C):
	def __init__(self):
		print("D 클래스의 생성자 호출!")
		super().__init__()
	
>>> objectD = D()
D 클래스의 생성자 호출!
B 클래스의 생성자 호출!
C 클래스의 생성자 호출!
A 클래스의 생성자 호출!

위에서 super 함수를 통하여 생성자를 호출하도록 하니, 정상적으로 모든 클래스의 생성자가 한번씩 호출되는 결과를 얻을 수 있습니다. 여기서 B와 C 클래스가 A 클래스의 자식인 것을 생각하여 인터프리터가 A 클래스의 생성자가 두번이나 호출되는 것을 피하는 방법입니다. 괜찮죠? 다중 상속이 필요하여 사용할 경우에는 다중 상속에 대해 충분한 이해를 하신 뒤에 다중 상속을 사용하시는 것을 권해드립니다.