저번 시간에는 민감한 정보를 외부로부터 숨길 수 있는 캡슐화에 대해 배웠습니다.

이번 시간에는 객체 지향 프로그래밍의 네 개의 기둥 중 세 번째, 상속에 대해 함께 알아보겠습니다.

👪 중복되는 코드

카페를 위한 직원 관리 프로그램을 개발하고 있다고 가정해 봅시다. 카페에는 여러 종류의 직원이 있는데요. 음료를 계산하는 카운터 직원, 음료를 만드는 바리스타, 배달을 위한 배달 직원 등이 있습니다. 이들을 각각의 클래스로 정의한다고 해봅시다.

class Cashier:
    """계산대 직원 클래스"""
    company_name = "스타커피"
    raise_percentage = 1.02
    coffee_price = 3000
    
    def __init__(self, name, wage, num_sold=0):
        self.name = name
        self.wage = wage
        self.num_sold = num_sold
        
    def raise_wage(self):
        """시급을 인상한다"""
        self.wage *= self.raise_percentage
        
    def take_order(self, money_received):
        """주문과 돈을 받고 거스름돈을 리턴한다"""
        if Cashier.coffee_price > money_received:
            print("돈이 충분하지 않습니다.")
            return money_received
        else:
            self.num_sold += 1
            change = money_received - Cashier.coffee_price
            return change
        
    def __str__(self):
        return f"{Cashier.company_name} 계산대 직원: {self.name}"

Cashier는 계산대 직원을 나타내는 클래스입니다. 이 클래스는 가게 이름, 시급 인상률, 커피 가격을 클래스 변수로 가지고 있고 이름, 시급, 하루 판매량을 인스턴스 변수로 가지고 있습니다.

taki = Cashier("타키탸키", 9000, 0)

타키탸키라는 직원의 인스턴스를 생성했습니다.

다음으로 타키탸키의 시급을 올려보겠습니다.

taki.raise_pay()
print(taki.wage)
9180

현재 시급에서 시급 인상률을 곱한 값이 잘 출력됩니다.

이번에는 타키탸키가 주문을 받도록 해보겠습니다.

print(taki.take_order(5000))
2000

커피 값이 3000원이기 때문에 손님으로부터 5000원을 받아서 2000원을 돌려주었습니다.

print(taki.take_order(2000))
돈이 충분하지 않습니다.
2000

커피값보다 낮은 돈을 주면 돈이 충분하지 않다는 문구와 받은 돈을 출력합니다.

다음으로 커피 가격을 출력해보겠습니다.

print(taki.coffee_price)
print(Cashier.coffee_price)
3000
3000

잘 출력됩니다.

하루 판매량도 출력해볼까요? 지금까지는 커피 한 잔을 팔았었죠?

print(taki.num_sold)
1

마지막으로 taki 인스턴스를 출력해보겠습니다.

print(taki)
스타커피 계산대 직원: 타키탸키

이번에는 배달원 클래스 DeliveryMan을 작성해보겠습니다.

class DeliveryMan:
    """배달원 클래스"""
    company_name = "스타커피"
    raise_percentage = 1.02
    
    def __init__(self, name, wage, on_stanby):
        self.name = name
        self.wage = wage
        self.on_stanby = on_stanby
        
    def raise_wage(self):
        """시급을 인상한다"""
        self.wage *= self.raise_percentage
        
    def deliver(self, address):
        """배달원이 대기 중이면 주어진 주소로 배달을 보내고 아니면 설명 문구를 출력한다"""
        if self.on_stanby:
            print(f"{address}로 배달 갑니다!")
            self.on_stanby = False
        else:
            print("이미 배달 갔습니다!")
            
    def back(self):
        """배달원을 복귀 처리한다"""
        self.on_stanby = True
        
    def __str__(self):
        return f"{DeliveryMan.company_name} 배달원: {self.name}"

DeliveryMan 클래스는 가게 이름, 시급 인상률을 클래스 변수로 가지고 있고 이름, 시급, 대기 상태를 인스턴스 변수로 가지고 있습니다.

대기 상태는 배달을 나갈 수 있는지, 아니면 이미 나가 있어서 나갈 수 없는 상태인지를 알려줍니다.

fire = DeliveryMan("파이리", 10000, True)

파이리 직원의 인스턴스를 생성했습니다.

파이리 직원의 시급도 인상해볼게요.

fire.raise_wage()
print(fire.wage)
10200

이번에는 배달을 보내겠습니다.

fire.deliver("서울시 포켓몬 트레이닝 센터")
서울시 포켓몬 트레이닝 센터로 배달 갑니다!

현재 파이리는 배달을 나간 상태입니다. 이 상태에서 배달을 한 번 더 보내면 어떻게 될까요?

fire.deliver("서울시 포켓몬 트레이닝 센터 2호점")
이미 배달 갔습니다!

배달하러 갔다는 문구를 잘 출력해줍니다.

배달 나간 파이리를 복귀시키고 다시 보내 봅시다.

fire.back()
fire.deliver("서울시 포켓몬 트레이닝 센터 2호점")
서울시 포켓몬 트레이닝 센터 2호점로 배달 갑니다!

마지막으로 fire 인스턴스를 출력해보겠습니다.

print(fire)
스타커피 배달원: 파이리

이렇게 Cashier 클래스와 DeliveryMan 클래스를 봤습니다. 카페에는 여러 종류의 직원이 있으니 앞으로 더 많은 클래스가 생성되어야 하는데요.

그런데 두 코드를 비교해보니 서로 중복되는 부분이 꽤 많습니다. 클래스 변수 company_name과 raise_percentage, 인스턴스 변수 name과 wage, 인스턴스 메소드 던더 이닛, 던더 str, raise_wage가 서로 겹치네요.

이렇게 똑같은 코드를 여러 번 쓰면 참 비효율적인데요. 이때, 상속이라는 개념을 활용하면 반복되는 내용을 한 번만 사용해도 됩니다.

👪 상속

상속이란, 두 클래스 사이에 부모-자식 관계를 설정하는 것을 말합니다.

떡볶이 클래스와 음식 클래스가 있습니다. '떡볶이는 음식이다'라고 말할 수 있지만 '음식은 떡볶이다'라는 말은 좀 어색합니다. 떡볶이가 음식에 포함되기 때문인데요.

이렇게 두 클래스 사이에 'A는 B다'라는 관계가 성립할 때, 상속 관계를 설정할 수 있습니다. 클래스 A의 개념이 클래스 B에 포함되어야 합니다. 이때, A는 자식 클래스, B는 부모 클래스라고 합니다.

자식 클래스는 부모 클래스의 모든 변수와 메소드를 물려 받습니다. 부모의 재산을 상속 받듯이요. 예컨대, 음식 클래스에 '맛, 가격, 따뜻함'이라는 변수가 있고 '조리된다, 먹힌다'라는 메소드가 있다고 하면 떡볶이 클래스는 이 모든 것들을 그대로 물려 받아 사용할 수 있습니다.

👪 부모 클래스 정의하기

앞서 Cashier 클래스와 DeliveryMan 클래스의 공통 부분을 살펴봤는데요. 상속을 하려면 일단 이 공통 부분으로만 이루어진 부모 클래스를 하나 만들어야 합니다. Employee라는 클래스를 만들어보겠습니다.

class Employee:
    """직원 클래스"""
    company_name = "스타커피"
    raise_percentage = 1.02
    
    def __init__(self, name, wage):
        """인스턴스 변수 설정"""
        self.name = name
        self.wage = wage
        
    def raise_wage(self):
        """시급을 인상하는 메소드"""
        self.wage += self.raise_percentage
        
    def __str__(self):
        """직원 정보를 문자열로 리턴하는 메소드"""
        return f"{Employee.company_name} 직원: {self.name}"

이제 Cahsier와 DeliveryMan 클래스의 공통 부분을 코드에서 지워줍니다.

'계산대 직원은 직원이다', '배달원은 직원이다'라는 말은 'A는 B다'라는 말이므로 상속 관계가 성립됩니다. 따라서, Employee 클래스를 부모 클래스로, Cashier 클래스와 DeliveryMan 클래스를 자식 클래스로 하는 상속 관계를 설정해야 합니다.

👪 상속 - 부모로부터 물려 받기

이제 코드로 상속 관계를 구현해 보겠습니다.

class Cashier(Employee):
    pass

이렇게 자식 클래스 이름 뒤에 괄호를 적고 그 안에 부모 클래스의 이름을 적습니다. 앞서 자식 클래스는 부모 클래스의 모든 변수와 메소드를 물려 받는다고 했습니다. 이 상태로 코드를 실행해보겠습니다.

taki = Cashier("타키탸키", 9000)
taki.raise_wage()
print(taki.wage)
print(taki)
9180
스타커피 직원: 타키탸키

Cashier 클래스 안에는 아무 내용이 없는데요. 그럼에도 코드가 잘 실행됩니다. 이는 Cashier 클래스가 Employee 클래스로부터 물려 받은 변수와 메소드를 사용했기 때문입니다.

클래스의 정보를 자세히 출력해주는 help 함수를 통해 Cashier 클래스를 살펴보겠습니다.

help(Cashier)

출력 결과를 보면 Employee 클래스에서 정의된 변수와 메소드들이 보입니다. inherited from Employee라는 문구는 Employee 클래스로부터 물려 받았다는 것을 알려줍니다.

Method resolution order는 Cashier 클래스의 상속 관계를 보여줍니다. Employee 클래스는 Cashier 클래스의 부모 클래스임을 나타내고 있습니다.

그런데 builtins.object는 무엇일까요? Python의 모든 클래스Builtins.object 클래스를 물려 받습니다. Employee 또한 이 클래스의 자식 클래스입니다.

마찬가지로 DeliveryMan 클래스도 Employee 클래스와 상속 관계를 맺을 수 있습니다.

class DeliveryMan(Employee):

👪 상속과 관련된 메소드와 함수들

상속과 관련된 메소드와 Python 함수를 알려 드리겠습니다.

❗ mro 메소드

앞서 help의 결과에서 Method resolution order:라는 부분을 봤습니다. 이 부분에 있는 결과는 해당 인스턴스의 클래스가 어떤 부모 클래스를 가지는지 보여줍니다. 이 결과는 다른 방법으로도 확인이 가능한데요. mro 메소드를 호출하면 됩니다.

print(Cashier.mro())
[<class '__main__.Cashier'>, <class '__main__.Employee'>, <class 'object'>]

맨 뒤 object 클래스는 Cashier 클래스의 입장에서 부모 클래스의 부모 클래스입니다.

우리가 자주 쓰는 list 클래스의 mro 실행 결과를 볼까요?

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

결과를 보니 리스트가 상속 받는 클래스는 object 하나밖에 없네요.

Python에서 들여쓰기를 잘못했을 때 나오는 에러를 나타내는 IndentationError의 클래스를 보면 부모, 부모의 부모, 부모의 부모의 부모, 부모의 부모의 부모의 부모가 있습니다. 최상위 클래스인 object부터 Python 문법 위반 시 발생하는 SyntaxError 클래스까지 집안 내력이 화려하네요.

❗ isinstance 함수

isinstance 함수는 어떤 인스턴스가 주어진 클래스의 인스턴스인지를 알려줍니다.

첫번째 파라미터에는 검사할 인스턴스의 이름을, 두번째 파라미터에는 기준 클래스의 이름을 넣고 실행하면 됩니다. 결과값은 불린 값으로 리턴됩니다.

fire = DeliveryMan("파이리", 10000)

print(isinstance(fire, Cashier))
print(isinstance(fire, DeliveryMan))
print(isinstance(fire, Employee))
False
True
True

fire 인스턴스는 DeliveryMan 클래스의 인스턴스이므로 True를 리턴하고 Cashier에는 False를 리턴합니다.

중요한 것은 부모 클래스인 Employee에도 True를 리턴한다는 것입니다. DeliveryMan이 Employee의 자식 클래스이기 때문이죠. 이는 곧, 자식 클래스의 인스턴스가 부모 클래스의 인스턴스이기도 하다는 점을 알려줍니다. 이는 다음 시간에 배울 다형성이라는 개념을 설명할 때, 핵심이 되는 원리이므로 반드시 기억해두세요!

❗ issubclass 함수

issubclass 함수는 한 클래스가 다른 클래스의 자식 클래스인지를 알려주는 함수입니다. 첫번째 파라미터에는 검사할 클래스의 이름을, 두번째 파라미터에는 기준이 되는 부모 클래스의 이름을 넣고 실행하면 됩니다.

print(issubclass(Cashier, Employee))
print(issubclass(Cashier, object))
print(issubclass(Cashier, list))
True
True
False

이번 시간에는 객체 지향 프로그래밍의 네 번째 기둥 중 세번째 상속에 대해 알아봤습니다. 프로그래밍에도 부모, 자식 관계가 있다는 게 재미있지 않나요?

다음 시간에는 오버라이딩, mro와 같은 상속과 관련된 더 다양한 개념들을 배워보겠습니다.

* 이 자료는 CODEIT의 '객체 지향 프로그래밍' 강의를 기반으로 작성되었습니다.
profile
There's Only One Thing To Do: Learn All We Can

0개의 댓글