점프 투 파이썬 : https://wikidocs.net/book/1
파이썬 기본을 갈고 닦자 : https://wikidocs.net/16031
코딩 도장 : https://dojang.io/mod/page/view.php?id=2378
추상 클래스(absctract class)
는 메서드의 목록만 가진 클래스이며, 상속받는 클래스에서 메서드 구현을 강제하기 위해 사용한다.
추상 클래스를 사용하려면, import
로 abc
모듈을 가져와야 한다. 그리고 클래스의 괄호
안에 `metaclass=ABCMeata
를 지정해야 한다. 추상 메서드를 만들 때는 메서드 위에 @abstractmethod
를 붙여 추상 메서드로 지정한다.
from abc import *
class AClass(metaclass=ABCMeta):
@abstractmethod
def absctractMethod(self):
pass
다음과 같이 써줄 수 있다.
추상 클래스는 어떨 때 쓸까?? 추상 클래스는 현재 클래스에서는 행동(메서드)
를 지정하기 애매하거나 너무 다양할 때 사용한다.
가령, Person
이라면 eat
, sleep
등이 있을 것이다. 그런데 Person
이 직접 밥을 어떻게 먹고, 어떻게 잠을 자고하는 것을 정의할 수는 없다. 이건 구체적인 각 인간들마다 다르기 때문이다. 그렇기 때문에 Person
의 eat, 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
다음과 같이 Person
의 eat, sleep
을 James
와 Dean
이 상속받아 오버라이딩하면 된다. 만약 오버라이딩을 하지 않으면 에러가 발생하게 된다.
class Dean(Person):
def eat(self):
print("yam yam")
Dean
클래스의 sleep
오버라이드 부분을 지우면 TypeError: Can't instantiate abstract class Dean with abstract method sleep
에러가 발생한다.
즉, 추상 클래스의 추상 메서드를 자식 클래스에서 오버라이드하지 않았다는 의미이다.
이처럼 추상 클래스는 자식 클래스가 반드시 구현해야 하는 메서드를 정해줄 수 있다.
여타 다른 언어와 같이 추상 클래스는 인스턴스화 할 수 없다. 이유는 간단하다. 인스턴스화해서 쓰려고 만든게 아니기 때문이다. 그래서 추상 메서드의 본문 부분이 모두 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
을 인스턴스화 하려고하니 에러가 발생한다.
따라서 추상 클래스는 오로지 상속에만사용한다. 반드시 자식 클래스에서 추상 클래스를 상속받아 추상 메서드를 구현해주어야 한다.
추상 클래스도 클래스이다. 때문에 클래스는 상태(속성, 변수)와 행동(메서드)를 가진다.
는 말이 성립된다. 단지, 추상 메서드를 사용할 수 있을 뿐이고, 인스턴스화 하지 못할 뿐이다.
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 패턴
이다.
template
패턴가령, 어떤 작업에는 일련의 과정을 거쳐야 한다고 하자. 작업
을 일반 메서드로 두고, 이 메서드 본문 안에 일련의 과정
을 순서대로 넣는다.
단, 일련의 과정
은 상황에 따라 다르므로, 자식 클래스
가 상속받아 구현하도록 하는 것이다. 말로만 들으면 안느껴지는데, 예시를 보도록 하자
라면을 끓인다고 한다면,
라면 끓이기는
은어떤 작업
이다. 여기에라면을 어떻게 끓일 지
는일련의 과정
이다.라면 끓이기 작업
은물 맞추기 -> 물 끓이기 -> 라면 넣기
과정을 거치지만,물 맞추기, 물 끓이기, 라면 넣기
는 사람들 마다, 그리고 라면마다 다 다르다.
라면 끓이기
과정이 물 맞추기 -> 물 끓이기 -> 라면 넣기
라는 것을 통일했다. 그렇다면 이것을 다음과 같이 써줄 수 있다.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
패턴이다. 디자인 패턴 중에 가장 기본적이지만 알면 굉장히 써먹을 것이 많은 패턴이고 인터페이스와는 다른 추상 클래스의 특징을 잘 사용한 패턴이다.
만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 그 새를 오리라고 부를 것이다.
이게 무슨 말인가? 싶을수도 있는데, 간단히 말해서 오리처럼 할 수 있다면 그게 새이든 사람이든 고물이든 로봇이든 상관없이 오리처럼 다루겠다는 것이다.
이는 객체의 타입이 중요한 프로그래밍 방식이 아닌 객체의 쓰임, 사용되는 양상(기능)이 더 중요하다는 것을 말한다.
가령, 정적 타입을 지원하는 언어의 경우 타입이 매칭되기 위해서는 상속을 이용해서 사용하는 방법이 있다.
다음의 사이트에 가서 아래의 코드를 구동시켜보자
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!!
}
}
위의 코드는 java
로 Duck 클래스
를 구현해놓았다. 이 Duck 클래스를 BabyDuck
가 상속 받는다.
Duck duck = new BabyDuck();
duck.quack(); // quack!
duck.swim(); // swim swim!!
그리고 다음이 핵심인데, java
는 부모 클래스 타입의 변수에 자식 클래스의 인스턴스
를 넣을 수 있다. 왜냐하면 is-a
관계이기 때문이다. BabyDuck는 Duck이다.
따라서 Duck
에 BabyDuck
가 들어갈 수 있는 것이다.
반면, 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
문법이나, java
의 Generic
을 사용하면 얼마든지 덕 타이핑을 사용할 수 있다.
정리하면 파이썬은 덕 타이핑을 지원하며 덕 타이핑은 속성과 메소드 존재에 의해 객체의 적합성이 결정된다