파이썬을 배워보자 15일차 - 클래스3 (추상 클래스와 덕 타이핑)

0

Python

목록 보기
15/18

점프 투 파이썬 : https://wikidocs.net/book/1
파이썬 기본을 갈고 닦자 : https://wikidocs.net/16031
코딩 도장 : https://dojang.io/mod/page/view.php?id=2378

클래스 3

1. 추상 클래스

추상 클래스(absctract class)는 메서드의 목록만 가진 클래스이며, 상속받는 클래스에서 메서드 구현을 강제하기 위해 사용한다.

추상 클래스를 사용하려면, importabc모듈을 가져와야 한다. 그리고 클래스의 괄호안에 `metaclass=ABCMeata를 지정해야 한다. 추상 메서드를 만들 때는 메서드 위에 @abstractmethod를 붙여 추상 메서드로 지정한다.

from abc import *

class AClass(metaclass=ABCMeta):
    @abstractmethod
    def absctractMethod(self):
        pass

다음과 같이 써줄 수 있다.

추상 클래스는 어떨 때 쓸까?? 추상 클래스는 현재 클래스에서는 행동(메서드)를 지정하기 애매하거나 너무 다양할 때 사용한다.

가령, Person이라면 eat, sleep 등이 있을 것이다. 그런데 Person이 직접 밥을 어떻게 먹고, 어떻게 잠을 자고하는 것을 정의할 수는 없다. 이건 구체적인 각 인간들마다 다르기 때문이다. 그렇기 때문에 Personeat, sleep 등을 추상메서드로 두고 자식 클래스에서 구현하도록 하는 것이다.

from abc import *

class Person(metaclass=ABCMeta):
    @abstractmethod
    def eat(self):
        pass

    @abstractmethod
    def sleep(self):
        pass

class James(Person):
    def eat(self):
        print("chop chop")

    def sleep(self):
        print("coa coa")

class Dean(Person):
    def eat(self):
        print("yam yam")

    def sleep(self):
        print("zzzz")
    

james = James()
dean = Dean()

james.eat() # chop chop
james.sleep() # coa coa
dean.eat() # yam yam
dean.sleep() # zzzz

다음과 같이 Personeat, sleepJamesDean이 상속받아 오버라이딩하면 된다. 만약 오버라이딩을 하지 않으면 에러가 발생하게 된다.

class Dean(Person):
    def eat(self):
        print("yam yam")

Dean 클래스의 sleep오버라이드 부분을 지우면 TypeError: Can't instantiate abstract class Dean with abstract method sleep 에러가 발생한다.

즉, 추상 클래스의 추상 메서드를 자식 클래스에서 오버라이드하지 않았다는 의미이다.

이처럼 추상 클래스는 자식 클래스가 반드시 구현해야 하는 메서드를 정해줄 수 있다.

1.1 추상 클래스는 인스턴스화 할 수 없다.

여타 다른 언어와 같이 추상 클래스는 인스턴스화 할 수 없다. 이유는 간단하다. 인스턴스화해서 쓰려고 만든게 아니기 때문이다. 그래서 추상 메서드의 본문 부분이 모두 pass인 것이다.

from abc import *

class Person(metaclass=ABCMeta):
    @abstractmethod
    def eat(self):
        pass

    @abstractmethod
    def sleep(self):
        pass

person = Person() # TypeError: Can't instantiate abstract class Person with abstract methods eat, sleep

추상 클래스 Person을 인스턴스화 하려고하니 에러가 발생한다.

따라서 추상 클래스는 오로지 상속에만사용한다. 반드시 자식 클래스에서 추상 클래스를 상속받아 추상 메서드를 구현해주어야 한다.

1.2 추상 클래스도 클래스다

추상 클래스도 클래스이다. 때문에 클래스는 상태(속성, 변수)와 행동(메서드)를 가진다.는 말이 성립된다. 단지, 추상 메서드를 사용할 수 있을 뿐이고, 인스턴스화 하지 못할 뿐이다.

from abc import *

class Person(metaclass=ABCMeta):
    heart = "do geuon do geuon"
    mind = "love!"
    countofheart = 1

    def readme(self):
        return Person.heart + Person.mind + str(Person.countofheart)

    @abstractmethod
    def eat(self):
        pass

    @abstractmethod
    def sleep(self):
        pass

class James(Person):
    def eat(self):
        print(Person.heart, Person.mind , Person.countofheart)
        print("chop chop")

    def sleep(self):
        print(self.readme())
        print("coa coa")

james = James()
james.eat()
# do geuon do geuon love! 1
# chop chop
james.sleep()
# do geuon do geuonlove!1
# coa coa

다음의 예제를 보면 알 수 있듯이, Person은 추상 클래스 임에도 클래스 변수 heart, mind, countofheart를 가질 수 있다. 물론, 맴버변수 또한 가질 수 있다.

readme 메서드는 일반 메서드이므로 추상 클래스에서도 일반 메서드를 만들 수 있다는 것을 확인할 수 있다. 그리고, 상속받은 자식에서도 추상 클래스의 일반 메서드(readme)를 호출할 수 있다는 것을 확인할 수 있다. 물론 맴버 변수도 자식에서 호출 가능하다.

이를 통해서 재밌는 패턴이 가능한데, 가장 유명한게 template 패턴이다.

1.3 추상 클래스를 통한 template 패턴

가령, 어떤 작업에는 일련의 과정을 거쳐야 한다고 하자. 작업을 일반 메서드로 두고, 이 메서드 본문 안에 일련의 과정을 순서대로 넣는다.

단, 일련의 과정은 상황에 따라 다르므로, 자식 클래스가 상속받아 구현하도록 하는 것이다. 말로만 들으면 안느껴지는데, 예시를 보도록 하자

라면을 끓인다고 한다면, 라면 끓이기는어떤 작업이다. 여기에 라면을 어떻게 끓일 지일련의 과정이다. 라면 끓이기 작업물 맞추기 -> 물 끓이기 -> 라면 넣기 과정을 거치지만, 물 맞추기, 물 끓이기, 라면 넣기는 사람들 마다, 그리고 라면마다 다 다르다.

  1. 먼저 우리는 라면 끓이기 과정이 물 맞추기 -> 물 끓이기 -> 라면 넣기라는 것을 통일했다. 그렇다면 이것을 다음과 같이 써줄 수 있다.
from abc import *

class Recipe(metaclass=ABCMeta):
    def makeFood(self):
        self.getWater()
        self.boilWater()
        self.setSoup()

Recipe 클래스를 만들었다. 라면 끓이기인 makeFood는 뭔지 모르겠지만 물 받고, 물 끓이고, 라면 넣고 과정을 거치는 것이다. 그럼 다음의 각 과정들은 경우(상황)에 따라 달라지므로 추상 메서드로 나두어서 자식 클래스에서 구체적으로 구현할 수 있도록 해준다.

class Recipe(metaclass=ABCMeta):
    def makeFood(self):
        self.getWater()
        self.boilWater()
        self.setSoup()
    
    @abstractmethod
    def getWater(self):
        pass

    @abstractmethod
    def boilWater(self):
        pass

    @abstractmethod
    def setSoup(self):
        pass

추상 클래스인 getWater, boilWater, setSoup은 자식 클래스에서 구체화해주면 된다.

class GyuRamen(Recipe):
    def getWater(self):
        print("500ml!")
    
    def boilWater(self):
        print("boggle boggle!")

    def setSoup(self):
        print("soup first and then noodle")

gyuRamen = GyuRamen()
gyuRamen.makeFood()
# 500ml!
# boggle boggle!
# soup first and then noodle

다음과 같이 자식 클래스에서 해당 추상 메서드를 오버라이드하고, makeFood() 메서드를 호출하면 일련의 과정인 getWater, boilWater, setSoup가 실행된다. 이는 자식 클래스에서 구현한 대로 작동하게 될 것이다.

어떻게 이것이 되는가?? 한다면, 아직 상속에서대해서 잘 이해하지 못한 것이다. 상속은 자식 클래스가 부모 클래스의 속성과 메서드를 가져온다.

사실 상속하면 이런 그림이긴 하지만, 그림에 현혹되서 makeFood는 부모 클래스에 있는데 어떻게 자식이 호출하나?? 할 수 있다. 그게 아니라 자식 클래스가 부모 클래스의 속성과 메서드를 모두 가지고 있어 호출이 가능한 것이다.

이렇게 이해하는 것이 좋다. 위 아래로 관계로 진짜 저장되는 것이 아닌, 부모의 속성과 메서드를 자식이 가지고 있게하는 것이 상속인 것이다. 따라서 문제없이 makeFood를 자식에서 정의한 getWater, boilWater, setSoup로 사용할 수 있는 것이다.

이렇게 사용하는 것이 바로 template 패턴이다. 디자인 패턴 중에 가장 기본적이지만 알면 굉장히 써먹을 것이 많은 패턴이고 인터페이스와는 다른 추상 클래스의 특징을 잘 사용한 패턴이다.

2. 덕 타이핑(Duck Typing)

만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 그 새를 오리라고 부를 것이다.

이게 무슨 말인가? 싶을수도 있는데, 간단히 말해서 오리처럼 할 수 있다면 그게 새이든 사람이든 고물이든 로봇이든 상관없이 오리처럼 다루겠다는 것이다.

이는 객체의 타입이 중요한 프로그래밍 방식이 아닌 객체의 쓰임, 사용되는 양상(기능)이 더 중요하다는 것을 말한다.

가령, 정적 타입을 지원하는 언어의 경우 타입이 매칭되기 위해서는 상속을 이용해서 사용하는 방법이 있다.

https://ideone.com/

다음의 사이트에 가서 아래의 코드를 구동시켜보자

import java.util.*;
import java.lang.*;
import java.io.*;

class Duck{
	public void quack(){
		System.out.println("quack!");
	}
	public void swim(){
		System.out.println("swim swim!!");
	}
}
	
class BabyDuck extends Duck{
	
}

class RobotDuck{
	public void quack(){
		System.out.println("weong~ quack!");
	}
	public void swim(){
		System.out.println("chick! swim swim!!");
	}
}

class Ideone
{
	public static void main (String[] args) throws java.lang.Exception
	{
		Duck duck = new BabyDuck();
		duck.quack(); // quack!
		duck.swim(); // swim swim!!
	}
}

위의 코드는 javaDuck 클래스를 구현해놓았다. 이 Duck 클래스를 BabyDuck가 상속 받는다.

Duck duck = new BabyDuck();
duck.quack(); // quack!
duck.swim(); // swim swim!!

그리고 다음이 핵심인데, java는 부모 클래스 타입의 변수에 자식 클래스의 인스턴스를 넣을 수 있다. 왜냐하면 is-a관계이기 때문이다. BabyDuck는 Duck이다.따라서 DuckBabyDuck가 들어갈 수 있는 것이다.

반면, RobotDuck은 상속은 안받았지만 quack(), swim()을 구현하였다. 그럼 `Duck 클래스 타입을 가지는 변수에 들어갈 수 있을까??

Duck duck = new RobotDuck();
duck.quack();
duck.swim();

어림도 없다 암! 물론 안된다. 이는 상속 관계가 아니므로 타입 매칭이 안되기 때문이다.

그런데, 이러한 프로그래밍 방식에 정면으로 반대하는 것이 바로 덕 타이핑이다. 덕 타이핑은 타입이 중요한 것이 아니라, 기능, 사용하는 쓰임, 양상이 중요하기 때문이다.

파이썬은 덕 타이핑을 지원한다. 따라서 상속 관계가 아니라도 객체가 가진 메서드와 속성을 통해 함수나 로직이 제대로 작동하도록 한다.

class Duck:
    def __init__(self):
        self.name = "duck"
    def quack(self):
        print("quack! quack!")
    
    def swim(self):
        print("swim swim")
    
class BabyDuck(Duck):
    pass

class RobotDuct:
    def __init__(self):
        self.name = "robot duck"
    def quack(self):
        print("weong~~~!! quack! quack!")
    
    def swim(self):
        print("chic~~~~~ swim swim")

다음의 코드는 위의 자바 코드를 파이썬으로 구현한 것이다. 다만 추가적으로 클래스의 속성도 덕 타이핑에 속한다는 것을 보여주기 위해 self.name을 추가했다.

이제 이를 실험하기 위한 test 함수를 준비해보자

def duckTest(duck):
    duck.quack()
    duck.swim()
    print(duck.name)

다음의 duckTest 함수는 duck을 받아서 quack(), swim() 메서드를 실행하고, name을 불러온다. 만약 자바였다면 duck은 지정한 타입과 이를 상속받은 자식 클래스들만 올 수 있을 것이다.

그러나, 파이썬은 덕 타이핑을 지원한다. 그냥 quack, swim, .name을 가지고만 있다면 누구든 들어가서 에러를 내지 않을 수 있다.

duckTest(Duck())
# quack! quack!
# swim swim
# duck
duckTest(BabyDuck())
# quack! quack!
# swim swim
# duck
duckTest(RobotDuct())
# weong~~~!! quack! quack!
# chic~~~~~ swim swim
# robot duck

BabyDuck은 어차피 Duck 클래스의 속성과 메서드를 상속받으므로 문제없이 구동될 것이다. 관건은 RobotDuct인데 아주 문제없이 잘 돌아간다.

이 처럼 덕 타이핑은 클래스의 타입, 상속 관계가 중요하기 보다는 객체가 사용되는 양상(메서드, 속성)이 더 중요하다는 것이다. 지정한 메서드나 속성만 잘 구현되어 있다면 정상적으로 작동하는 것이다.

덕 타이핑을 지원하는 또 다른 언어로는 타입스크립트도 있다. 타입스크립트도 두 타입이 같은 지 다른 지 판별하는데 상속관계로 파악하는 것이 아닌, 메서드나 속성이 같은 지로 본다. 이는 자바스크립트가 정적 타입을 지원하지 않았었기 때문이다.

파이썬 역시도 자바스크립트와 마찬가지로 정적 타입을 기본적으로 지원하지 않기 때문에 덕 타이핑을 권장하고 쉽게 사용할 수 있다.( 원한다면 정적 타이핑을 사용할 수도 있다. )

물론, 정적 타이핑을 지원하는 c++, java에서는 덕 타이핑을 못쓰는 것은 아니다. c++template문법이나, javaGeneric을 사용하면 얼마든지 덕 타이핑을 사용할 수 있다.

정리하면 파이썬은 덕 타이핑을 지원하며 덕 타이핑은 속성과 메소드 존재에 의해 객체의 적합성이 결정된다

0개의 댓글