Python 강좌 : 제 27강 - 클래스
클래스(class)
클래스(class)는 객체 지향 프로그래밍(OOP)에서 특정한 객체를 생성하기 위해서 속성(변수)과 메서드(함수) 등을 정의하는 틀입니다.
프로그램을 구성할 때, 동일한 코드나 알고리즘이 반복되거나 함수들을 하나로 묶어주는 공간이 필요하게 됩니다.
클래스를 사용하게 되면 재사용성, 가독성, 간결화된 코드를 구현할 수 있습니다.
예를 들어, 동일한 함수를 사용하더라도 변수를 추가로 더 많이 만들지 않아도 되며 어떤 함수를 실행하기 위해서 선행되어야 하는 함수 등도 중복해서 구문에 포함하지 않아도 됩니다.
지금까지는 데이터를 목록(list)이나 사전(dict)에 저장하고, 그 데이터를 다루는 기능은 별도의 함수로 작성했습니다.
이 방식은 데이터와 기능이 서로 떨어져 있기 때문에, 프로그램이 커질수록 어떤 함수가 어떤 데이터를 다루는지 파악하기 어려워집니다.
클래스는 데이터(속성)와 그 데이터를 다루는 기능(메서드)을 하나로 묶는 방법입니다.
이번 강좌에서는 클래스를 정의하는 방법, 클래스로 만든 인스턴스, 메서드의 첫 번째 매개변수인 self, 인스턴스마다 따로 저장되는 속성, 그리고 기존 클래스를 물려받는 상속을 차례대로 다룹니다.
# 클래스 없이 사전과 함수로 관리
def make_person(name, age):
return {"name": name, "age": age}
def introduce(person):
print("이름은 " + person["name"] + "이고, 나이는 " + str(person["age"]))
p1 = make_person("박XX", 10)
p2 = make_person("김XX", 30)
introduce(p1)
introduce(p2)- 결과
- 이름은 박XX이고, 나이는 10
이름은 김XX이고, 나이는 30
위 예제는 클래스 없이 사람의 정보를 사전으로 만들고, 함수로 출력합니다.
동작에는 문제가 없지만, introduce 함수는 "name"과 "age" 키를 가진 사전이 들어온다고 가정하고 있을 뿐입니다.
키 이름을 잘못 적은 사전을 전달하거나, 사람과 관계없는 사전을 전달해도 막을 방법이 없습니다.
또한, 사람과 관련된 함수가 늘어날수록 함수들이 파일 곳곳에 흩어지게 됩니다.
클래스를 사용하면 사람이라는 개념의 데이터 구조와 기능을 한 곳에 정의할 수 있습니다.
클래스 정의와 사용
아래 예제는 사람을 표현하는 Human 클래스를 정의하고, 세 개의 인스턴스를 만들어 사용합니다.
먼저 전체 코드와 결과를 확인한 다음, 구성 요소를 하나씩 나누어 살펴봅니다.
class Human:
def __init__(self):
self.name = "알 수 없음"
self.age = 99
def man(self, name, age=10):
self.name = name
self.age = age
def woman(self, name, age):
self.name = name
self.age = age
def prt(self):
print("이름은 " + self.name + "이고, 나이는 " + str(self.age))
a = Human()
a.man("박XX")
b = Human()
b.woman("김XX", 30)
c = Human()
a.prt()
b.prt()
print(vars(c))- 결과
- 이름은 박XX이고, 나이는 10
이름은 김XX이고, 나이는 30
{‘name’: ‘알 수 없음’, ‘age’: 99}
결과의 첫 번째 줄은 a.man("박XX")으로 이름만 전달했기 때문에, 나이가 기본값인 10으로 출력됩니다.
두 번째 줄은 b.woman("김XX", 30)으로 이름과 나이를 모두 전달한 결과입니다.
세 번째 줄은 아무 메서드도 호출하지 않은 c의 속성으로, __init__에서 설정한 초기값이 그대로 남아 있습니다.
세 인스턴스는 같은 클래스로 만들었지만 서로 다른 값을 가지고 있습니다.
class Human:
...class 클래스 이름:을 사용하여 클래스의 이름을 설정할 수 있습니다.
클래스 정의 구문은 실행되는 순간 클래스 객체를 만들어 Human이라는 이름에 할당합니다. 이 시점에는 아직 사람 한 명도 만들어지지 않았습니다.
Python의 클래스 이름 표기 방식은 PEP 8에 따라 각 단어의 첫 글자를 대문자로 표기하는 CapWords(파스칼 표기법, PascalCase) 방식을 따릅니다.
예를 들어, Human, HumanInfo와 같이 표기합니다.
함수와 변수는 snake_case, 클래스는 CapWords로 표기하므로, 이름만 보고도 클래스인지 함수인지 구분할 수 있습니다.
- Tip :
...(Ellipsis)나pass는 아무 동작도 하지 않는 구문으로, 비어 있는 클래스나 함수를 정의할 때 사용합니다.
class Human:
def __init__(self):
self.name = "알 수 없음"
self.age = 99def __init__(self)을 통해 인스턴스가 생성되었을 때의 변수나 조건 등을 초기화 및 실행할 수 있습니다.
__init__ 메서드는 인스턴스가 생성된 직후 자동으로 호출되는 초기화 메서드입니다.
Human()을 호출하면 Python은 먼저 비어 있는 인스턴스를 만들고, 그 인스턴스를 self로 전달하여 __init__을 실행합니다.
self.변수 이름을 통하여 인스턴스에서 사용될 변수들의 초기값을 할당할 수 있습니다.
name 변수와 age 변수에 아무런 값도 할당되지 않는다면, 알 수 없음과 99의 값이 각각 할당됩니다.
- Tip :
__init__은 값을 반환하지 않아야 합니다.return으로None이 아닌 값을 반환하면TypeError가 발생합니다.
class Human:
def man(self, name, age=10):
self.name = name
self.age = ageman 메서드는 self, name과 age를 매개변수로 사용합니다.
self.name과 name은 서로 다른 변수입니다.
self.name은 인스턴스의 속성으로 인스턴스 전반에 걸쳐 영향을 미치며, name은 메서드 내부에서만 사용되는 지역 변수입니다.
메서드가 끝나면 지역 변수 name은 사라지지만, self.name에 저장한 값은 인스턴스에 남아 다른 메서드에서도 사용할 수 있습니다.
age의 값은 10으로 초기설정이 되어있으므로, age 값을 입력하지 않는다면 10으로 자동 할당됩니다.
class Human:
def woman(self, name, age):
self.name = name
self.age = agewoman 메서드는 name과 age를 매개변수로 사용하며 두 개의 인자를 모두 전달해야 오류가 발생하지 않습니다.
만약, 특정 메서드에서 self.변수 이름으로 선언한다면, 인스턴스의 다른 메서드에서도 불러와 사용할 수 있습니다.
__init__에서 초기화하지 않더라도 일반 메서드에서도 속성의 생성, 삭제, 변경 등이 가능합니다.
다만, 모든 속성을 __init__에서 초기화해 두어야 어떤 속성이 존재하는지 파악하기 쉽습니다.
__init__에서 초기화하지 않은 속성을 메서드 호출 전에 읽으면 AttributeError가 발생하므로, 초기값을 정해 두는 편이 안전합니다.
class Human:
def prt(self):
print("이름은 " + self.name + "이고, 나이는 " + str(self.age))prt 메서드는 출력용 메서드이며, 값을 입력하지 않고 호출만으로도 메서드가 실행됩니다.
메서드에서 어떤 값도 입력받지 않더라도, self는 선언해야 사용할 수 있습니다.
prt 메서드는 인자를 받지 않는 대신, self를 통해 인스턴스에 저장된 값을 읽어 출력합니다.
a = Human()
a.man("박XX")
b = Human()
b.woman("김XX", 30)
c = Human()
a.prt()
b.prt()
print(vars(c))위 코드는 첫 번째 예제의 Human 클래스 정의가 있어야 실행할 수 있습니다.
클래스 이름()으로 클래스를 호출하여 인스턴스를 생성하고, 인스턴스.메서드 이름()으로 클래스의 메서드를 실행시킬 수 있습니다.
인스턴스.메서드 이름(인자1, 인자2)를 이용하여 a와 b 인스턴스에 값을 할당할 수 있습니다.
*.prt() 메서드는 self 외에 아무런 인자를 필요로 하지 않으므로, 바로 사용이 가능합니다.
vars(인스턴스)를 사용하여 인스턴스에 할당된 속성을 사전 형식으로 확인할 수 있습니다.
c 인스턴스에는 __init__(self)를 통하여 초기화한 값인 알 수 없음과 99의 값이 할당되어 있습니다.
- Tip : 속성을 저장하는 용도의 클래스는
dataclasses모듈의@dataclass데코레이터를 사용하면__init__메서드 등을 자동으로 생성할 수 있습니다. (Python 3.7 이상)
클래스와 인스턴스
클래스와 인스턴스의 관계는 설계도와 그 설계도로 만든 제품에 비유할 수 있습니다.
클래스는 “사람은 이름과 나이를 가지고, 자기소개를 할 수 있다”처럼 어떤 속성과 기능을 가질지 정의한 틀입니다.
인스턴스(instance)는 그 틀을 이용해 실제로 만든 하나하나의 객체입니다. 하나의 클래스로 인스턴스를 원하는 만큼 만들 수 있습니다.
class Human:
pass
a = Human()
b = Human()
print(type(a))
print(isinstance(a, Human))
print(a is b)
print(a == b)- 결과
- <class ‘__main__.Human’>
True
False
False
type(a)는 a를 만든 클래스를 반환합니다. __main__은 현재 실행 중인 파일(모듈)에서 정의된 클래스라는 의미입니다.
isinstance(a, Human)은 a가 Human 클래스의 인스턴스인지 확인하며, True를 반환합니다.
a is b가 False인 것은 Human()을 호출할 때마다 새로운 객체가 만들어진다는 의미입니다.
a == b도 False가 됩니다. 클래스에서 비교 방법(__eq__)을 따로 정의하지 않으면, ==는 is와 같이 같은 객체인지를 비교하기 때문입니다.
정리하면, 클래스를 한 번 정의해 두고 인스턴스는 필요한 만큼 생성하여 사용합니다.
Human 클래스 자체는 이름이나 나이를 가지지 않으며, 이름과 나이는 각각의 인스턴스가 가집니다.
자기 자신(self)
메서드는 클래스 안에 한 번만 정의되어 있지만, 여러 인스턴스가 같은 메서드를 함께 사용합니다.
그러므로, 메서드가 실행될 때 지금 어떤 인스턴스의 데이터를 다뤄야 하는지 알려 줄 방법이 필요합니다.
그 역할을 하는 것이 메서드의 첫 번째 매개변수인 self입니다.
class Human:
def __init__(self):
self.name = "알 수 없음"
self.age = 99
def man(self, name, age=10):
self.name = name
self.age = age
a = Human()
a.man("박XX")
b = Human()
Human.man(b, "이XX", 20)
print(vars(a))
print(vars(b))- 결과
- {‘name’: ‘박XX’, ‘age’: 10}
{‘name’: ‘이XX’, ‘age’: 20}
self 인자는 자기 자신을 의미합니다. 여기서의 자기 자신은 클래스를 통해 생성된 인스턴스(instance)를 의미합니다.
인스턴스 메서드의 첫 번째 매개변수에는 메서드를 호출한 인스턴스가 자동으로 전달됩니다.
a.man("박XX")처럼 호출하면, 내부적으로는 Human.man(a, "박XX")와 같이 a가 self로 전달됩니다.
예제의 Human.man(b, "이XX", 20)은 이 과정을 직접 작성한 것으로, b.man("이XX", 20)과 같은 결과가 됩니다.
그러므로, a.man(...)이 실행될 때 self.name = name은 a.name = name이 되고, b로 호출하면 b.name = name이 됩니다.
그러므로, 인스턴스 메서드는 반드시 첫 번째 매개변수를 선언해야 하며, 관례적으로 self라는 이름을 사용합니다.
self는 예약어가 아니므로 다른 이름을 사용해도 동작하지만, 모든 Python 코드가 self를 사용하므로 다른 이름은 사용하지 않습니다.
class Human:
def prt():
print("출력")
a = Human()
a.prt()- 결과
- TypeError: Human.prt() takes 0 positional arguments but 1 was given
자주 하는 실수는 메서드를 정의할 때 self를 빠뜨리는 것입니다.
a.prt()는 인자 없이 호출한 것처럼 보이지만, 실제로는 Human.prt(a)로 실행되어 인자 하나(a)가 자동으로 전달됩니다.
prt에는 이 인자를 받을 매개변수가 없으므로, 0개의 위치 인자를 받는데 1개가 전달되었다는 TypeError가 발생합니다.
이런 오류 메시지를 보면, 메서드 정의에 self가 빠지지 않았는지 먼저 확인합니다.
인스턴스 속성과 클래스 속성
self.name처럼 self에 저장한 값을 인스턴스 속성(instance attribute)이라 부릅니다.
인스턴스 속성은 각 인스턴스마다 따로 저장되므로, 한 인스턴스의 값을 바꿔도 다른 인스턴스에는 영향을 주지 않습니다.
반면, 클래스 본문에 직접 작성한 변수는 클래스 속성(class attribute)이 되며, 모든 인스턴스가 하나의 값을 공유합니다.
class Human:
def __init__(self, name):
self.name = name
a = Human("박XX")
b = Human("김XX")
a.name = "최XX"
a.height = 180
print(vars(a))
print(vars(b))
print(b.height)- 결과
- {‘name’: ‘최XX’, ‘height’: 180}
{‘name’: ‘김XX’}
AttributeError: ‘Human’ object has no attribute ‘height’
이번 예제의 __init__은 name 매개변수를 받으므로, Human("박XX")처럼 인스턴스를 만들 때 바로 이름을 전달합니다.
a.name = "최XX"로 a의 이름을 바꿨지만, b의 이름은 김XX 그대로입니다.
각 인스턴스는 자신의 속성을 자기만의 사전(__dict__)에 저장하며, vars(인스턴스)가 반환하는 것이 바로 이 사전입니다.
a.height = 180처럼 클래스에 정의하지 않은 속성도 인스턴스에 추가할 수 있으며, 이 속성은 a에만 생깁니다.
그러므로, b.height를 읽으면 AttributeError가 발생합니다.
- Tip : 이처럼 인스턴스마다 속성이 달라지면 코드를 이해하기 어려우므로, 속성은 가능한 한
__init__에서 모두 정의합니다.
class Human:
species = "사람"
def __init__(self, name):
self.name = name
a = Human("박XX")
b = Human("김XX")
print(a.species, b.species)
Human.species = "호모 사피엔스"
print(a.species, b.species)
a.species = "외계인"
print(a.species, b.species)
print(vars(a))- 결과
- 사람 사람
호모 사피엔스 호모 사피엔스
외계인 호모 사피엔스
{‘name’: ‘박XX’, ‘species’: ‘외계인’}
species는 클래스 본문에 작성했으므로 클래스 속성입니다.
인스턴스.속성으로 값을 읽으면 Python은 인스턴스의 속성을 먼저 찾고, 없으면 클래스의 속성을 찾습니다.
첫 번째 줄은 a와 b 모두 species 인스턴스 속성이 없으므로 클래스 속성인 사람이 출력됩니다.
두 번째 줄은 Human.species로 클래스 속성을 변경했으므로, 두 인스턴스 모두 바뀐 값이 출력됩니다.
세 번째 줄에서는 a.species = "외계인"으로 값을 할당했는데, 이는 클래스 속성을 바꾸는 것이 아니라 a에 같은 이름의 인스턴스 속성을 새로 만드는 것입니다.
그러므로, a는 자신의 인스턴스 속성인 외계인을, b는 여전히 클래스 속성인 호모 사피엔스를 출력합니다. vars(a)에도 species가 추가된 것을 확인할 수 있습니다.
class Human:
friends = []
def __init__(self, name):
self.name = name
def add_friend(self, friend):
self.friends.append(friend)
a = Human("박XX")
b = Human("김XX")
a.add_friend("이XX")
print(b.friends)- 결과
- [‘이XX’]
클래스 속성에 목록이나 사전처럼 변경 가능한 객체를 두면 의도하지 않은 결과가 발생합니다.
self.friends.append(friend)는 할당이 아니라 기존 목록을 수정하는 것이므로, 인스턴스 속성이 새로 만들어지지 않고 클래스 속성인 목록 하나가 수정됩니다.
그 결과, a에 친구를 추가했는데 b.friends에도 이XX가 들어 있게 됩니다.
인스턴스마다 따로 가져야 하는 값은 __init__에서 self.friends = []처럼 인스턴스 속성으로 초기화해야 합니다.
- Tip : 클래스 속성은 모든 인스턴스가 공유하는 상수나 설정값처럼 변하지 않는 값에 사용하는 것이 좋습니다.
상속(inheritance)
상속(inheritance)은 클래스가 다른 클래스로부터 속성과 메서드를 물려받는 기능입니다.
부모 클래스(Parent Class, Super Class)에서 정의된 속성과 메서드를 자식 클래스(Child Class, Sub Class)에서 사용할 수 있습니다.
상속을 하더라도, 부모 클래스의 기능을 그대로 사용할 수 있습니다.
상속을 통해 코드를 구성한다면 중복되는 코드가 사라져 더 간결한 코드와 가독성 높은 프로그램을 구성할 수 있습니다.
- Tip : 부모 클래스를 슈퍼 클래스라고도 부르며, 마찬가지로 자식 클래스도 서브 클래스로도 부를 수 있습니다.
class Dog:
def __init__(self, name):
self.name = name
def eat(self):
print(self.name + " : 밥을 먹습니다.")
def bark(self):
print(self.name + " : 멍멍")
class Cat:
def __init__(self, name):
self.name = name
def eat(self):
print(self.name + " : 밥을 먹습니다.")
def meow(self):
print(self.name + " : 야옹")상속이 왜 필요한지 위 예제로 확인해 보겠습니다.
Dog와 Cat 클래스는 __init__과 eat 메서드가 완전히 같은 코드입니다. 서로 다른 것은 bark와 meow 메서드뿐입니다.
eat 메서드의 출력 문구를 바꾸려면 두 클래스를 모두 수정해야 하며, 동물의 종류가 늘어날수록 같은 코드도 계속 늘어납니다.
한 곳만 고치고 다른 곳을 빠뜨리면, 클래스마다 동작이 달라지는 오류가 생기기 쉽습니다.
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(self.name + " : 밥을 먹습니다.")
class Dog(Animal):
def bark(self):
print(self.name + " : 멍멍")
class Cat(Animal):
def meow(self):
print(self.name + " : 야옹")
dog = Dog("바둑이")
cat = Cat("나비")
dog.eat()
dog.bark()
cat.eat()
cat.meow()
print(isinstance(dog, Animal), isinstance(dog, Cat))
print(issubclass(Dog, Animal))- 결과
- 바둑이 : 밥을 먹습니다.
바둑이 : 멍멍
나비 : 밥을 먹습니다.
나비 : 야옹
True False
True
공통된 부분을 Animal 부모 클래스로 옮기고, Dog와 Cat은 Animal을 상속받아 자신에게만 필요한 메서드만 작성합니다.
Dog("바둑이")를 호출하면 Dog에는 __init__이 없으므로 부모 클래스인 Animal의 __init__이 실행되어 name이 저장됩니다.
dog.eat()도 Dog에서 eat을 찾지 못하므로 Animal의 eat을 찾아 실행합니다.
이제 eat의 동작을 바꾸려면 Animal 한 곳만 수정하면 되고, 새로운 동물을 추가할 때도 Animal을 상속받기만 하면 됩니다.
isinstance(dog, Animal)이 True인 것처럼, 자식 클래스의 인스턴스는 부모 클래스의 인스턴스로도 취급됩니다. 이를 “개는 동물이다(is-a)” 관계라고 표현합니다.
반면, Dog와 Cat은 서로 상속 관계가 없으므로 isinstance(dog, Cat)은 False입니다.
issubclass(Dog, Animal)은 클래스 사이의 상속 관계를 확인합니다.
- Tip : 상속은 “A는 B의 한 종류이다”가 자연스러운 경우에 사용합니다. 단지 코드를 재사용하기 위해 관계없는 클래스를 상속하면 오히려 구조가 복잡해집니다.
class Human:
def __init__(self):
self.name = "알 수 없음"
self.age = 99
def set_name(self, name):
self.name = name
print("Human Class")
def set_age(self, age):
self.age = age
class Man(Human):
def set_name(self, name):
if "XX" in name:
self.name = name.replace("XX", " 모 씨")
print("Man Class")
def prt(self):
print("이름은 " + self.name + "이고, 나이는 " + str(self.age))
a = Man()
a.set_name("김XX")
a.prt()- 결과
- Man Class
이름은 김 모 씨이고, 나이는 99
Man 클래스는 Human 클래스를 상속받아 set_name 메서드를 다시 정의하고, prt 메서드를 추가합니다.
결과의 첫 번째 줄이 Human Class가 아닌 Man Class인 것은 Man에 정의한 set_name이 실행되었기 때문입니다.
두 번째 줄의 나이 99는 Man에 __init__이 없어 Human의 __init__이 실행된 결과입니다.
class Human:
...
class Man(Human):
...class 클래스 이름(상속 받을 클래스 이름):을 사용하여 부모 클래스로부터 속성을 상속받을 수 있습니다.
Man 클래스에는 __init__ 메서드가 없으므로, 부모 클래스인 Human의 __init__ 메서드가 호출되어 age가 99로 초기화됩니다.
부모 클래스에 정의된 내용을 그대로 사용할 수 있으며, 추가적인 기능(prt 메서드)을 포함할 수도 있습니다.
a.set_age(20)처럼 Man에 정의하지 않은 set_age 메서드도 부모 클래스에서 찾아 사용할 수 있습니다.
class Human:
def set_name(self, name):
self.name = name
print("Human Class")
class Man(Human):
def set_name(self, name):
if "XX" in name:
self.name = name.replace("XX", " 모 씨")
print("Man Class")만약 부모 클래스에 정의된 메서드의 이름과 동일한 이름으로 자식 클래스에 생성한다면, 부모 클래스의 메서드는 무시되고 자식 클래스의 메서드가 실행됩니다.
이를 메서드 오버라이딩(Method Overriding)이라 부릅니다.
즉, 자식 클래스에서 메서드를 재정의해 사용할 수 있습니다.
메서드를 재정의하지 않는다면 부모 클래스의 메서드를 호출합니다.
메서드를 찾는 순서는 인스턴스 → 자식 클래스 → 부모 클래스 순서입니다. 먼저 찾은 것을 사용하므로, 자식 클래스에 같은 이름이 있으면 부모 클래스의 메서드까지 가지 않습니다.
예제의 Man.set_name은 이름에 XX가 없으면 아무 일도 하지 않습니다. 오버라이딩한 메서드는 부모의 동작을 완전히 대체하므로, 부모 메서드가 하던 일이 필요하다면 직접 호출해야 합니다. 이 방법은 다음 섹션에서 다룹니다.
부모 클래스 호출(super)
메서드를 오버라이딩하면 부모 클래스의 메서드는 더 이상 자동으로 실행되지 않습니다.
하지만 부모의 동작을 완전히 버리기보다, 부모의 동작은 그대로 사용하고 일부만 추가하거나 바꾸고 싶은 경우가 많습니다.
이때 super()를 사용해 부모 클래스의 메서드를 호출합니다.
class Human:
def __init__(self):
self.name = "알 수 없음"
self.age = 99
def set_name(self, name):
self.name = name
print("Human Class")
class Man(Human):
def set_name(self, name):
if "XX" in name:
self.name = name.replace("XX", " 모 씨")
print("Man Class")
super().set_name(self.name + "(가명)")
def prt(self):
print("이름은 " + self.name + "이고, 나이는 " + str(self.age))
a = Man()
a.set_name("김XX")
a.prt()- 결과
- Man Class
Human Class
이름은 김 모 씨(가명)이고, 나이는 99
super().메서드 이름()을 사용해 부모 클래스의 메서드를 호출할 수 있습니다.
메서드 오버라이딩을 하더라도, super()를 통해 재정의하기 전의 부모 클래스 메서드를 사용할 수 있습니다.
예제는 Man 클래스의 set_name에서 이름을 변경한 다음, 부모 클래스의 set_name을 호출해 (가명)을 덧붙입니다.
실행 순서대로 보면, Man.set_name이 self.name을 김 모 씨로 바꾸고 Man Class를 출력합니다.
이어서 super().set_name("김 모 씨(가명)")이 Human.set_name을 실행하여 self.name을 다시 할당하고 Human Class를 출력합니다.
super()로 호출하더라도 self는 같은 인스턴스(a)이므로, 부모 메서드에서 바꾼 값이 a에 그대로 남습니다.
super()로 접근할 수 있는 것은 부모 클래스에 정의된 메서드와 클래스 속성입니다.
self.name이나 self.age처럼 __init__에서 할당한 인스턴스 속성은 인스턴스에 저장되므로, super().name이 아닌 self.name으로 접근합니다.
super().name으로 접근하면 AttributeError: 'super' object has no attribute 'name' 오류가 발생합니다.
class Human:
def __init__(self):
self.name = "알 수 없음"
class Man(Human):
def __init__(self, name):
super().__init__()
print(self.name)
self.name = name
a = Man("박XX")
print(vars(a))- 결과
- 알 수 없음
{‘name’: ‘박XX’}
자식 클래스에서 __init__ 메서드를 재정의하면, 부모 클래스의 __init__ 메서드는 자동으로 호출되지 않습니다.
그러므로, 부모 클래스의 초기화 과정이 필요하다면 super().__init__()을 호출해야 합니다.
예제는 super().__init__()으로 부모의 초기화를 먼저 실행해 name을 알 수 없음으로 설정한 다음, 자식 클래스에서 전달받은 이름으로 다시 할당합니다.
class Human:
def __init__(self, name):
self.name = name
class Man(Human):
def __init__(self, name, army):
self.army = army
a = Man("박XX", True)
print(vars(a))
print(a.name)- 결과
- {‘army’: True}
AttributeError: ‘Man’ object has no attribute ‘name’
super().__init__()을 빠뜨리면 부모 클래스에서 설정하는 속성이 만들어지지 않습니다.
Man의 __init__은 army만 저장하고 Human의 __init__은 실행되지 않았으므로, vars(a)에 name이 없고 a.name에서 AttributeError가 발생합니다.
자식 클래스에서 __init__을 정의할 때는 super().__init__(name)처럼 부모의 초기화를 먼저 호출하고, 자식 클래스만의 속성을 그 뒤에 추가하는 것이 일반적인 형태입니다.
다중 상속(Multiple Inheritance)
하나의 클래스가 두 개 이상의 부모 클래스를 상속받을 수도 있습니다.
여러 클래스의 기능을 조합할 때 사용하지만, 부모 클래스들에 같은 이름의 메서드가 있으면 어느 쪽을 사용할지 정해야 합니다.
class A:
def hello(self):
print("A")
class B:
def hello(self):
print("B")
class C(A, B):
pass
C().hello()
print([cls.__name__ for cls in C.__mro__])- 결과
- A
[‘C’, ‘A’, ‘B’, ‘object’]
만약 상속 받는 클래스가 여러 개라면, class 클래스 이름(클래스1, 클래스2, ...):의 형태로 다중으로 상속받을 수 있습니다.
다중 상속시 상속 받을 클래스의 이름이 앞 쪽에 있는 클래스가 우선권을 얻습니다.
즉, 클래스1과 클래스2, 클래스3에 동일한 이름의 메서드가 있다면 클래스1의 메서드를 사용합니다.
메서드를 찾는 순서를 메서드 결정 순서(MRO, Method Resolution Order)라 부르며, 클래스.__mro__로 확인할 수 있습니다.
결과의 ['C', 'A', 'B', 'object']는 hello를 C에서 먼저 찾고, 없으면 A, 그다음 B, 마지막으로 object 순서로 찾는다는 의미입니다. A에서 hello를 찾았으므로 A가 출력됩니다.
class A:
def hello(self):
print("A")
class B:
def hello(self):
print("B")
class C(B, A):
pass
C().hello()
print([cls.__name__ for cls in C.__mro__])- 결과
- B
[‘C’, ‘B’, ‘A’, ‘object’]
부모 클래스의 순서만 C(B, A)로 바꾸면 MRO가 C → B → A가 되어 B가 출력됩니다.
이처럼 다중 상속에서는 부모 클래스를 작성한 순서가 동작을 결정하므로, 같은 이름의 메서드가 여러 부모에 있다면 순서에 주의해야 합니다.
super() 역시 단순히 “바로 위 부모”가 아니라 MRO에서 다음 순서의 클래스를 호출합니다.
-
Tip : 모든 클래스는 최상위 클래스인
object를 상속합니다.isinstance(인스턴스, 클래스)와issubclass(클래스, 부모 클래스)로 인스턴스와 상속 관계를 확인할 수 있습니다. -
Tip : 다중 상속은 구조를 복잡하게 만들기 쉬우므로, 꼭 필요한 경우가 아니라면 단일 상속을 사용하는 것이 좋습니다.
정리
| 이름 | 설명 | 특징 |
|---|---|---|
class 이름: |
클래스 정의 | 이름은 CapWords 표기 |
클래스() |
인스턴스 생성 | 호출할 때마다 새로운 객체 생성 |
__init__(self, ...) |
초기화 메서드 | 인스턴스 생성 직후 자동 호출 |
self |
메서드를 호출한 인스턴스 | a.f()는 클래스.f(a)와 같음 |
self.속성 |
인스턴스 속성 | 인스턴스마다 따로 저장 |
| 클래스 본문의 변수 | 클래스 속성 | 모든 인스턴스가 공유 |
vars(인스턴스) |
인스턴스 속성 확인 | 사전(__dict__) 반환 |
class 자식(부모): |
상속 | 부모의 속성과 메서드를 물려받음 |
| 메서드 오버라이딩 | 같은 이름의 메서드 재정의 | 자식 클래스의 메서드가 우선 |
super() |
부모 클래스 메서드 호출 | MRO의 다음 클래스를 호출 |
클래스.__mro__ |
메서드 결정 순서 | 다중 상속 시 왼쪽 부모가 우선 |
isinstance / issubclass |
인스턴스·상속 관계 확인 | bool 반환 |
클래스는 데이터와 기능을 묶는 틀이며, 실제 데이터는 클래스로 만든 각각의 인스턴스가 가집니다.
메서드의 self는 메서드를 호출한 인스턴스이므로, 같은 메서드라도 어떤 인스턴스로 호출했는지에 따라 다른 데이터를 다룹니다.
상속은 여러 클래스의 공통된 부분을 부모 클래스로 모아 중복을 없애고, 자식 클래스에서는 다른 부분만 추가하거나 재정의할 때 사용합니다.
댓글 남기기