본문 바로가기

IT/Python

[객체지향 파이썬 프로그래밍] __init__() method [1]

1. 일반적인 클래스 계층 구조를 정의한 예제


__ init __메소드를 최상위 클래스인 Card 에 포함시킴으로써 최상위 클래스의 초기화를 세 개의 하위 클래스인 

NumberCard와 AceCardFaceCard에 공통적으로 적용했습니다.


예제는 일반적인 다형성 디자인입니다. 하위 클래스는 _points() 메소드를 각각 구현합니다.

모든 하위 클래스는 동일한 서명을 갖습니다. 즉, 같은 메소드와 속성을 갖습니다.


  • NumberCardAceCardFaceCard는 Card클래스를 상속받습니다.

  • Card 클래스를 상속받은 하위 클래스들은 _points 메소드를 암묵적으로 overriding 합니다. -> 부모 클래스에서 명시적으로 _points 메소드를 선언하지 않았음.

  • 그리고 이를 이용해 여러 card 인스턴스를 생성합니다.


Card class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Card:
    def __init__(self, rank, suit):
        self.suit = suit
        self.rank = rank
        self.hard, self.soft = self._points()
 
class NumberCard(Card):
    def _points(self):
        return int(self.rank), int(self.rank)
 
class AceCard(Card):
    def _points(self):
        return 111
 
class FaceCard(Card):
    def _points(self):
        return 1010
 
cards = [AceCard('A''♠'), NumberCard('2''♠'), NumberCard('3''♠')]
 
for in cards:
    print("suit : " + str(i.suit))
    print("rank : " + str(i.rank))
    print("hard : " + str(i.hard))
    print("soft : " + str(i.soft))
 
print('-------------------------------')
 
 
>>suit : ♠
rank : A
hard : 1
soft : 11
suit : ♠
rank : 2
hard : 2
soft : 2
suit : ♠
rank : 3
hard : 3
soft : 3
-------------------------------


  • 이러한 방법으로 52장의 카드를 모두 열거해서 생성을 하게된다면, 같은 타이핑의 증가로 인해 지루함오타 및 오류의 가능성이 증가합니다.
  • 따라서 이를 간편하게 만들기 위해서 팩토리(factory) 함수가 필요해집니다.

팩토리 함수를 살펴보기 전에, 다른 방법을 먼저 살펴보겠습니다.

2. __init__()으로 메니페스트 상수 생성


명백한/명시적인 상수(manifest constant) 클래스의 정의하는 것을 통해서 스위트의 클래스를 정의할 수 있습니다.

블랙잭에서는 스위트는 중요하지 않으며, 간단한 문자열로 표현할 수 있습니다.

상수 객체 생성의 한 예로 스위트 클래스를 만들어 보겠습니다. 대부분의 어플리케이션은 상수 집합으로 정의할 수 있는 객체 도메인을 포함합니다.

이러한 고정 객체 도메인 역시 전략이나 상태디자인 패턴 구현의 하나입니다.


  • 상태에 대해서 하나의 클래스로 관리를 합니다.
  • 이를 통해서 상태 풀(pool)을 따로 관리하는 것을 통해서, 추후 프로젝트의 변경사항이 생길 때, 상태 클래스만 변경하는 것을 통해서 유연함을 증가시킬 수 있습니다.


mnifest constant
1
2
3
4
class Suit:
    def __init__(self, name, symbol):
        self.name = name
        self.symbol = symbol


다음은 위 클래스 주변에 선언할 상수도메인입니다.


1
Club, Diamond, Heart, Spade = Suit('Club''♣'), Suit('Diamond''◆'), Suit('Heart''♥'), Suit('Spade''♠')


이제 다음 코드 조각처럼 Card를 생성합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# __init__()으로 매니페스트 상수 생성 --------------------------------------------------------------------------------------
 
class Suit:
    def __init__(self, name, symbol):
        self.name = name
        self.symbol = symbol
 
class Card:
    def __init__(self, rank, suit):
        self.suit = suit
        self.rank = rank
        self.hard, self.soft = self._points()
 
class NumberCard(Card):
    def _points(self):
        return int(self.rank), int(self.rank)
 
class AceCard(Card):
    def _points(self):
        return 111
 
class FaceCard(Card):
    def _points(self):
        return 1010
 
Club, Diamond, Heart, Spade = Suit('Club''♣'), Suit('Diamond''◆'), Suit('Heart''♥'), Suit('Spade''♠')
print("Club, Diamond, Heart, Spade : {},{},{},{}".format(Club, Diamond, Heart, Spade))
print('-------------------------------')
cards = [AceCard('A', Spade), NumberCard('2', Spade), NumberCard('3', Spade), NumberCard('3', Spade)]
print("cards : " + str(cards))
print('-------------------------------')
for in cards:
    print("OBJECT : " + str(i))
    print("SUIT : " + str(i.suit))
    print("suit name : " + str(i.suit.name))
    print("suit name : " + str(i.suit.symbol))
    print("rank : " + str(i.rank))
    print("hard : " + str(i.hard))
    print("soft : " + str(i.soft))
    print('-------------------------------')
 
 
>> Club, Diamond, Heart, Spade : <__main__.Suit object at 0x10c192438>,<__main__.Suit object at 0x10c192470>,<__main__.Suit object at 0x10c1924a8>,<__main__.Suit object at 0x10c1924e0>
-------------------------------
cards : [<__main__.AceCard object at 0x10c192550>, <__main__.NumberCard object at 0x10c192588>, <__main__.NumberCard object at 0x10c1925c0>, <__main__.NumberCard object at 0x10c1925f8>]
-------------------------------
OBJECT : <__main__.AceCard object at 0x10c192550>
SUIT : <__main__.Suit object at 0x10c1924e0>
suit name : Spade
suit name : ♠
rank : A
hard : 1
soft : 11
-------------------------------
OBJECT : <__main__.NumberCard object at 0x10c192588>
SUIT : <__main__.Suit object at 0x10c1924e0>
suit name : Spade
suit name : ♠
rank : 2
hard : 2
soft : 2
-------------------------------
OBJECT : <__main__.NumberCard object at 0x10c1925c0>
SUIT : <__main__.Suit object at 0x10c1924e0>
suit name : Spade
suit name : ♠
rank : 3
hard : 3
soft : 3
-------------------------------
OBJECT : <__main__.NumberCard object at 0x10c1925f8>
SUIT : <__main__.Suit object at 0x10c1924e0>
suit name : Spade
suit name : ♠
rank : 3
hard : 3
soft : 3
-------------------------------


해당 예제처럼 개수가 적은 경우는 한 문자로 된 스위트 코드를 사용할 때에 비해, 메소드의 성능이 크게 향상되지 않습니다. 조금 더 복잡하게는 전략이나 상태 객체를 위와 같은 짧은 리스트로 생성할 수 있습니다.

작고 고정된 풀의 상수를 사용해 객체를 재사용하면 전략이나 상태 디자인 패턴이 조금 더 효율적으로 동작합니다.


3. 팩토리 함수로 __init__() 활용


파이썬에서 팩토리는 대개 다음과 같은 두가지 방식으로 접근합니다.


  • 필요한 클래스의 객체를 생성하는 함수를 정의합니다.
  • 객체 생성 메소드를 포함하는 클래스를 정의합니다. 디자인 패턴 책에서 설명하듯이 이 방식은 완벽한 팩토리 디자인 패턴입니다. 
    자바 같은 언어는 독립형 함수를 지원하지 않으므로 팩토리 클래스 계층 구조가 필요합니다.


클래스 정의의 대표적인 장점은 상속을 통한 코드의 재사용입니다. 팩토리 클래스의 함수는 타겟 클래스 계층 구조와 복잡한 객체 생성을 랩핑(wrapping) 하는데 있습니다.

팩토리 클래스를 만들면 타겟 클래스 계층 구조가 확장될 때, 팩토리 클래스에 하위 클래스를 추가할 수 있습니다.


즉, 동일한 메소드 이름을 갖는 서로 다른 팩토리 클래스 정의 간에 호환해서 사용할 수 있는 다형성 팩토리 클래스가 됩니다.


정의한 팩토리가 코드를 재사용하지 않으면, 클래스 계층 구조는 파이썬에서 의미가 없습니다. 이럴 경우에는 단순히 동일한 이름의 메소드를 사용합니다.


다음은 다양한 Card 하위 클래스의 팩토리 함수입니다.


1
2
3
4
5
6
7
8
def card(rank, suit):
    if rank == 1 return AceCard('A', suit)
    elif 2<= rank < 11 return NumberCard(str(rank), suit)
    elif 11 <= rank <14 :
        name = {11'J'12'Q'13'K'}[rank]
        return FaceCard(name, suit)
    else:
        raise Exception("Rank out of range")


위 코드는 모든 랭크와 스위트를 열거해서 완전한 52장의 카드 덱을 만듭니다.


전체코드는 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class Suit:
    def __init__(self, name, symbol):
        self.name = name
        self.symbol = symbol
 
class Card:
    def __init__(self, rank, suit):
        self.suit = suit
        self.rank = rank
        self.hard, self.soft = self._points()
 
class NumberCard(Card):
    def _points(self):
        return int(self.rank), int(self.rank)
 
class AceCard(Card):
    def _points(self):
        return 111
 
class FaceCard(Card):
    def _points(self):
        return 1010
 
def card(rank, suit):
    if rank == 1 return AceCard('A', suit)
    elif 2<= rank < 11 return NumberCard(str(rank), suit)
    elif 11 <= rank <14 :
        name = {11'J'12'Q'13'K'}[rank]
        return FaceCard(name, suit)
    else:
        raise Exception("Rank out of range")
 
Club, Diamond, Heart, Spade = Suit('Club''♣'), Suit('Diamond''◆'), Suit('Heart''♥'), Suit('Spade''♠')
deck = [card(rank, suit) for rank in range(114for suit in (Club, Diamond, Heart, Spade)]
 
print("------------------------------------------------")
for in range(len(deck)):
    print("OBJECT : {}".format(deck[i]))
    print("suit : {}".format(deck[i].suit))
    print("suit->name : {}".format(deck[i].suit.name))
    print("suit->symbol : {}".format(deck[i].suit.symbol))
    print("rank : {}".format(deck[i].rank))
    print("hard : {}".format(deck[i].hard))
    print("soft : {}".format(deck[i].soft))
    print("------------------------------------------------")
 
>>------------------------------------------------
OBJECT : <__main__.AceCard object at 0x102d6f588>
suit : <__main__.Suit object at 0x102d6f470>
suit->name : Club
suit->symbol : ♣
rank : A
hard : 1
soft : 11
------------------------------------------------
OBJECT : <__main__.AceCard object at 0x102d6f5c0>
suit : <__main__.Suit object at 0x102d6f4a8>
suit->name : Diamond
suit->symbol : ◆
rank : A
hard : 1
soft : 11
------------------------------------------------
OBJECT : <__main__.AceCard object at 0x102d6f5f8>
suit : <__main__.Suit object at 0x102d6f4e0>
suit->name : Heart
suit->symbol : ♥
rank : A
hard : 1
soft : 11
------------------------------------------------
.
.
.
(중략)


3-1). 불완전한 팩토리 디자인과 모호한 else 절

 

위의 코드에서 card()함수의 if문 구조를 다시 살펴보면, 모든 if 조건을 만족하지 않을 때, 수행하는 else절에서 별다른 액션 없이 예외만 발생시킵니다.

여기서 else절의 사용에는 다소 논란이 있는 부분이 있습니다.


  • else절에 조건을 명시하지 않으면, 잡아내기 어려운 디자인 오류가 발생할 수 있으므로, 빈 채로 두어서는 안 된다는 주장
  • else절의 조건이 너무 명백할 때는 큰 문제가 없다는 주장


여기서 제일 중요한 것은 모호한 else절을 피하는 것이 가장 중요합니다.


1
2
3
4
5
6
7
8
9
def card(rank, suit):
    if rank == 1 return AceCard('A', suit)
    elif 2<= rank < 11 return NumberCard(str(rank), suit)
    else :
        name = {11'J'12'Q'13'K'}[rank]
        return FaceCard(name, suit)
 
 
deck = [card(rank, suit) for rank in range(114for suit in (Club, Diamond, Heart, Spade)]


위 함수는 동작하는지, 만약 if 조건이 조금 더 복잡하면 어떻게 될지 생각해볼 필요가 있습니다.


  • 위 if문을 한눈에 이해하는 프로그래머도 있는 반면, 누군가의 모든 조건이 상호 베타적으로 올바른지 면밀히 따져보는 프로그래머도 있습니다.
  • 고급 파이썬 프로그래밍에서는 else절의 조건을 추측하게 두지 않습니다. 조건은 초보에게도 명백하던지, 외부에 명시적으로 드러나야 합니다.


* 그럼 언제 else를 사용할까요?

거의 없습니다. 조건이 확실할 때만 사용합니다. 의심이 들 때는 명확한 조건을 명시한 후, 예외를 발생시키는 else를 사용합니다.

절대적으로 모호한 else절을 피해야 합니다.


3-2). elif 시퀸스를 이용한 단순화와 일관성


팩토리 함수 card() 에는 매우 일반적인 두가지 팩토리 디자인 패턴이 섞여 있습니다.

  • if-else 시퀸스
  • 매핑(mapping)


이 둘 중 하나만 사용해 단순화하는게 좋습니다.

매핑은 항상 elif 조건으로 대체할 수 있습니다.(언제나 그렇습니다. 하지만 역은 성립하지 않습니다. elif 조건을 매핑으로 변경하기는 까다롭습니다)


다음 코드는 매핑이 없는 Card팩토리 입니다.


1
2
3
4
5
6
7
8
9
10
11
def card(rank, suit):
    if rank == 1 return AceCard('A', suit)
    elif 2<= rank < 11 return NumberCard(str(rank), suit)
    elif rank == 11:
        return FaceCard('J', suit)
    elif rank == 12:
        return FaceCard('Q', suit)
    else rank == 13:
        return FaceCard('K', suit)
    else:
        raise Exception("Rank out of range")


card() 팩토리 함수를 다시 작성했습니다. 매핑은 추가적인 elif절로 변환했습니다. 

이 함수는 이전 버전보다 조금 더 일관적인 것을 확인할 수 있습니다.


3-3). 매핑과 클래스 객체를 이용한 단순화


순차적인 elif 조건 대신 맵핑을 사용할 수도 있습니다. 매우 복잡한 조건을 표현할 때는 elif 조건 나열이 합리적입니다.

하지만 단순한 경우라면 매핑이 종종 더 나은 성능을 발휘하며, 가독성도 좋습니다.


class는 일급 클래스 객체이므로 rank 매개변수를 생성하는 클래스와 쉽게 매핑할 수 있습니다.

다음은 매핑만 사용하는 Card 팩토리입니다.


1
2
3
def card(rank, suit):
    class_ = {1: AceCard, 11:FaceCard, 12:FaceCard, 13:FaceCard}.get(rank, NumberCard)
    return class_(rank, suit)


rank 객체를 임의의 한 클래스로 매핑했습니다. 이어 이 클래스에 rank와 suit값을 넘겨 최종 Card 인스턴스를 만듭니다.


defaultdict 클래스도 사용할 수 있습니다.


defaultdict(lambda: NumberCard, {1:AceCard, 11:FaceCard, 12:FaceCard, 12:FaceCard})


defaultdict 클래스는 기본적으로 0개의 매개변수를 갖는 메소드여야 합니다. 

따라서 lambda 생성자를 사용하여 상수를 위한 함수 래퍼를 만듭니다. 하지만 위 함수에는 심각한 결점이 있습니다.


  • 이전 버전에 있던 1를 A로, 13을 K로 바꾸는 변환이 누락되었습니다.


이러한 기능을 추가하려면 다음과 같은 문제가 생깁니다.


  • Card 하위 클래스와 문자열로 된 rank 객체를 제공하려면 매핑을 변경해야합니다. 


이런 경우, 다음과 같은 4가지 해결책을 사용합니다.


  1. 두 병렬 매핑(parallel mapping)을 할 수 있습니다. 제안하고 싶은 방법은 아니며, 어느 부분이 바람직하지 않은지 강조할 목적으로 살펴봅니다.
  2. 2 튜플(tuple) 에 매핑할 수 있습니다. 이때도 단점이 있습니다.
  3. partial() 함수에 매핑할 수 있습니다. partial() 함수는 functools 모듈에 속합니다.
  4. 위와 같은 매핑에 좀 더 잘 어올리게 클래스 정의를 수정하는 방법


두 병렬 매핑


두 병렬 매핑을 사용하는 기본적인 방법은 다음과 같습니다.

def card(rank, suit):
    class_ = {1: AceCard, 11:FaceCard, 12:FaceCard, 13:FaceCard}.get(rank, NumberCard)
    rank_str = {1:'A'11:'J'12:'Q'13:'K'}.get(rank, str(rank))
    return class_(rank_str, suit)


해당 방법은 매핑 키 1,11,12,13의 순서가 반복되므로 바람직하지 않습니다. 

소프트웨어를 업데이트하다보면 병렬 구조가 바뀔 수 있으므로 이러한 반복은 좋은 방법이 아닙니다.


* 병렬 구조를 사용하지 않습니다.

두 병렬 구조는 튜플이나 다른 종류의 적절한 콜렉션으로 대체해야합니다.


부분 함수 방식


2 튜플 함수와 두 가지 매개변수 중 하나에 매핑하는 방법 외에도 partial()함수를 생성할 수 있습니다. 이 함수는 이미 제공받은 매개변수의 일부(전부는 아닙니다.)를 가집니다.

functools 라이브러리의 partial() 함수를 사용해 rank 매개변수를 포함하는 부분 클래스를 만듭니다.


다음은 객체 생성에 사용할 partial() 함수와 rank의 매핑입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
from functools import partial
 
 
def card(rank, suit):
    part_class = {
        1: partial(AceCard, 'A'),
        11: partial(FaceCard, 'J'),
        12: partial(FaceCard, 'Q'),
        13: partial(FaceCard, 'K'),
    }.get(rank, partial(NumberCard, str(rank)))
 
 
    return part_class(suit)


rank 객체를 part_class에 할당된 partial() 함수에 매핑합니다. 

이후 partial() 함수는 suit 객체를 넘겨받아 최종 객체를 생성합니다. 함수형 프로그래밍에서 partial() 함수는 매우 일반적으로 사용되는 기법입니다.

위 예제처럼 객체 메소드가 아닌, 함수가 있는 특정 상황에서 사용합니다.


하지만 partial() 함수는 일반적으로 대부분의 객체지향 프로그래밍에서 쓸모가 없습니다. 클래스의 메소드를 간단히 업데이트하면 partial() 함수를 생성하지 않아도

서로 다른 조합의 매개변수를 받을 수 있습니다. partial() 함수는 객체를 생성하는 플루언스 인터페이스(fluent interface) 생성과 유사합니다.


팩토리용 플루언트 API

정의한 순서대로 메소드를 사용해야 하는 클래스를 디자인할 때가 있습니다. 메소드의 순차적 실행은 partial() 함수 생성과 매우 비슷합니다.

x.a().b()와 같은 객체 표기를 생각해봅시다. 이는 x(a,b)로 볼 수 있습니다..

x.a() 함수는 b()를 기다리는 partial()  함수의 일종입니다. 이 경우에는 x(a) (b)로 생각할 수 있습니다.


파이썬은 두 가지 방법으로 상태를 관리합니다. 객체를 업데이트할 수도 있고, (어느정도) 안정적인 partial() 함수를 생성할 수도 있습니다.

두 방법의 결과는 동일하므로 partial() 함수를 플루언트 팩토리 객체로 다시 쓸 수 있습니다. 

self를 반환하는 플루언트 메소드로 rank 객체를 설정합니다. suit 객체 설정에서 실제 Card 인스턴스를 생성합니다.


다음은 특정 순서로 사용해야 하는 두 개의 메소드 함수를 포함하는 Card 플루언트 팩토리 클래스입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
class CardFactory:
    def rank(self, rank):
        self.class_self.rank_str={
            1:(AceCard, 'A'),
            11:(FaceCard, 'J'),
            12:(FaceCard, 'Q'),
            13:(FaceCard, 'K'),
        }
 
        return self
 
    def suit(self, suit):
        return self.class_(self.rank_str,suit)


rank() 메소드는 생성자의 상태를 업데이트하고, suit() 메소드는 최종 Card 객체를 생성합니다.


위 팩토리 클래스를 다음과 같이 사용합니다.


1
2
card = CardFactory()
deck = [card.rank(r+1).suit(s) for in range(13for in (Club, Diamond, Heart, Spade]


팩토리 인스턴스를 먼저 생성한 후 Card 인스턴스 생성에 사용합니다. 이렇게해도 Card 클래스 계층 구조 내에서 __init__()의 실제 동작 방식은 변하지 않습니다.

다만 클라이언트 어플리케이션의 객체 생성 방법이 변합니다.