클래스

CHOI·2021년 12월 31일
0

Python

목록 보기
27/33
post-thumbnail

클래스는 객체를 표현하기 위한 문법이다. 예를 들어서 게임을 만든다고 하면 기사, 마법사, 궁수, 사제 직업별로 클래스를 만들어서 표현할 수 있다.

물론 집, 차, 나무 등도 클래스로 표현할 수 있따. 특히 프로그래밍에서는 현실 세계의 개념들 뿐만 아니라 컴퓨터 안에서 사용되는 개념들도 클래스로 만들어서 표현한다. 웹 브라우저에서 내용이 길어지면 나타나는 스크롤 바, 프로그램에서 자주 보이는 버튼이나 체크 박스 등이 대표적이다.

지금까지 말한 궁수, 마법사 , 기사, 집, 자동차, 스크롤 바, 버튼 등 처럼 특정한 개념이나 모양으로 존재하는 것을 객체(Object) 라고 부른다. 그리고 프로그래밍으로 객체를 만들때 사용하는 것이 클래스이다.

그럼 게임의 기사 캐릭터를 클래스로 표현하려면 무엇이 필요할까? 일단 게임의 캐릭터는 체력, 마나, 공격력, 주문력 등이 필요하다. 그리고 기사 캐릭터는 칼로 베기, 찌르기 등의 스킬이 있어야 할 것이다.

여기서 체력, 마나, 공격력, 주문력 등의 데이터를 클래스의 속성(attribure)이라고 부르고 베기, 찌르기 와 같은 기능을 메서드(method)라고 부른다.

이러한 프로그래밍 방법을 객체지향(object oriented) 프로그래밍이라고 한다. 객체지향 프로그래밍은 복잡한 문제를 잘게 나누어 객체로 만들고, 객체를 조합하여 문제를 해결한다. 따라서 현실에서 복잡한 문제를 처리하기에 유용하고 어떠한 부분의 기능을 개선하거나 발전시킬때도 해당 클래스만 수정하면 되니까 유지 보수에도 효율적이다.

지금까지 숫자 1, 2 ,3 문자 'a', 'b', 'c' 리스트, 딕셔너리 등을 가지고 프로그램을 만들어보았는데 사실 파이썬에서는 이 모든 것이 객체이다. 이번에는 클래스를 이용하여 객체를 표현하는 방법에 대해서 알아보자.

1. 클래스와 메서드 만들기

클래스는 class 에 클래스 이름을 지정한 뒤에 : (콜론) 을 붙이고 그 다음 줄 부터 def 로 메서드를 작성하면 된다. 여기서 메서드는 클래스 안에 있는 함수를 말한다.

클래스 이름을 짓는 방식은 변수와 동일하다. 보통 파이썬에서 클래스의 이름은 대문자로 시작한다. 그리고 메서드 작성 방법은 함수와 같으며 코드는 반드시 들여쓰기를 해야한다. 또한 메서드의 첫 번째 매개변수는 만드시 self 로 지정해야한다.

class 클래스이름:
    def 메서드(self):
        코드

이제 간단한 사람 클래스를 지정해보자.

class Person:
		def greeting(self):
				print("Hello")

그러면 이제 이 클래스를 사용해보자. 다음과 같이 클래스이름() 을 변수에 할당하자.

james = Person()

Person 으로 변수 james 를 만들었는데 이 jamesPerson인스턴스(instance)이다. 클래스는 특정 개념을 표현할 뿐 사용하려면 인스턴스를 생성해야한다.

메서드 호출하기

이제 메서드를 호출해보자.

>>> james.greeting()
Hello

james.greeting 를 호출하니 'Hello' 가 출력됐다. 이와 같이 인스턴스로 메서드를 호출하는 방식을 인스턴스 메서드라고 한다.

파이썬에서 흔히 볼 수 있는 클래스

지금까지 사용한 int , list , dict 등도 사실 클래스이다. 우리는 이 클래스로 인스턴스를 만들고 메서드를 사용했다.

>>> a = int(10)
>>> a
10
>>> b = list(range(10))
>>> b
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> c = dict(x=10, y=20)
>>> c
{'x': 10, 'y': 20}

int 클래스에 10을 넣어서 a 인스턴스를 만들었고 list 클래스에 range(10) 을 넣어서 b 인스턴스를 만들었고 dict 클래스에 x=10, y=20 를 넣어서 c 인스턴스를 만들었다.

물론 정수는 매우 자주 사용하므로 int 를 생략하고 10을 바로 넣는다. 그리고 리스트와 딕셔너리도 자주 사용되므로 축약된 문법인 []{} 도 사용하지만 클래스인 것은 같다.

>>> b = list(range(10))
>>> b.append(20)
>>> b
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 20]

위와 같이 우리는 리스트를 조작할 때 메서드를 사용했었다. 인스턴스 bappend 메서드를 사용하여 리스트에 값을 추가했다. 이 부분도 지금까지 메서드를 만들고 사용한 것과 같은 방식이다.

즉 파이썬에서 자료형도 클래스이다. 다음과 같이 type 을 사용하면 객체(인스턴스)가 어떤 클래스인지 알 수 있다.

>>> a = 10
>>> type(a)
<class 'int'>
>>> b = [0, 1, 2]
>>> type(b)
<class 'list'>
>>> c = {'x':10, 'y':20}
>>> type(c)
<class 'dict'>
>>> maria = Person()
>>> type(maria)
<class '__main__.Person'>

인스턴스와 객체의 차이점

클래스는 객체를 표현하는 문법이라고 했었는데 클래스와 인스턴스를 만든다고 하니 좀 헷갈릴 것이다. 사실 인스턴스와 객체는 같은 개념이다. 보통 객체만 지정할 때는 객체(object)라고 부르고 클래스와 연관지어 말할 때는 인스턴스(instance)라고 부른다. 그래서 다음과 같이 리스트 변수 a, b 가 있으면 a, b 는 객체이다. 그리고 a, blist 클래스의 인스턴스이다.

a = list(range(10))
b = list(range(20))

% 빈 클래스

class Person:
    pass

메서드안에 메서드 호출하기

class Person:
    def greeting(self):
        print('Hello')
 
    def hello(self):
        self.greeting()    # self.메서드() 형식으로 클래스 안의 메서드를 호출
 
james = Person()
james.hello()    # Hello

메서드 안에서 메서드를 호출할 때는 self.메서드() 형식으로 호출해야한다. self 없이 메서드 이름만 사용하면 클래스 바깥쪽에 있는 함수를 호출한다는 뜻이니까 주의해야한다.

특정 클래스의 인스턴스인지 확인하기

  • isinstance(인스턴스, 클래스)
>>> class Person:
...     pass
...
>>> james = Person()
>>> isinstance(james, Person)
True

현재 인스턴스가 특정 클래스의 인스턴스인지 확인할때는 isinstance 함수를 사용해야한다. 특정 클래스가 맞으면 True 를 아니면 False 를 리턴한다.

isinstance 는 주로 객체의 자료형을 판단할 때 사용한다. 예를 들어 팩토리얼 함수는 1부터 n까지 양의 정수를 차례대로 곱해야 하는데 실수와 음의 정수는 곱할 수 없다. 이런 경우 isinstance 를 사용하여 (숫자)객체가 양의 정수일 경우에만 계산하도록 만들 수 있다.

def factorial(n):
    if not isinstance(n, int) or n < 0:    # n이 정수가 아니거나 음수이면 함수를 끝냄
        return None
    if n == 1:
        return 1
    return n * factorial(n - 1)

2. 속성 사용하기

class 클래스이름:
    def __init__(self):
        self.속성 =

지금까지 클래스에서 메서드를 만들고 호출하였다. 이번에는 클래스의 속성을 만들어보고 사용해보자. 속정(attribute)을 만들 때는 __init__ 메서드 안에서 self.속성 에 값을 할당한다.

class Person:
    def __init__(self):
        self.hello = '안녕하세요.'
 
    def greeting(self):
        print(self.hello)
 
james = Person()
james.greeting()    # 안녕하세요.

실행 결과

안녕하세요.

Person 클래스의 __init__ 메서드에서 self.hello 에 '안녕하세요.' 인사말을 넣었다.

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

__init__ 메서드는 james = Person() 처럼 클래스에 () 괄호를 붙여서 인스턴스를 만들 떄 호출되는 특별한 메서드이다. 따라서 __init__ 는 initialize 라는 이름 그대로 인스턴스(객체)를 초기화 한다.

이와 같이 앞 뒤로 __ (밑줄 두 개)가 붙는 메서드는 파이썬이 자동으로 호출해주는 메서드인데 스페셜 메서드(special method) 또는 매직 메서드(magic method)라고 부른다. 앞으로 파이썬에서 여러가지 기능을 사용할 때 이 스페셜 메서드를 채우는 방식으로 사용하게 될 것이다.

이제 greeting 메서드를 살펴보자.

def greeting(self):
        print(self.hello)

greeting 메서드에서는 printself.hello 를 출력하도록 하였다.

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

james = Person() 에서 인스턴스를 만들고 greeting 메서드를 호출해보면 self.hello 에 저장된 '안녕하세요.' 가 출력된다.

지금까지 __init__ 메서드에서 속성을 만들고 greeting 메서드에서 속성을 사용해봤다. 속성은 __init__ 메서드에서 만든다는 점과 self.속성 방식으로 값을 할당한다는 점이 중요하다. 클래스 안에서 속성을 활용할 때도 self.속성 방식으로 사용하면 된다.

self 의 의미

그런데 도대체 self 는 무엇일까? self 는 인스턴스 자기 자신을 의미한다. 우리가 인스턴스를 생성할 때 self.hello = '안녕하세요.' 처럼 자기 자신에 속성을 추가하였다. 여기서 __init__ 메서드에 매개변수 self 에 들어가는 값은 Person() 이라고 할 수 있다. 그리고 self 가 완성된 뒤에 james 에 할당된다. 이후 메서드를 호출하면 현재 인스턴스가 자동으로 매개변수 self 에 들어온다. 그래서 greeting 메서드에서 print(self.hello) 처럼 속성을 출력할 수 있던 것이였다.

지금까지 만든 클래스는 단순히 인사만 하는 클래스였다. 클래스로 인스턴스를 만들어봐야 다 똑같이 '안녕하세요.' 마 출력할 뿐이다.

인스턴스를 받을 때 값 받기

이번에는 클래스로 인스턴트를 만들때 값을 받는 방법에 대해서 알아보자.

class 클래스이름:
    def __init__(self, 매개변수1, 매개변수2):
        self.속성1 = 매개변수1
        self.속성2 = 매개변수2

위와 같이 __init__ 메서드에서 self 다음으로 값을 받을 매개변수를 지정한다. 그리고 매게변수를 self.속성 에 넣어준다.

class Person:
	def __init__(self, name, age, address):
		self.hello ='안녕하세요'
		self.name = name
		self.age = age
		self.address = address

	def greeting(self):
		print("{} 저는 {}입니다.".format(self.hello, self.name))

james = Person("제임스", 20, "경기도 수원시")
james.greeting()

print("이름 :", james.name)
print("나이 :", james.age)
print("주소 :", james.address)

실행 결과

안녕하세요 저는 제임스입니다.
이름 : 제임스
나이 : 20
주소 : 경기도 수원시

__init__ 메서드의 매개변수로 self 다음에 name, age , address 를 지정했다. 그리고 self.name = name 처럼 매개변수를 그대로 self 에 넣어서 속성으로 만들었다.

def __init__(self, name, age, address):
        self.hello = '안녕하세요.'
        self.name = name
        self.age = age
        self.address = address

이제 Person 괄호 안에 이름, 나이, 주소를 콤마로 구분하여 넣은 다음에 변수에 할당한다.

james = Person("제임스", 20, "경기도 수원시")

그러면 이름은 "제임스", 나이는 20, 주소는 "경기도 수원시"인 james 인스턴스가 생성이 된다.

james 인스턴스의 greeting 메서드를 호출해보면 "안녕하세요. 저는 제임스입니다." 처럼 인삿말과 함께 이름이 출력이 된다.

james.greeting()       # 안녕하세요 저는 제임스입니다.

클래스 안에서 속성에 접근할 때 self.속성 형식이였다. 그러면 클래스 밖에서 속성에 접근할 때는 어떻게 할까? 인스턴스.속성 형식으로 접근하면 된다.

print("이름 :", james.name)       # 제임스
print("나이 :", james.age)        # 20
print("주소 :", james.address)    # 경기도 수원시

이렇게 인스턴스를 통해 접근하는 속성을 인스턴스 속성이라고 한다.

% 클래스의 위치 인수, 키워드 인수

클래스로 인스턴스를 만들 때에서 위치 인수와 키워드 인수를 활용할 수 있다. 규칙은 함수와 동일하다.

위치 인수와 리스트 언패킹을 사용하려면 다음과 같이 *args 를 사용하면 된다. 이때 매개변수에서 값을 가져오려면 args[0] 처럼 사용하면 된다.

class Person():
		def __init__(self, *args):
				self.name = args[0]
				self.age = args[1]
				self.address = args[2]
	
james = Person(*["제임스", 20, "경기도 수원시"])

키워드 인수와 딕셔너리 언패킹을 사용하려면 다음과 같이 **kwargs 를 사용하면 된다. 이때 매개변수에서 값을 가져오려면 kwargs['name'] 을 사용해야한다.

class Person():
		def __init__(self, **kwargs):
				self.name = kwargs['name']
				self.age = kwargs['age']
				self.address = kwargs['address']
		
james1 = Person(name = "제임스", age = 20, address = "경기도 수원시")
james2 = Person(**{'name' : '제임스', 'age' : 20, 'address' : '경기도 수원시'})

% 인스턴스를 생성한 뒤에 속성 추가하기 ,특정 속성만 허용하기 (slots)

지금까지 인스턴스의 속성은 __init__ 메서드에서 추가한 뒤에 사용했다. 하지만 클래스로 인스턴스를 만든 뒤에도 인스턴스.속성 형식으로 속성을 계속 추가할 수 있다. 다음 Person 클래스는 빈 클래스이지만 인스턴스를 만든 뒤 name 속성을 추가한다.

>>> class Person:
...     pass
...
>>> maria = Person()         # 인스턴스 생성
>>> maria.name = '마리아'    # 인스턴스를 만든 뒤 속성 추가
>>> maria.name
'마리아'

이렇게 추가된 속성은 해당 인스턴스에만 존재한다. 따라서 다른 인스턴스를 만들면 해당 속성이 추가되어있지 않다.

인스턴스를 생성한 다음에 속성을 추가하므로 __init__ 메서드가 아닌 다른 메서드에서도 속성을 추가할 수 있다. 단, 이때는 메서드를 호출해야만 속성이 추가된다.

>>> class Person:
...     def greeting(self):
...         self.hello = '안녕하세요'    # greeting 메서드에서 hello 속성 추가
...
>>> maria = Person()
>>> maria.hello    # 아직 hello 속성이 없음
Traceback (most recent call last):
  File "<pyshell#22>", line 1, in <module>
    maria.hello
AttributeError: 'Person' object has no attribute 'hello'
>>> maria.greeting()    # greeting 메서드를 호출해야
>>> maria.hello         # hello 속성이 생성됨
'안녕하세요'

인스턴스는 자유롭게 속성을 추가할 수 있지만 특정 속성만 허용하고 다른 속성은 제한하고 싶을 수 있다. 이럴 경우에는 __slots__ 에 허용할 속성 이름을 리스트로 넣어주면 된다. 속성 이름은 반드시 문자열로 넣어줘야한다.

>>> class Person:
...     __slots__ = ['name', 'age']    # name, age만 허용(다른 속성은 생성 제한)
...
>>> maria = Person()
>>> maria.name = '마리아'                     # 허용된 속성
>>> maria.age = 20                            # 허용된 속성
>>> maria.address = '서울시 서초구 반포동'    # 허용되지 않은 속성은 추가할 때 에러가 발생함
Traceback (most recent call last):
  File "<pyshell#32>", line 1, in <module>
    maria.address = '서울시 서초구 반포동'
AttributeError: 'Person' object has no attribute 'address'

3. 비공개 속성

앞서 만든 Person 클래스에서는 속성으로 hello, name, age, address 가 있었다.

class Person:
    def __init__(self, name, age, address):
        self.hello = '안녕하세요.'
        self.name = name
        self.age = age
        self.address = address

이 속성들은 메서드에서 self.속성 으로 접근할 수 있었고 메서드 밖에서 인스턴스.속성 으로 접근할 수 있었다.

>>> maria = Person('마리아', 20, '서울시 서초구 반포동')
>>> maria.name
'마리아'

이번에는 클래스 밖에서는 접근할 수 없고 클래스 안에서만 사용할 수 있는 비공개 속성(private attribute)을 사용해보자.

비공개 속성은 __속성 방식이다. __ 밑줄 두 개로 시작해야한다. 단, __속성__ 처럼 밑줄이 앞뒤로 두 개가 있으면 비공개 속성이 아니므로 주의해야한다.

class 클래스이름:
    def __init__(self, 매개변수)
        self.__속성 =

그러면 Person 클래스에 wallet 이라는 비공개 속성을 넣어보자.

class Person:
    def __init__(self, name, age, address, wallet):
        self.name = name
        self.age = age
        self.address = address
        self.__wallet = wallet    # 변수 앞에 __를 붙여서 비공개 속성으로 만듦
 
maria = Person('마리아', 20, '서울시 서초구 반포동', 10000)
maria.__wallet -= 10000    # 클래스 바깥에서 비공개 속성에 접근하면 에러가 발생함

실행 결과

Traceback (most recent call last):
  File "C:\project\class_private_attribute_error.py", line 9, in <module>
    maria.__wallet -= 10000    # 클래스 바깥에서 비공개 속성에 접근하면 에러가 발생함
AttributeError: 'Person' object has no attribute '__wallet'

위와 같이 해보면 에러가 발생하는 것을 볼 수 있다. 비공개 속성 wallet 을 만들고 메서드 밖에서 wallet 속성에 접근하려고 하니까 오류가 발생한 것이다.

비공개 속성은 메서드 안에서만 접근할 수 있다. 다음과 같이 지갑에서 돈을 내는 pay 메서드를 만들어보자.

class Person:
    def __init__(self, name, age, address, wallet):
        self.name = name
        self.age = age
        self.address = address
        self.__wallet = wallet    # 변수 앞에 __를 붙여서 비공개 속성으로 만듦
 
    def pay(self, amount):
        self.__wallet -= amount   # 비공개 속성은 클래스 안의 메서드에서만 접근할 수 있음
        print('이제 {0}원 남았네요.'.format(self.__wallet))
 
maria = Person('마리아', 20, '서울시 서초구 반포동', 10000)
maria.pay(3000)

pay 는 돈을 내면 해당 금액을 지갑에서 빼도록 만들었다. 이제 지갑에 있는 돈을 뺄 수 있게 되었다.

물론 돈을 낼 때마다 지갑에 있는 금액을 출력하지 않아도 된다. 보통은 지갑에 있는 돈이 얼마인지 확인하고 모자르면 쓰지 못하는 식으로 만든다.

def pay(self, amount):
        if amount > self.__wallet:    # 사용하려고 하는 금액보다 지갑에 든 돈이 적을 때
            print('돈이 모자라네...')
            return
        self.__wallet -= amount

이처럼 비공개 속성은 클래스 바깥으로 드러내고 싶지 않을 때 사용한다. 즉, 중요한 값이여서 함부로 바꾸면 안될 때 비공개 속성을 주로 사용한다. 비공개 속성을 사용하는 경우는 메서드로 한정한다.

% 공개 속성과 비공개 속성

클래스 바깥에서도 접근이 가능한 속성을 공개 속성(public attribute)라 하고 클래스 안에서만 접근할 수 있는 속성을 비공개 속성(privage attribute) 라고 부른다.

% 비공개 메서드

속성 뿐만 아니라 메서드도 이름 앞에 __ 밑줄 두 개로 시작하면 비공개 메서드를 생성할 수 있다. 비공개 메서드는 클래스 안에서만 호출이 가능한 메서드이다.

class Person:
    def __greeting(self):
        print('Hello')
 
    def hello(self):
        self.__greeting()    # 클래스 안에서는 비공개 메서드를 호출할 수 있음
 
james = Person()
james.__greeting()    # 에러: 클래스 바깥에서는 비공개 메서드를 호출할 수 없음

비공개 메서드도 클래스 바깥에서 드러내고 싶지 않을 때 사용한다. 예를 들어서 게임 캐릭터가 마나를 소비해서 스킬을 쓴다면 마나 소비량은 비공개 메서드로 만들고 스킬을 사용하는 메서드는 공개 메서드로 만든다. 만약 마나 소비량을 계산하는 메서드를 공개 메서드로 만들면 마음대로 마나를 차감할 수 있으므로 잘못된 클래스 설계가 된다.

지금까지 클래스 사용 방법에 대해서 알아보았다. 클래스는 특정 개념을 표현(정의)할 뿐 사용 하려면 인스턴스로 만들어야 한다는 점이 중요하다. 그리고 속성, 메서드를 사용할 때 self 와 인스턴스를 사용해야 한다는 점도 알아두자.

1. 클래스 속성과 인스턴스 속성

클래스 속성 사용하기

클래스 속성은 다음과 같이 클래스에 바로 속성을 만든다.

class 클래스이름:
    속성 =

그러면 이제 간단하게 사람 클래스를 만들고 클래스 속성으로 가방 속성을 만들어 사용해보자. 다음과 같이 Person 클래스에 바로 bag 속성을 넣고 put_bag 메서드를 만든다. 그리고 두 개의 인스턴스를 만든 뒤에 각각 put_bag 메서드를 사용한다

class Person:
		bag = []
	
		def put_bag(self, stuff):
				self.bag.append(stuff)

james = Person()
james.put_bag('책')

maria = Person()
maria.put_bag('열쇠')

print(james.bag)
print(maria.bag)

실행 결과

['책', '열쇠']
['책', '열쇠']

가방에 물건을 넣는 간단한 동작을 만들어보았다. 그런데 결과가 조금 이상하다. jamesmaria 인스턴스를 만들고 각자 put_bag 메서드로 물건을 넣었는데 james.bagmaria.bag 로 가방을 출력해보면 넣었던 물건이 합쳐져서 나온다. 즉, 클래스 속성은 클래스에 속해 있으면 모든 인스턴스에서 공유된다.

class Person:
    bag = []
 
    def put_bag(self, stuff):
        self.bag.append(stuff)

put_bag 메서드에서 클래스 속성 bag 에 접근할 때 self 를 사용했다. 사실 self 는 현재 인스턴스를 뜻하므로 클래스 속성을 지칭하기에 조금 모호하다.

class Person:
    bag = []
 
    def put_bag(self, stuff):
        Person.bag.append(stuff)    # 클래스 이름으로 클래스 속성에 접근

따라서 클래스 속성에 접근할 때는 클래스.속성 형식이 좀 더 명확하다.

Person.bag 라고 하니까 클래스 Person 에 속한 속성이라는 것을 바로 알 수 있다. 마찬가지로 클래스 바깥에서도 클래스 속성에 접근할 때 클래스 이름으로 접근하면 된다.

print(Person.bag)

% 속성, 메서드 이름 찾는 순서 (dict)

파이썬에서 속성, 메서드 이름을 찾을 때 인스턴스, 클래스 순서대로 찾게 된다. 그래서 인스턴스 속성이 없으면 클래스 속성을 찾게 되므로 jaems.bag , maria.bag 도 문제 없이 작동한다. 겉으로 보기에는 인스턴스 속성을 사용하는 것 같지만 실제로는 클래스 속성이다.

인스턴스와 클래스에서 __dict__ 속성을 출력해보면 현재 인스턴스와 클래스의 속성을 알 수 있다.

>>> james.__dict__
{}
>>> Person.__dict__
mappingproxy({'__module__': '__main__', 'bag': ['책', '열쇠'], 'put_bag': <function Person.put_bag at 0x028A32B8>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None})

인스턴스 속성

그러면 가방을 여러 사람이 공유하지 않으려면 어떻게 해야할까? 그냥 bag 를 인스턴스 속성으로 만들면 된다.

class Person:
    def __init__(self):
        self.bag = []
 
    def put_bag(self, stuff):
        self.bag.append(stuff)
 
james = Person()
james.put_bag('책')
 
maria = Person()
maria.put_bag('열쇠')
 
print(james.bag)
print(maria.bag)

실행 결과

['책']
['열쇠']

위와 같이 하면 각자 넣은 물건만 출력이 된다. 즉, 인스턴스 속성은 인스턴스별로 독립되어 있으며 서로 영향을 끼치지 않는다.

  • 클래스 속성: 모든 인스턴스가 공유. 인스턴스 전체가 사용해야 하는 값을 저장할 때 사용
  • 인스턴스 속성: 인스턴스별로 독립되어 있음. 각 인스턴스가 값을 따로 저장해야 할 때 사용

비공개 클래스 속성

클래스 속성도 비공개로 만들 수 있다.

class 클래스이름:
    __속성 =# 비공개 클래스 속성

비공개 속성은 앞서 배웠듯이 앞에 밑줄 두 개 __ 를 붙이면 만들어지고 클래스 바깥에서 접근할 수 없다.

예를 들어서 기사 게임 캐릭터가 아이템을 최대 10개만 소유할 수 있다고 하자

class Knight:
    __item_limit = 10    # 비공개 클래스 속성
 
    def print_item_limit(self):
        print(Knight.__item_limit)    # 클래스 안에서만 접근할 수 있음
 
 
x = Knight()
x.print_item_limit()    # 10
 
print(Knight.__item_limit)    # 클래스 바깥에서는 접근할 수 없음

실행 결과

10
Traceback (most recent call last):
  File "C:\project\class_private_class_attribute_error.py ", line 11, in <module>
    print(Knight.__item_limit)    # 클래스 바깥에서는 접근할 수 없음
AttributeError: type object 'Knight' has no attribute '__item_limit'

실행을 해보면 Knight 클래스의 비공개 속성 __item_limit 는 클래스 안의 print_item_limit 메서드를 통해서는 접근할 수 있지만 클래스 바깥에서 접근하면 에러가 발생하는 것을 볼 수 있다. 아이템 보유 제한이 10개 인데 이 클래스를 사용하는 사람이 마음대로 __item_limit = 1000 로 수정해버리면 골란할 것이다.

이처럼 비공개 클래스 속성은 클래스 바깥으로 드러내고 싶지 않을 때 사용한다.

% 클래스와 메서드의 독스트링

함수와 마찬가지로 클래스와 메서드에서도 독스트링을 사용할 수 있다. 다음과 같이 클래스와 메서드를 만들 때 : 콜론 바로 다음줄에 """ """ 큰 따음표 3 개 또는 ''' ''' 작은 따음표 3 개로 문자열을 입력하면 된다. 그리고 클래스 독스트링은 클래스.__doc__ 형식으로, 메서드 독스트링은 클래스.메서드.__doc__ 또는 인스턴스.메서드.__doc__ 형식으로 사용할 수 있다.

class Person:
    '''사람 클래스입니다.'''

    def greeting(self):
        '''인사 메서드입니다.'''
        print('Hello')

print(Person.__doc__)
print(Person.greeting.__doc__)

x = Person()
print(x.greeting.__doc__)

실행 결과

사람 클래스입니다.
인사 메서드입니다.
인사 메서드입니다.

2. 정적 메서드

지금까지는 클래스의 메서드를 사용할 때 인스턴스를 통해 사용했다. 이번에는 인스턴스를 통하지 않고 바로 클래스에서 호출할 수 있는 정적 메서드에 대해서 알아보자.

먼저 정적 메서드이다. 정적 메서드는 다음과 같이 메서드 위에 @staticmethod 를 붙인다. 이때 정적 메서드는 매개변수로 self 를 지정하지 않는다.

class 클래스이름:
    @staticmethod
    def 메서드(매개변수1, 매개변수2):
        코드

@staticmethod 처럼 앞에 @ 가 붙은 것을 데코레이터라고 하며 메서드(함수)에 추가 기능을 구현할 때 사용한다. 데코레이터에 관한 자세한 내용은 뒤에서 배우겠다.

그러면 간단하게 덧셈과 곱셈을 하는 정적 메서드를 만들어보자.

class Calc:
    @staticmethod
    def add(a, b):
        print(a + b)
 
    @staticmethod
    def mul(a, b):
        print(a * b)
 
Calc.add(10, 20)    # 클래스에서 바로 메서드 호출
Calc.mul(10, 20)    # 클래스에서 바로 메서드 호출

실행 결과

30
200

Calc 클래스에서 @staticmethod 를 붙여서 add 메서드와 mul 메서드를 만들었다. 정적 메서드를 호출할 때는 다음과 같이 클래스.메서드() 로 바로 호출할 수 있다.

Calc.add(10, 20)    # 클래스에서 바로 메서드 호출
Calc.mul(10, 20)    # 클래스에서 바로 메서드 호출

정적 메서드는 self 를 받지 않기 때문에 인스턴스 속성에 접근할 수 없다. 그래서 보통 정적 메서드는 인스턴스 속성, 인스턴스 메서드에 접근할 필요가 없을 때 사용한다.

여기서 만든 add 메서드와 mul 메서드는 숫자 두 개를 받아서 계산할 뿐 인스턴스 속성은 필요하지 않는다.

그러면 무엇을 정적 메서드로 만들어야 할까? 정적 메서드는 메서드의 실행이 외부 상태에 영향을 끼치지 않는 순수 함수(pure function)을 만들 때 사용한다. 순수 함수는 부수 효과(side effect)가 없고 입력값이 같으면 언제나 같은 출력 값을 반환한다. 즉, 정적 메서드는 인스턴스의 상태를 변화시키지 않는 메서드를 만들 때 사용한다.

% 파이썬 자료형의 인스턴스 메서드와 정적 메서드

파이썬의 자료형도 정적 메서드와 인스턴스 메서드가 나눠져 있다. 예를 들어 세트에 요소를 더할 때는 인스턴스 메서드를 사용하고, 합집합을 구할 때는 정적 메서드를 사용하도록 만들어져있다.

>>> a = {1, 2, 3, 4}
>>> a.update({5})    # 인스턴스 메서드
>>> a
{1, 2, 3, 4, 5}
>>> set.union({1, 2, 3, 4}, {5})    # 정적(클래스) 메서드
{1, 2, 3, 4, 5}

이처럼 인스턴스의 내용을 변경할 때는 update 와 같은 인스턴스 메서드를 사용하면 되고 인스턴스의 내용과 상관없이 결과만 구하면 될 경우 set.union() 과 같은 정적 메서드를 사용한다.

3. 클래스 메서드

이번에는 정적 메서드와 비슷하지만 약간의 차이점이 있는 클래스 메서드를 사용해보자.

클래스 메서드는 다음과 같이 클래스 메서드 위에 @classmethod 를 붙인다. 이때 클래스 메서드는 첫 번째 매개변수에 cls 를 지정해야한다. (cls는 class 에서 따온것이다.)

class 클래스이름:
    @classmethod
    def 메서드(cls, 매개변수1, 매개변수2):
        코드

그러면 사람 클래스를 만들고 인스턴스가 몇 개 만들어졌는지 출력하는 메서드를 만들어보자.

class Person:
    count = 0    # 클래스 속성
 
    def __init__(self):
        Person.count += 1    # 인스턴스가 만들어질 때
                             # 클래스 속성 count에 1을 더함
 
    @classmethod
    def print_count(cls):
        print('{0}명 생성되었습니다.'.format(cls.count))    # cls로 클래스 속성에 접근
 
james = Person()
maria = Person()
 
Person.print_count()    # 2명 생성되었습니다.
2명 생성되었습니다.
class Person:
    count = 0    # 클래스 속성
 
    def __init__(self):
        Person.count += 1    # 인스턴스가 만들어질 때
                             # 클래스 속성 count에 1을 더함

먼저 인스턴스가 만들어질 때 마다 숫자가 추가되어야 하므로 __init__ 메서드에서 클래스 속성 count 에 1을 더해준다. self.count += 1 을 해도 똑같이 작동은 하지만 클래스 속성에 접근한다는 것을 더 명확하게 하기 위해서 Person.count += 1 을 해주었다.

		@classmethod
    def print_count(cls):
        print('{0}명 생성되었습니다.'.format(cls.count))    # cls로 클래스 속성에 접근

이제 @classmethod 를 붙여서 클래스 메서드를 만든다. 클래스 메서드는 첫 번째 매개변수가 cls 인데 여기에는 현재 클래스가 들어온다. 따라서 cls.count 처럼 cls 로 클래스의 속성 count 에 접근할 수 있다.

james = Person()
maria = Person()
 
Person.print_count()    # 2명 생성되었습니다.

그 뒤에 인스턴스 두 개를 만들고 print_count 클래스 메서드를 호출하면 "2명 생성되었습니다." 가 출력된다.

클래스 메서드는 정적 메서드처럼 인스턴스 없이 호출할 수 있다는 점은 같다. 하지만 클래스 메서드는 메서드 안에서 클래스 속성, 클래스 메서드에 접근할 때 사용한다.

특히 cls 를 사용하면 메서드 안에서 현재 클래스의 인스턴스도 만들 수 있다. 즉, cls 는 클래스이므로 cls()Person() 과 같다.

		@classmethod
    def create(cls):
        p = cls()    # cls()로 인스턴스 생성
        return p

지금까지는 클래스의 기본적인 내용에 대해서 알아봤는데 이번에는 클래스 상속(inheritance)에 대해서 알아보자.

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

보통 기반 클래스를 부모 클래스(parent class), 슈퍼 클래스(super class) 라고 부르고 파생 클래스를 자식 클래스(child class), 서브 클래스(subclass)

클래스 상속은 생물 분류를 떠올리면 쉽다. 예를 들어서 조류, 어류는 공통 조상인 척추 동물로 부터 특성을 공유하면서 각자의 고유 특성을 가진다. 둘다 척추를 가졌다는 특성은 변화가 없지만 날개를 가지고 있으면 조류, 물속에 살면 어류인 식이다. 즉, 같은 계통으로 특성은 공유하면서 전혀 상관없이 조류가 꽃식물의 특성은 가지고 있지 않는다.

마찬가지로 클래스 상속도 기반 클래스의 능력을 그대로 활용하면서 새로운 클래스를 만들 때 사용한다. 동물로 예시를 들면 척추 동물에서 파충류, 조류, 포유류 등을 만드는 식이다.

그런데 새롭게 클래스를 만들면 되지 굳이 클래스 상속을 만든 이유가 무엇일까? 클래스를 계속해서 새롭게 만들다가 보면 중복되는 부분을 반복해서 만들어야 하는 경우가 생긴다. 이럴 때는 상속을 만들면 중복되는 기능을 만들지 않아도 된다. 따라서 상속은 기존의 기능을 그대로 사용할 수 있어서 효율적이다.

1. 사람 클래스, 학생 클래스

클래스 상속은 다음과 같이 클래스를 만들 때 괄호를 붙이고 기반 클래스의 이름을 넣으면 된다.

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

그러면 간단하게 사람 클래스를 만들고 파생 클래스로 학생 클래스를 만들어보자.

class Person:
    def greeting(self):
        print('안녕하세요.')
 
class Student(Person):
    def study(self):
        print('공부하기')
 
james = Student()
james.greeting()    # 안녕하세요.: 기반 클래스 Person의 메서드 호출

실행 결과

안녕하세요.
공부하기

class Student(Person): 이런 식으로 클래스를 만들 때 괄호안에 기반 클래스인 Person 을 넣으면 Person 클래스의 기능을 물려 받은 Student 클래스가 만들어진다.

따라서 Student 클래스는 greeting 메서드를 정의하지 않아도 Person 으로 부터 물려받았기 때문에 사용할 수 있는 것이다.

이처럼 클래스 상속은 연관되면서 동등한 기능일 때 사용한다. 즉, 학생은 사람이므로 연관된 개념이고, 학생은 사람에서 역할만 확장되었을 뿐 동등한 개념이다.

% 상속 관계 확인하기 ( issubclass )

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

2. 상속관계

앞서 만든 Student 클래스는 Person 클래스로 부터 상속 받아서 만들었다.

class Person:
    def greeting(self):
        print('안녕하세요.')
 
class Student(Person):
    def study(self):
        print('공부하기')

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

포함 관계

하지만 학생 클래스가 아니라 사람 목록을 관리하는 클래스를 만든다면 어떻게 할까? 다음과 같이 리스트 속성에 Person 인스턴스를 넣어서 관리하면 된다.

class Person:
    def greeting(self):
        print('안녕하세요.')
 
class PersonList:
    def __init__(self):
        self.person_list = []    # 리스트 속성에 Person 인스턴스를 넣어서 관리
 
    def append_person(self, person):    # 리스트 속성에 Person 인스턴스를 추가하는 함수
        self.person_list.append(person)

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

정리하자면 같은 종류에 동등한 관계일 때는 상속을 사용하고, 그 이외에는 속성에 인스턴스를 넣는 포함 방식을 사용하면 된다.

2. 기반 클래스의 속성

이번에는 기반 클래스에 들어있는 인스턴스 속성을 이용해보자. 다음과 같이 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__
파이썬 코딩 도장
Traceback (most recent call last):
  File "C:\project\class_inheritance_attribute_error.py", line 14, in <module>
    print(james.hello)
AttributeError: 'Student' object has no attribute 'hello'

james.school 은 잘 실행됐지만 james.hello 는 안나온다 그 이유는 Person 클래스의 __init__ 메서드가 호출되지 않았기 때문이다.

즉, Person__init__ 메서드가 호출되지 않으면 self.hello = '안녕하세요.' 도 실행되지 않아서 속성이 만들어지지 않는다.

기반 클래스 초기화 ( 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__()                # super()로 기반 클래스의 __init__ 메서드 호출
        self.school = '파이썬 코딩 도장'
 
james = Student()
print(james.school)
print(james.hello)

실행 결과

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

super().__init__() 를 함으로써 기반 클래스를 초기화되어 속성이 만들어진다.

더 명확하게 super 사용하기

현재 클래스가 어떤 클래스인지 명확하게 표시하는 방법도 있다.

  • super(파생클래스, self).메서드
class Student(Person):
    def __init__(self):
        print('Student __init__')
        super(Student, self).__init__()     # super(파생클래스, self)로 기반 클래스의 메서드 호출
        self.school = '파이썬 코딩 도장'

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

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

class Person:
    def __init__(self):
        print('Person __init__')
        self.hello = '안녕하세요.'
 
class Student(Person):
    pass
 
james = Student()
print(james.hello)

실행 결과

Person __init__
안녕하세요.

4. 메서드 오버라이팅

이번에는 파생 클래스에서 기반 클래스의 메서드를 새로 정의하는 메서드 오버라이팅에 대해서 알아보자 우선 다음 코드를 보자.

class Person:
    def greeting(self):
        print('안녕하세요.')
 
class Student(Person):
    def greeting(self):
        print('안녕하세요. 저는 파이썬 코딩 도장 학생입니다.')
 
james = Student()
james.greeting()

실행 결과

안녕하세요. 저는 파이썬 코딩 도장 학생입니다.

james.gretting() 처럼 Student 메서드를 호출하니까 "안녕하세요. 저는 파이썬 코딩 도장 학생입니다." 가 나왔다.

오버라이딩(overriding)은 무시하다, 우선하다 라는 의미가 있는데 말 그대로 기반 클래스의 메서드를 무시하고 새로운 메서드를 만든다는 뜻이다. 위에서도 기반 클래스의 메서드가 아닌 파생 클래스의 메서드가 실행된 것을 볼 수 있다.

그렇다면 왜 오버라이딩을 사용할까? 보통 프로그램에서 어떠한 기능이 같은 이름으로 계속 사용되어야 할 때 오버라이딩을 사용한다. 만약 Student 클래스의 인사하는 메서드를 greeting2 로 만들어야 한다면 모든 소스코드에서 greetinggreeting2 로 바꿔야하는 일이 발생할 것이다.

다시 기반 클래스와 파생 클래스의 각각의 메서드를 보면 중복되는 문구가 보인다, 둘다 "안녕하세요." 라는 문구가 중복이 된다.

def greeting(self):
        print('안녕하세요.')
def greeting(self):
        print('안녕하세요. 저는 파이썬 코딩 도장 학생입니다.')

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

class Person:
    def greeting(self):
        print('안녕하세요.')
 
class Student(Person):
    def greeting(self):
        super().greeting()    # 기반 클래스의 메서드 호출하여 중복을 줄임
        print('저는 파이썬 코딩 도장 학생입니다.')
 
james = Student()
james.greeting()

실행 결과

안녕하세요.
저는 파이썬 코딩 도장 학생입니다.

즉, 중복되는 기능은 파생 클래스에 또 만들지 말고 기반 클래스에서 재활용하면 된다.

이처럼 오버라이딩은 원래 기능을 유지하면서 새로운 기능을 덧붙일 때 사용한다.

5. 다중 상속

다중 상속은 여러 기반 클래스로 부터 상속을 받아서 파생 클래스를 만드는 방법이다. 다음과 같이 클래스를 만들 때 괄호 안에 클래스 이름을 콤마로 구분하여 넣는다.

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()         # 안녕하세요.: 기반 클래스 Person의 메서드 호출
james.manage_credit()    # 학점 관리: 기반 클래스 University의 메서드 호출
james.study()            # 공부하기: 파생 클래스 Undergraduate에 추가한 study 메서드

실행 결과

안녕하세요.
학점 관리
공부하기

위와 같이 다중 상속 클래스는 클래스를 만들 때 괄호 안에 기반 클래스를 콤마로 구분하여 넣으면 된다.

다이아몬드 상속

그러면 이번에는 좀 더 복잡한 클래스 상속을 해보자.

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 클래스가 있다. 그리고 이 둘 클래스를 상속 받는 D 클래스가 있다.

클래스간의 관계가 다이아몬드 처럼 생겨서 객체지향 프로그래밍에서 이러한 상속 관계를 다이아몬드 상속이라고 부른다.

그리고 위 코드를 자세히 보면 알겠지만 A, B, C 클래스 모두 greeting 이라는 메서드를 가지고 있다. 그렇다면 D 클래스에서는 어떤 클래스의 메서드를 호출할까? 조금 애매하다.

프로그래밍에서 이렇게 명확하지 않고 애매한 상태를 좋아하지 않는다. 그래서 다이아몬드 상속은 문제가 많다고 하여 '죽음의 다이아몬드' 라고 불린다.

메서드 탐색 순서 ( mro )

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

  • 메서드. mro()
>>> D.mro()
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

이와 같이 하면 메서드 탐색 순서가 나온다. MRO 에 따르면 D 의 메서드 탐색 순서는 먼저 본인 D ,그리고 B ,그리고 C 이러한 순서로 진행된다. 따라서 D 로 인스턴스를 만들고 greeting 메서드를 호출하면 Bgreeting 이 호출된다( D 에는 greeting 메서드가 없기 때문)

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

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

% object 클래스

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

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

파이썬에서 모든 클래스는 object 의 상속을 받기 때문에 파이썬3에서는 기본적으로 생략한다 따라서 원래

class X:
		pass

는 사실 아래와 같다.

class X(object):
		pass

파이썬 2에서는 class X: 가 old-style 클래스를 만들고, class X(object): 가 new-style 클래스를 만들었다. 그래서 파이썬 2에서는 이 둘을 구분해서 사용해야 했지만, 파이썬 3에서는 old-style 클래스가 삭제되었고 class X:class X(object): 모두 new-style 클래스를 만든다. 따라서 파이썬 3에서는 괄호 안에 object를 넣어도 되고 넣지 않아도 된다.

6. 추상 클래스

파이썬에서는 추상 클래스를 제공하는데 추상 클래스는 메서드의 목록만 가지고 있는 클래스이며 상속 받는 클래스에서 메서드 구현을 강제하기 위해 사용한다.

먼저 추상 클래스를 사용하기 위해선 importabc 모듈을 가지고 와야한다(abc 는 abstract base class 의 약자이다). 그리고 클래스의 괄호 안에 metaclass=ABCMeta 를 지정하고 메서드를 만들 때 위에 @abstractmethod 를 붙여 추상 메서드로 지정한다.

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

여기서 import abc 를 안하고 from abc import * 를 하여 모든 메서드와 클래스를 모듈로부터 가져왔다. 만약 import abc 를 했다면 abc.ABCMeta, @abc.abstractmethod 로 사용해야 한다. 이에 대한 자세한 내용은 나중에

그러면 학생 추상 클래스 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()

실행 결과

Traceback (most recent call last):
  File "C:\project\class_abc_error.py", line 16, in <module>
    james = Student()
TypeError: Can't instantiate abstract class Student with abstract methods go_to_school

실행해보면 오류가 발생한다. 그 이유는 추상 클래스 StudentBase 메서드를 상속받는 Student 클래스가 study 메서드만 구현하고 go_to_school 메서드를 구현하지 않았기 때문이다.

따라서 추상 클래스를 상속 받았다면 @abstractmethod 가 붙은 모든 추상 메서드를 구현해야한다.

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()

실행 결과

공부하기
학교가기

StudentBase 는 학생이 반드시 해야하는 일을 추상 클래스로 만들었다. 그리고 Student 클래스에서는 추상 클래스 StudentBase 의 모든 추상 메서드를 구현하여 학생 클래스를 만들었다. 이처럼 추상 클래스는 파생 클래스가 반드시 구현해야 하는 메서드를 정해줄 수 있다.

참고로 추상 클래스의 모든 추상 메서드들이 구현되었는지 확인하는 시점은 파생 클래스가 인스턴스를 만들 때이다. 따라서 james = Student() 를 할 때 확인이 가능하다.

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

또 한가지 중요한 점이 있는데 추상 클래스로 인스턴스를 만들 수 없다는 점이다. 다음과 같이 추상 클래스로 인스턴스를 만들면 오류가 발생한다.

>>> james = StudentBase()
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    james = StudentBase()
TypeError: Can't instantiate abstract class StudentBase with abstract methods go_to_school, study

그래서 지금까지 추상 클래스에서 추상 메서드를 만들 때 pass 만 넣어서 빈 메서드로 만든 것이다. 왜냐하면 추상 클래스로 인스턴스를 만들 수 없으므로 추상 메서드도 호출할 일이 없기 때문이다.

@abstractmethod
    def study(self):
        pass    # 추상 메서드는 호출할 일이 없으므로 빈 메서드로 만듦
 
    @abstractmethod
    def go_to_school(self):
        pass    # 추상 메서드는 호출할 일이 없으므로 빈 메서드로 만듦

정리하자면 추상 클래스는 인스턴스로 만들 때 사용하지 않으며 오직 상속에만 사용한다. 그리고 파생 클래스에서 반드시 구현해야 하는 메서드를 정해줄 때 사용한다.

지금까지 상속에 대해 알아보았는데 내용이 다소 어려웠다. 여기서는 클래스를 상속받는 방법과 메서드 오버라이딩 방법 정도만 기억하면 된다. 그리고 상속은 같은 종류이면서 동등한 기능일 때 사용한다는 점이 중요하다. 다중 상속과 추상 클래스는 나중에 필요할 때 다시 돌아와서 찾아보자.

profile
벨로그보단 티스토리를 사용합니다! https://flight-developer-stroy.tistory.com/

0개의 댓글