본문 바로가기

Programming

객체지향 프로그래밍(2) 상속과 다형성

객체 지향 프로그래밍의 4가지 Concept

1. 추상화

특정 프로그래밍에서 필요한 최소한의 정보를 제외한 나머지 부분을 가리는 것, 함수, class가 추상화의 좋은 예시다. 예를 들어 덧셈을 계산해주는 함수를 sum_function으로 정의하면, 이후 덧셈을 계산할 때 마다 sum_function과 이 함수에 필요한 파라미터만 있으면 된다. 추상화를 잘해야 협업과 디버깅에 용이하다. 이를 위해 클래스, 변수, method들 이름에 기능과 역할이 담기게 이름을 잘 짓는것이 중요하다. 여기에 더해 docstring을 이용해 기능을 문서화 해 놓으면 help를 통해 한꺼번에 볼 수 있고 협업할 때 좋다!

class BankAccount:
    # docstring 문서화
    """ 
    은행 계좌 생성 class
    parameters: owner_name(str), balance(float)
    return: 은행계좌 인스턴스
    """
    def __init__(self,owner_name,balance):
        self.owner_name = owner_name
        self.balance = balance
    
    def deposit(self,amount):
        self.balance += amount

파이썬은 동적 타입 언어로, 정적 타입 언어인 C가 변수의 type을 선언해야되는 것과 달리 변수 type 선언을 하지 않는다. 이는 협업 시 인풋 type을 뭘로 써야될지 모르는 문제를 야기할 수 있다. 이를 해결하기 위해, 파이썬은 type hinting을 통해 변수와 함수 return type을 선언해줄 수 있다. 변수는 :  함수는-> 통해 type을 선언해준다. 이는 강제성은 없지만 잘못된 type이 들어가면 IDE에서 경고를 준다.

class BankAccount:
    def __init__(self,owner_name:str,balance:float)->None:
        self.owner_name = owner_name
        self.balance = balance
    
    def deposit(self,amount:float)->None:
        self.balance += amount

 


2. 캡슐화

캡슐화는 다음 2가지 정의가 있다.

1) 객체(class)의 일부 내용(변수,method)에 대한 외부의 직접적인 접근을 차단하는 것 

다음과 같이 외부로부터의 접근을 차단하고자 하는 변수나 method 앞에 __ 를 붙이면 된다. 여기서는 변수 age와 method can_smoke를 캡슐화하여 외부로부터의 접근을 차단했다. 

class Citzen:
    def __init__(self,name,age,id):
        self.name = name
        self.__age = age
        self.id = id
        
    def __can_smoke(self):
    	return self.__age > 19

2) 객체의 속성과 그것을 사용하는 행동을 하나로 묶는 것

외부로부터 접근이 차단된 변수를 특정 method를 통해서만 접근할 수 있게 하는 것, 다음과 같이 지금 __age는 외부에서 접근이 불가능 하지만, get_age, set_age method를 통해 간접 접근이 가능하다. 이때 보안이 중요한 변수(주민등록번호 등)의 경우 getter, setter method를 만들지 않는게 좋다.

class Citzen:
    def __init__(self,name,age,id):
        self.name = name
        self.__age = age
        self.id = id
        
    # getter method
    def get_age(self): 
        return self.__age
        
    # setter method
    def set_age(self,value):
        return self.__age = value

이처럼 변수와 method 앞에 __를 붙이는 것을 name mangling이라고 한다. 이는 해당 변수나 method 앞에 클래스 명을 붙혀 이름을 변환하는데, 이 때문에 해당 변수나 method로 접근이 안되는 것이다. name mangling은 클래스 상속 시 발생할 수 있는 name conflicts를 예방하는게 목적이다. 이에 따라 instance.class이름__변수 를 통해 해당 변수나 method에 접근이 가능하고 엄밀하게 캡슐화가 안 된 것이다. 즉, Java와 다르게 파이썬은 언어 차원에서 캡슐화를 지향하지 않기에 이를 지원하지 않는다. 따라서 파이썬 개발자들은 변수나 method 앞에 _를 붙혀 이를 외부에서 직접 접근하지 말라는 약속을 만들었다. 

데코레이터 캡슐화

기존 사용하던 변수 age를 캡슐화할 때, property 데코레이터를 사용하면 효율적이다. @property는 해당 method를 getter로 만들고 @age.setter를 통해 setter method도 선언할 수 있다. 내부적으로는 setter, getter method가 동작하지만 외부에서는 age를 변수처럼 사용할 수 있다. 이러한 데코레이터 캡슐화의 장점은 기존 main 코드의 변수명을 그대로 사용 가능하다는 점이다. 

class Citzen:
    def __init__(self,name,age,id):
        self.name = name
        self.age = age
        self.id = id

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self,value):
        if value <0:
            self._age = 0
        else:
            self._age = value

클래스의 변수를 사용할 때는 직접 접근이 아닌, method를 통한 간접 접근으로 코드를 짜는게 유지 보수 측면에서 좋다.

 


3. 상속

두 클래스 사이에 부모-자식 관계를 설정하는 것. 이때 자식 클래스의 개념이 부모에 포함되어야 한다. 자식 클래스는 부모 클래스의 모든 변수와 method를 상속 받는다. 자식 클래스로 만든 인스턴스는 자동으로 부모 클래스의 인스턴스가 된다.

# 부모 클래스
class Employee:
    company_name = '수민월드'
    raise_pct = 1.03

    def __init__(self,name,wage):
        self.name = name
        self.wage = wage
    
    def raise_pay(self):
        self.wage *= self.raise_pct
        
# 자식 클래스
class Cashier(Employee):
	pass
    
# 자식은 부모의 변수, method 사용 가능
c1 = Cashier('hyun',9000)
c1.raise_pay()

# mro: 해당 객체의 부모 클래스 확인(인스턴스로는 안됨)
Cashier.mro()
[__main__.Cashier, __main__.Employee, object]

Overiding

상속 받은 내용을 자식 클래스에 맞게 수정하는 것, 파이썬은 mro(method resolution order)에 따라 자식 -> 부모 방향으로 method를 탐색하므로 자식 method가 먼저 호출되어 오버라이딩이 의미를 갖게 된다. 오버라이딩 할 method에서 특정 변수를 유지하고 싶다면 super( )를 사용해 자식 클래스에서 부모 method를 호출한다. 아래와 같이 __init__ method에 name, wage를 유지한체 num_sold 변수만 추가하고 싶다면 super( )로 부모의 method를 호출한다.  

# 자식 클래스
class Cashier(Employee):
    raise_pct = 1.05
    price = 4000

    def __init__(self,name,wage,num_sold):
        # super: 자식 클래스에서 부모 클래스의 method를 호출 할 때, self 쓰지 않는다.
        super().__init__(name,wage)
        self.num_sold = num_sold

    def take_order(self,money):
        if Cashier.price > money:
            print(f"{Cashier.price-money}원 부족")
        else:
            self.num_sold += 1
            return money - Cashier.price

    def __str__(self):
        return Cashier.company_name + f"계산대 직원 : {self.name}"

상속은 여러 클래스의 공통 부분을 부모 클래스로 만들고 이를 상속하여 자식을 만든다. 이때 상속 받은 변수와 method를 바꾸는 것을 overiding이라고 한다. 즉, 자식 클래스는 부모에게 그대로 상속 받은 것, 상속 받아 바꾼 것, 새로 추가한 것 세가지 종류의 변수와 method를 갖는다.

다중 상속

부모를 둘 이상 갖는 자식 클래스를 만드는 것, 여러 부모 method와 변수 중 어떤 것을 실행하는가에 대한 문제가 있기 때문에 위험하다. 따라서 다른 객체지향 언어인 Java는 다중 상속이 불가하다. 파이썬에서는 다음 두 규칙을 통해 다중 상속의 위험을 방지하지만 일반 클래스의 다중 상속은 되도록 쓰지 않는게 좋다(추상 클래스 다중 상속은 OK!).

1. 부모 클래스끼리 같은 이름의 method를 갖지 않게 하기

2. 같은 이름의 method는 자식 클래스에서 overiding: mro에 의해 자식 클래스 method를 가장 먼저 호출

class Engineer:
    def __init__(self,language):
        self.language = language
    
    def show(self):
        print(f"활용 가능한 언어: {self.language}")

class Drummer:
    def __init__(self,durm_stick):
        self.durm_stick = durm_stick
    
    def play_drum(self):
        print(f"드럼 스틱: {self.durm_stick}")

# 다중 상속 argument 순서에 따라 mro가 정해진다. 
# EngineerDrummer -> Engineer -> Drummer -> Object
class EngineerDrummer(Engineer,Drummer):
    def __init__(self, language,drum_stick):
        Engineer.__init__(self,language)
        Drummer.__init__(self,drum_stick)

 


4. 다형성(Polymorphism)

하나의 변수가 서로 다른 클래스의 여러 인스턴스를 가리킬 때, 이 변수는 다형성이 있다고 하고 이를 클래스 다형성이라고 한다. 아래 Rectangle, Circle과 같이 method가 동일한 경우 각각의 인스턴스를 refer하는 변수 s로 해당 method를 호출하는데 문제가 없지만, 그렇지 않을 때 문제가 발생한다.

class Rectangle:
    def __init__(self,width,height):
        self.width = width
        self.height = height

    def area(self):
        return self.height * self.width
        
class Circle:
    def __init__(self,radius):
        self.radius = radius
    
    def area(self):
        return pi * self.radius**2

r1 = Rectangle(2,3)
c1 = Circle(5)

[s.area() for s in [r1,c1]]

추상 클래스 상속과 다형성

추상 클래스는 ABC(Abstract Base Class)를 상속 받고, 하나 이상의 추상 method를 갖는 클래스다. 추상 클래스는 자체로 인스턴스 생성이 불가하고 자식 클래스에 상속해 자신의 추상 method overiding을 강제한다. 즉, 추상 method를 overiding 해야 해당 자식 클래스가 일반 클래스가 되어 인스턴스 생성이 가능해진다. 이를 통해 다형성의 문제점을 해결할 수 있다. 다음과 같이 Shape라는 추상 클래스에 추상 method area를 선언하면, 이를 상속 받는 자식 클래스들의 인스턴스는 무조건 area method가 overiding 된 상태이므로, isinstance(Circle, Shape)를 통해 다형성을 유지할 수 있다.

from abc import ABC,abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass

class Circle:
    def __init__(self,radius):
        self.radius = radius
    
    def area(self):
        return pi * self.radius**2

모든 자식 클래스에서 공통으로 사용할 부분을 추상 클래스에 추상 method로 선언하고 이를 자식에서 super( )로 호출할 수 있다.

한편, 다음과 같이 @property, @abstractmethod 를 같이 써서 자식 클래스에게 변수 x를 정의할 것을 강제할 수 있다. 즉, 부모 클래스에 추상 method이자 getter method를 만들어 자식이 해당 getter의 대상이 되는 인스턴스 변수 _x를 갖도록 강제할 수 있다.

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass
    
    @property
    @abstractmethod
    def x(self):
        pass

class RightTri(Shape):
    def __init__(self,length):
        self.length = length
    
    def area(self):
        return 2/3 * sqrt(self.length)
    
    @property
    def x(self):
        return self._x

    @x.setter
    def x(self,value):
        self._x = value

또한, 추상 method들로만 이뤄져 있는 추상 클래스의 다중 상속은 아무 문제가 없으며 일반적으로 많이 쓰인다. 하지만 여러 부모 추상 클래스들 간에 이름이 겹치는 일반 method가 있으면 일반 클래스 다중 상속과 똑같은 문제가 생길 수 있다.