앞에서 클래스의 기본 개념부터 property, Special Method, Callable Object, 클래스 데코레이터까지 살펴봤습니다.
이번에는 객체지향 프로그래밍에서 매우 중요한 개념인 상속(Inheritance)을 알아보겠습니다.
상속은 이미 만들어진 클래스의 기능을 새로운 클래스가 물려받아 사용할 수 있도록 하는 기능입니다.
예를 들어 동물이라는 공통적인 클래스를 만들었다고 해보겠습니다.
class Animal:
def eat(self):
print("먹습니다.")
def sleep(self):
print("잡니다.")
이제 강아지 클래스를 만들 때 eat()과 sleep()을 다시 작성할 필요 없이 Animal을 상속받을 수 있습니다.
class Dog(Animal):
def bark(self):
print("멍멍")
그러면 Dog 객체는 자신이 직접 작성한 bark()뿐만 아니라 Animal에서 물려받은 eat()과 sleep()도 사용할 수 있습니다.
이번 글에서는 이런 상속의 기본 개념과 실제 사용 방법을 알아보겠습니다.
1. 상속(Inheritance)이란?
상속은 하나의 클래스가 다른 클래스의 속성과 메서드를 물려받는 기능입니다.
상속을 받는 클래스를 자식 클래스(Child Class) 또는 **파생 클래스(Derived Class)**라고 합니다.
상속을 제공하는 클래스를 부모 클래스(Parent Class) 또는 **기반 클래스(Base Class)**라고 합니다.
구조를 보면 다음과 같습니다.
부모 클래스
↓
상속
↓
자식 클래스
예를 들어:
class Animal:
def eat(self):
print("먹습니다.")
class Dog(Animal):
def bark(self):
print("멍멍")
여기서:
Animal → 부모 클래스
Dog → 자식 클래스
입니다.
Dog 클래스에서 Animal을 괄호 안에 작성한 것이 상속을 의미합니다.
class Dog(Animal):
2. 부모 클래스의 메서드 사용하기
가장 기본적인 상속 예제를 살펴보겠습니다.
class Animal:
def eat(self):
print("먹습니다.")
def sleep(self):
print("잡니다.")
class Dog(Animal):
def bark(self):
print("멍멍")
이제 Dog 객체를 만들어 보겠습니다.
dog = Dog()
dog.eat()
dog.sleep()
dog.bark()
결과:
먹습니다.
잡니다.
멍멍
Dog 클래스에는 eat()과 sleep()이 없습니다.
그런데도 사용할 수 있습니다.
왜냐하면 Dog가 Animal을 상속받았기 때문입니다.
즉, 부모 클래스의 기능을 자식 클래스에서 그대로 사용할 수 있습니다.
3. 자식 클래스에 새로운 기능 추가하기
상속의 장점은 부모 클래스의 기능을 그대로 사용하는 것뿐만 아니라 자식 클래스만의 새로운 기능을 추가할 수 있다는 것입니다.
class Animal:
def eat(self):
print("먹습니다.")
class Dog(Animal):
def bark(self):
print("멍멍")
def run(self):
print("달립니다.")
이제 Dog는 다음과 같은 기능을 가지고 있습니다.
dog = Dog()
dog.eat()
dog.bark()
dog.run()
결과:
먹습니다.
멍멍
달립니다.
정리하면:
Animal
└── eat()
Dog
├── eat() ← 부모에게 상속
├── bark() ← Dog에서 추가
└── run() ← Dog에서 추가
이처럼 공통 기능은 부모 클래스에 작성하고, 특정 클래스에만 필요한 기능은 자식 클래스에 추가할 수 있습니다.
4. 왜 상속을 사용할까?
상속을 사용하는 가장 큰 이유 중 하나는 코드의 중복을 줄이는 것입니다.
예를 들어 강아지와 고양이가 있다고 해보겠습니다.
상속을 사용하지 않는다면 다음처럼 작성할 수 있습니다.
class Dog:
def eat(self):
print("먹습니다.")
def sleep(self):
print("잡니다.")
def bark(self):
print("멍멍")
class Cat:
def eat(self):
print("먹습니다.")
def sleep(self):
print("잡니다.")
def meow(self):
print("야옹")
eat()과 sleep()이 중복됩니다.
이런 공통 기능을 부모 클래스로 분리할 수 있습니다.
class Animal:
def eat(self):
print("먹습니다.")
def sleep(self):
print("잡니다.")
class Dog(Animal):
def bark(self):
print("멍멍")
class Cat(Animal):
def meow(self):
print("야옹")
이제 공통 기능은 Animal에서 한 번만 관리하면 됩니다.
Animal
/ \
Dog Cat
Dog와 Cat은 공통 기능을 물려받고 각자의 기능만 추가합니다.
5. 생성자 __init__()도 상속할 수 있을까?
가능합니다.
부모 클래스에 __init__()이 있다면 자식 클래스에서 별도의 __init__()을 정의하지 않는 경우 부모의 __init__()을 사용할 수 있습니다.
class Person:
def __init__(self, name):
self.name = name
class Student(Person):
pass
이제:
student = Student("Kim")
print(student.name)
결과:
Kim
Student에 __init__()이 없기 때문에 부모 클래스인 Person의 __init__()을 사용할 수 있습니다.
6. 자식 클래스에서 __init__()을 새로 정의하면?
자식 클래스에서 __init__()을 직접 정의하면 부모의 __init__()이 자동으로 실행되는 것은 아닙니다.
예를 들어:
class Person:
def __init__(self, name):
self.name = name
class Student(Person):
def __init__(self, name, school):
self.school = school
이 경우:
student = Student("Kim", "ABC High School")
print(student.school)
은 정상적으로 동작하지만,
print(student.name)
은 name이 설정되지 않았기 때문에 문제가 발생합니다.
자식 클래스의 __init__()에서 부모의 초기화 작업을 별도로 호출하지 않았기 때문입니다.
이럴 때 사용하는 것이 super()입니다.
7. super()란?
super()는 자식 클래스에서 부모 클래스의 메서드를 호출할 때 사용하는 기능입니다.
다음과 같이 사용할 수 있습니다.
class Person:
def __init__(self, name):
self.name = name
class Student(Person):
def __init__(self, name, school):
super().__init__(name)
self.school = school
이제:
student = Student("Kim", "ABC High School")
print(student.name)
print(student.school)
결과:
Kim
ABC High School
여기서:
super().__init__(name)
은 부모 클래스인 Person의 __init__()을 호출합니다.
즉,
self.name = name
이 부모 클래스에서 실행됩니다.
그리고 자식 클래스에서는 추가적인 초기화 작업을 수행합니다.
self.school = school
8. super()는 __init__()에서만 사용하는 것이 아니다
super()는 부모 클래스의 다른 메서드를 호출할 때도 사용할 수 있습니다.
class Animal:
def speak(self):
print("동물이 소리를 냅니다.")
class Dog(Animal):
def speak(self):
super().speak()
print("멍멍")
사용하면:
dog = Dog()
dog.speak()
결과:
동물이 소리를 냅니다.
멍멍
자식 클래스에서 부모 클래스의 기능을 먼저 실행하고 추가적인 작업을 수행할 수 있습니다.
9. 메서드 오버라이딩(Overriding)
상속에서 매우 중요한 개념이 메서드 오버라이딩(Method Overriding)입니다.
오버라이딩은 자식 클래스에서 부모 클래스의 메서드를 같은 이름으로 다시 정의하는 것입니다.
예를 들어:
class Animal:
def speak(self):
print("동물이 소리를 냅니다.")
class Dog(Animal):
def speak(self):
print("멍멍")
Dog는 Animal의 speak()를 상속받지만 같은 이름의 메서드를 다시 정의했습니다.
따라서:
dog = Dog()
dog.speak()
결과는:
멍멍
입니다.
부모 클래스의 speak() 대신 자식 클래스의 speak()이 실행됩니다.
10. 오버라이딩을 사용하는 이유
동일한 기능이지만 클래스마다 동작이 달라야 하는 경우가 있습니다.
예를 들어 모든 동물에게 speak()라는 기능은 있지만 실제 소리는 다릅니다.
class Animal:
def speak(self):
print("소리를 냅니다.")
class Dog(Animal):
def speak(self):
print("멍멍")
class Cat(Animal):
def speak(self):
print("야옹")
각 객체를 호출하면:
dog = Dog()
cat = Cat()
dog.speak()
cat.speak()
결과:
멍멍
야옹
같은 speak()라는 메서드를 호출했지만 객체에 따라 다른 결과가 나옵니다.
이런 구조는 객체지향 프로그래밍에서 매우 중요한 개념으로 연결됩니다.
11. 부모와 자식의 기능을 함께 사용하기
오버라이딩을 하면서 부모 클래스의 기능도 사용하고 싶을 수 있습니다.
이때 super()를 사용합니다.
class Animal:
def speak(self):
print("동물이 소리를 냅니다.")
class Dog(Animal):
def speak(self):
super().speak()
print("멍멍")
이제:
dog = Dog()
dog.speak()
결과:
동물이 소리를 냅니다.
멍멍
즉, 자식 클래스가 부모의 기능을 완전히 대체하는 것이 아니라 부모 기능을 확장하는 형태로 사용할 수 있습니다.
12. isinstance()로 상속 관계 확인하기
파이썬에서는 객체가 특정 클래스의 인스턴스인지 확인할 수 있습니다.
class Animal:
pass
class Dog(Animal):
pass
dog = Dog()
print(isinstance(dog, Dog))
print(isinstance(dog, Animal))
결과:
True
True
dog는 Dog의 객체이면서 Animal을 상속받았기 때문에 Animal의 인스턴스로도 판단됩니다.
반면:
animal = Animal()
print(isinstance(animal, Dog))
결과:
False
부모 클래스의 객체라고 해서 자식 클래스의 인스턴스가 되는 것은 아닙니다.
13. issubclass()로 클래스의 상속 관계 확인하기
객체가 아니라 클래스 자체의 상속 관계를 확인하려면 issubclass()를 사용할 수 있습니다.
class Animal:
pass
class Dog(Animal):
pass
다음과 같이 확인할 수 있습니다.
print(issubclass(Dog, Animal))
결과:
True
반대로:
print(issubclass(Animal, Dog))
결과:
False
정리하면:
isinstance()
→ 객체가 특정 클래스의 인스턴스인지 확인
issubclass()
→ 클래스가 다른 클래스를 상속받았는지 확인
14. 다중 상속(Multiple Inheritance)
파이썬에서는 하나의 클래스가 여러 클래스를 동시에 상속받는 것도 가능합니다.
이를 다중 상속(Multiple Inheritance)이라고 합니다.
class Flyer:
def fly(self):
print("날아갑니다.")
class Swimmer:
def swim(self):
print("수영합니다.")
class Duck(Flyer, Swimmer):
pass
이제 Duck은 두 부모 클래스의 기능을 모두 사용할 수 있습니다.
duck = Duck()
duck.fly()
duck.swim()
결과:
날아갑니다.
수영합니다.
구조는 다음과 같습니다.
Flyer ─────┐
├── Duck
Swimmer ───┘
다중 상속은 강력하지만 여러 부모 클래스에 같은 이름의 메서드가 존재할 경우 어떤 메서드를 사용할지 고려해야 합니다.
이때 파이썬의 MRO(Method Resolution Order)가 사용됩니다.
MRO는 클래스의 메서드를 어떤 순서로 찾을지 결정하는 규칙입니다.
15. MRO 확인하기
클래스의 MRO는 __mro__ 또는 mro()를 사용해서 확인할 수 있습니다.
class A:
pass
class B(A):
pass
print(B.mro())
결과는 대략 다음과 같은 형태입니다.
[<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
즉, B에서 메서드를 찾고 없으면 A에서 찾고, 그래도 없으면 최종적으로 object에서 찾습니다.
다중 상속에서는 이 순서가 더 중요해집니다.
16. 모든 클래스의 최상위에는 object가 있다
파이썬의 클래스는 결국 object라는 기본 클래스를 기반으로 합니다.
직접 상속을 작성하지 않아도:
class Person:
pass
개념적으로는 object와 연결되어 있습니다.
class Person(object):
pass
두 방식은 파이썬 3에서 사실상 같은 의미입니다.
그래서 다음과 같이 확인할 수도 있습니다.
print(issubclass(Person, object))
결과:
True
object는 파이썬 객체 시스템의 가장 기본이 되는 클래스라고 이해하면 됩니다.
17. 상속을 사용할 때 주의할 점
상속은 코드를 재사용하는 데 유용하지만 무조건 많이 사용하는 것이 좋은 것은 아닙니다.
특히 클래스 사이에 실제로 “is-a” 관계가 있는지 생각해 보는 것이 중요합니다.
예를 들어:
Dog is an Animal
이라는 관계는 자연스럽습니다.
따라서:
class Dog(Animal):
이 적절합니다.
반면:
Car is an Engine
은 일반적으로 자연스러운 관계가 아닙니다.
자동차가 엔진을 가지고 있는 것이지 자동차가 엔진의 한 종류인 것은 아니기 때문입니다.
이런 경우에는 상속보다는 객체를 속성으로 포함하는 구성(Composition)이 더 적합할 수 있습니다.
class Engine:
pass
class Car:
def __init__(self):
self.engine = Engine()
즉,
is-a 관계
→ 상속을 고려
has-a 관계
→ 객체를 포함하는 구성(Composition)을 고려
라고 생각하면 이해하기 쉽습니다.
18. 상속의 핵심 정리
이번 글에서 배운 내용을 정리하면 다음과 같습니다.
부모 클래스
공통적인 속성과 기능을 정의합니다.
class Animal:
def eat(self):
print("먹습니다.")
자식 클래스
부모 클래스를 상속받고 자신만의 기능을 추가할 수 있습니다.
class Dog(Animal):
def bark(self):
print("멍멍")
메서드 오버라이딩
부모 클래스의 메서드를 자식 클래스에서 다시 정의합니다.
class Dog(Animal):
def speak(self):
print("멍멍")
super()
부모 클래스의 메서드를 호출합니다.
super().speak()
isinstance()
객체의 인스턴스 관계를 확인합니다.
isinstance(dog, Animal)
issubclass()
클래스의 상속 관계를 확인합니다.
issubclass(Dog, Animal)
마무리
이번 글에서는 객체지향 프로그래밍에서 중요한 상속(Inheritance)을 알아봤습니다.
상속을 사용하면 공통적인 기능을 부모 클래스에 정의하고, 자식 클래스에서는 이를 재사용하면서 자신만의 기능을 추가할 수 있습니다.
가장 기본적인 구조는 다음과 같습니다.
class Animal:
def eat(self):
print("먹습니다.")
class Dog(Animal):
def bark(self):
print("멍멍")
그리고 자식 클래스에서 부모의 기능을 확장하거나 변경할 때는 super()와 메서드 오버라이딩을 사용할 수 있습니다.
class Dog(Animal):
def eat(self):
super().eat()
print("강아지가 먹습니다.")
다만 상속을 사용할 때는 단순히 코드를 재사용하기 위해 무조건 부모 클래스를 만드는 것보다 클래스 사이에 실제로 자연스러운 관계가 있는지 생각하는 것이 중요합니다.
다음 글에서는 클래스 시리즈의 마지막 주제인 추상 클래스와 추상 메서드(Abstract Class & Abstract Method)를 살펴보겠습니다.
추상 클래스가 왜 필요한지, ABC, @abstractmethod를 어떻게 사용하는지, 그리고 상속과 어떤 차이가 있는지 알아보겠습니다.