티스토리 뷰

 

클래스의 필요성을 어떻게 설명하면 쉽게 와닿을까 고민하다가 예시를 만들어보았습니다. 우리의 민속놀이(?)인 스타크래프트의 대표 유닛 마린을 파이썬으로 구현해보겠습니다. 전부 다 구현할 수는 없으니, 아주 간단히 체력, 공격력, 방어력 정도만 만들어보죠. 마린 2-3마리를 만들어서 서로 공격을 시킬 겁니다.

 

우선 마린을 만들고 공격 함수 shoot을 만들었습니다.

 

marine1 = {'unit': 'marine', 'hp': 40, 'attack': 6, 'armor': 0}
marine2 = {'unit': 'marine', 'hp': 40, 'attack': 6, 'armor': 0}

def shoot(me, enemy):
    enemy['hp'] -= me['attack'] - enemy['armor']

 

 

이제 1번 마린에게 2번 마린을 쏘라고 시켜보죠.

 

>>> shoot(marine1, marine2)  # marine1 shoots marine2
>>> print(marine1['hp'], marine2['hp'])
40 34

 

 

일단 만들어진 것 같긴 한데마린 한 마리 만들 때마다 저 긴 줄을 쓰자니 좀 불편합니다. 마린은 배럭에서 만들어야죠.

 

def barracks():
    return {'unit': 'marine', 'hp': 40, 'attack': 6, 'armor': 0}

def shoot(me, enemy):
    enemy['hp'] -= me['attack'] - enemy['armor']

 

 

똑같이 1번 마린에게 2번 마린을 쏘게 시켜봅니다.

 

>>> marine1 = barracks()
>>> marine2 = barracks()
>>> shoot(marine1, marine2)  # marine1 shoots marine2
>>> print(marine1['hp'], marine2['hp'])
40 34

 

결과는 예상대로 동일합니다. 그런데 이 코드는 아쉬운 점이 있습니다. barracks가 반환하는 마린 유닛(딕셔너리)shoot 함수는 모두 마린이라는 하나의 대상을 표현하는 데에 필요한 두 요소입니다. 마린 유닛과 shoot 함수 중 하나만 있어서는 쓸모가 없습니다. 이왕이면 묶어서 관리하는 게 좋지 않을까요?

 

def barracks():
    def shoot(me, enemy):
        enemy['hp'] -= me['attack'] - enemy['armor']
    return {'unit': 'marine', 'hp': 40, 'attack': 6, 'armor': 0, 'shoot': shoot}

 

 

위 코드의 barracks 함수는 함수 내에서 정의된 shoot 함수를 마린 유닛에 포함시켜서 반환합니다. 이제 함수를 따로 관리하지 않아도 되겠군요. 동작도 확인해보겠습니다.

 

>>> marine1 = barracks()
>>> marine2 = barracks()
>>> marine1['shoot'](marine1, marine2)  # marine1 shoots marine2
>>> print(marine1['hp'], marine2['hp'])
40 34

 

잘 되는군요! 이 정도면 꽤 잘 된 것 같은데이제 진짜 문제를 만들어보겠습니다. 마린의 공방업은 엔지니어링 베이에서 합니다. 엔지니어링 베이에서 업그레이드가 완료되면, 앞으로 만들어질 마린뿐만 아니라 이미 만들어진 마린도 모두 업그레이드가 되어야 합니다.

 

 

일단 앞으로 만들 마린부터 업그레이드 해보죠.

 

attack = 6  # default attack
armor = 0  # default armor
	
def engineering_bay(upgrade):
    global attack, armor
    if upgrade == 'attack':
        attack += 1
    elif upgrade == 'armor':
        armor += 1

def barracks():
    def shoot(me, enemy):
        enemy['hp'] -= me['attack'] - enemy['armor']
    return {'unit': 'marine', 'hp': 40, 'attack': attack, 'armor': armor, 'shoot': shoot}

 

아쉬운대로 global을 사용했습니다. 공방업 1씩 시켜볼건데, 업그레이드 전과 후를 비교해보겠습니다.

 

>>> marine1 = barracks()
>>> marine2 = barracks()
>>> engineering_bay('attack')  # upgrade marine attack
>>> engineering_bay('armor')  # upgrade marine armor
>>> marine3 = barracks()
>>> print(marine1['attack'], marine1['armor'])
6 0
>>> print(marine2['attack'], marine2['armor'])
6 0
>>> print(marine3['attack'], marine3['armor'])
7 1

 

이런 방식으로는 이미 만든 마린은 업그레이드가 안되는군요. 방법이 없지는 않습니다. 지금 만들어져 있는 변수를 모두 검사해서 마린을 찾아 attackarmor를 올려주면 됩니다.

 

k, v = None, None
for k, v in globals().items():
    if isinstance(v, dict) and 'unit' in v and v['unit'] == 'marine':
        v['attack'] = attack
        v['armor'] = armor

 

확인해보겠습니다.

 

>>> print(marine1['attack'], marine1['armor'])
7 1
>>> print(marine2['attack'], marine2['armor'])
7 1
>>> print(marine3['attack'], marine3['armor'])
7 1

 

되긴 되는데 굉장히 마음에 들지 않네요. globals()에 변수가 얼마나 있을지도 모르고, 경우에 따라 마린이 globals()가 아닌 다른 곳에 있을 수도 있습니다. 이제 클래스를 적용해보죠. 얼마나 코드가 깔끔해지는지 한번 보세요.

 

class Marine:
    unit = 'marine'
    attack = 6
    armor = 0

    def __init__(self):
        self.hp = 40

    def shoot(self, other):
        other.hp -= self.attack - other.armor

class Barracks:
    def train_marine(self):
        return Marine()

class Engineering_bay:
    def upgrade(self, to_upgrade):
        if to_upgrade == 'attack':
            Marine.attack += 1
        elif to_upgrade == 'armor':
            Marine.armor += 1

 

Marine 클래스는 마린의 속성과 동작을 모두 내포하고 있습니다. 함수와 변수를 따로 관리할 필요가 없습니다. Marine 클래스 정의 맨 앞부분에 있는 unit, attack, armor는 클래스 변수라고 부릅니다. 클래스 변수는 Marine 클래스의 인스턴스들이 공유하는 값들입니다. 모든 마린은 유닛 이름, 공격력, 방어력이 같아야 하므로 이와 같이 클래스 변수로 만들었습니다. 반대로 hp는 마린마다 다를 수 있습니다. 따라서 인스턴스 변수로 만들었습니다. Barracks 클래스는 Marine 인스턴스를 만들어서 쭉쭉 뽑아냅니다. 공장 같은 개념이죠.

 

Engineering_bay 클래스에서는 마린의 업그레이드를 담당합니다. 여기서 중요한 것은, 인스턴스의 attack, armor를 바꾸는 것이 아니라 클래스의 attack, armor를 바꾼다는 점입니다. 이렇게 해야 Marine의 모든 인스턴스를 일괄적으로 변경할 수 있습니다. 이 내용은 점투파에도 자세히 설명되어 있습니다.

 

 

마린 유닛을 딕셔너리로 구현했을 때보다 사용법도 간단합니다. 마린 두 마리를 만들어서 1번에게 2번을 쏘라고 시켜보겠습니다.

 

>>> b = Barracks()
>>> marine1 = b.train_marine()
>>> marine2 = b.train_marine()
>>> marine1.shoot(marine2)
>>> print(marine1.hp, marine2.hp)
40 34

 

이번엔 공업을 해보겠습니다. 공업 전과 후에 만든 마린이 모두 업그레이드 되어야 합니다.

 

>>> E = Engineering_bay()
>>> E.upgrade('attack')
>>> marine3 = b.train_marine()
>>> print(marine1.attack, marine2.attack, marine3.attack)
7 7 7

 

잘 되는군요. 실제로 2번 마린에게 1번을 쏴보라고 할까요?

 

>>> marine2.shoot(marine1)
>>> print(marine1.hp, marine2.hp)
33 34

 

예상한 결과가 나왔습니다. 이번엔 방업을 해보죠.

 

>>> E.upgrade('armor')
>>> print(marine1.armor, marine2.armor, marine3.armor)
1 1 1

 

모든 마린이 일괄적으로 방업 되었습니다. 공방 모두 1업 되었으니 대미지는 처음과 똑같이 6이겠군요.

 

>>> marine1.shoot(marine3)
>>> print(marine3.hp)
34

 

이렇게 클래스를 이용하면 속성(체력, 공격력, 방어력)과 동작(쏘기)을 하나로 묶어서 관리하기에 매우 편합니다. 또한 클래스의 인스턴스는 정수, 문자열, 리스트, 딕셔너리 등과 동등하게 변수로서의 지위를 갖습니다. 사실 이미 . 위의 예제에서 보았듯이 모든 인스턴스의 속성을 일괄적으로 변경하는 것도 클래스에서는 쉽게 할 수 있습니다.

 

 

게으른 파이썬

댓글