23.03.31 TIL

최창수·2023년 4월 1일
0

오늘 한 것

  • 알고리즘 0~1단계 문제 7개 풀이
  • 팀 과제 마무리
  • 과제 해설 및 코드 리뷰 받기

팀 과제 마무리

팀 과제를 마무리 하였다. 몇가지 기능을 추가하고 디버깅을 하였다.

기능 구현: 속도에 따른 전투순서

회피 혹은 공격 순서를 위해 Character에 agility속성을 만들어 두었는데, 미처 구현하지 못해 딱히 쓰는 데가 없었다. 사용처를 만들어 보기 위해 플레이어 팀과 적 팀의 속도 평균을 구해 선공을 정하게 만드는 기능을 구현하기로 하였다.

문제: 기존 코드

    def battle_scence_turn(self):
        while (self.hero_list and self.monster_list):
            for character in self.hero_list:
                behavior_dict = {'A': character.attack,
                                 'B': character.hero_magic_attack}
                player_input = foo1(behavior_dict)  # 일반/마법 공격타입 선택 문장 출력
                select_enemy = foo2(self.monster_list)  # 공격할 적 선택 문장 출력
                behavior_dict[player_input](select_enemy)
                # 마법 혹은 공격 후에
                self.battle_scene_check_del_entity(select_enemy)

            for monster in self.monster_list.keys():
                target = random.choice(self.hero_list)
                monster.attack()
                self.battle_scene_check_del_entity(target)

기존 코드는 몬스터와 플레이어가 남아있는 동안 while을 돌리고 그 안에서 남은 플레이어 캐릭터와 몬스터 캐릭터를 for문을 통해 각각 공격을 진행한다.

해결: 함수로 분리

def battle_scene_turn(self):
        ...
        	#플레이어팀과 몬스터 팀의 민첩 평균 비교
            for entity in self.hero_list.values():
                i += 1
                heroes_agility += entity.agility
            heroes_agility = heroes_agility//i

            monsters_agility = 0
            i = 0
            for entity in self.monster_list.values():
                i += 1
                monsters_agility += entity.agility
            monsters_agility = monsters_agility//i
            # 비교의 결과에 따라 순서대로 두 함수들을 호출
            heroes_agility = heroes_agility+random.randint(-2, 2)
            monsters_agility = monsters_agility+random.randint(-1, 1)
            print(
                f'플레이어팀의 속도: {heroes_agility}, 몬스터팀의 속도: {monsters_agility}')
            if heroes_agility+random.randint(-2, 2) < monsters_agility:
                print('몬스터가 먼저 공격합니다.')
                self.monster_scene_turn()
                self.hero_scene_turn()
            else:
                print('플레이어가 먼저 공격합니다.')
                self.hero_scene_turn()
                self.monster_scene_turn()
            ...
            
    # 플레이어들의 공격을 진행하는 부분입니다
    def hero_scene_turn(self):
        for key_ in list(self.hero_list.keys()):  
            if not self.monster_list:  # 몬스터 없으면 바로 종료
                break
           ...
       

    # 적 공격 순서
    def monster_scene_turn(self):
        for key_ in self.monster_list.keys():  # 몬스터들의 공격이 진행되는 부분입니다
            #  플레이어가 모두 죽으면 바로 끝냄
            if not self.hero_list:
                break
			...

함수로 분리하여도 별다른 인자를 받을 필요가 없는 구조로 작성되어 있어서 쉽게 함수로 분리하여 사용할 수 있었다.

문자열 이쁘게 좌우 정렬해서 출력하기

터미널에서 테이블을 출력하고 싶을 때, 문자열의 길이에 따라 테이블이 예쁘게 나오지 않고 깨질 수 있다. 이를 해결하고 싶다.

시도 1: 문자열 길이 제한

문자열 길이를 8 이하로 제한하고 탭문자(\t)을 이용해보려 했으나 잘 되지 않았다. 결국 딱 8 글자일 때는 정렬이 깨진다. 그러나 7 글자까지 제한하고 싶지는 않았다.

시도 2: 길이에 따른 탭문자 횟수변경

TAB='\t'
NOTAB=''
f"\t\t{hero_name}\t\t{TAB if len(hero_name)<8 else NOTAB}{monster_list.get(str(i)).monster_class_name}"

위와같이 하면 탭 문자의 횟수를 길이에 따라 유동적으로 변경시켜서 테이블을 깨지지 않게 할 수 있었다. 그러나 너무 길어져서 더 나은 방법이 있을 것이라 생각하고 찾아보았다.

해결: formatting

f-string은 변수를 출력할 때 최대 몇 칸을 차지하게 출력하고 그 몇 칸 안에서 오른쪽/왼쪽/중앙 정렬중 어떤걸 선택할지 정할 수 있다.

s1 = '1234'
result1 = f'|{s1:<10}|'
print(result1)
# |1234      | 왼쪽
 

result2 = f'|{s1:^10}|'
print(result2)
# |   1234   | 중앙

result3 = f'|{s1:>10}|'
print(result3)
# |      1234| 오른쪽

공백 채우기, 소수점 표시도 지원한다.

 f'{"12":=^10}'  # 가운데 정렬하고 '=' 문자로 공백 채우기
#'====12===='
y = 3.42134234
f'{y:10.4f}'  # 소수점 4자리까지 표현하고 총 자리수를 10으로 맞춤
'    3.4213'

과제 해설 및 코드 리뷰

해설과 코드리뷰 과정에서 나온 코드 지적사항을 정리하였다.

1. import * 지양

from characters import *

해당 모듈의 모든 변수와 함수를 import 하기 때문에, 의도하지 않은 충돌이 발생할 수 있다. 꼭 필요한 요소만 import하도록 하자.

from characters import Character, Hero, Monster, BossMonster, hero_status_wikipedia

2. 더 간편한 행동 선택 구현

클래스의 메서드들 중 하나를 선택해서 실행하게 할때, 클래스 자신이 선택대상이 되는 함수들의 이름을 저장하고 있는 딕셔너리 하나를 가지고 있게 하면 더 간단하게 코드를 짤 수 있다.

class Character:
    """
    모든 캐릭터의 모체가 되는 클래스
    """

    def __init__(self, name, hp, power, magic_power, agility):
        self.behaviors={
        	'1':{'vervose':'일반공격', 'action':self.attack}
        	}
	...
    
class Hero:
	def __init__(self, name, hp, power, magic_power, agility,mana):
    super().__init__(name, hp, power, magic_power, agility)
    self.mana=mana
        self.behaviors={
        	'2':{'vervose':'특수공격', 'action':self.magic_attack}
        	}
'''
{
	'1':{'vervose':'일반공격', 'action':self.attack},
	'2':{'vervose':'특수공격', 'action':self.magic_attack}
}
'''
   ...
            

3. kwargs 활용하기(packing,unpacking)

함수(메서드)가 많은 수의 인자를 받을 때에는 kwargs와 언패킹을 이용해 한번에 딕셔너리로 주고받는 것이 편리하다. 또한 휴먼 에러를 줄일 수 있다.

더 안전한 코드는 사람 코더의 실수에 영향이 적은 코드이다.

class Character:
    """
    모든 캐릭터의 모체가 되는 클래스
    """

    def __init__(self, **kwargs):  # packing
    	self.name=kwargs.get('name')
        self.max_hp=kwargs.get('hp')
        self.hp=self.max_hp
        ...
        
    
class Hero:
	def __init__(self, **kwargs):  # packing
    super().__init__(**kwargs)  # unpacking
    self.max_mama=kwargs.get('mana')
	self.mana=self.max_mana
    
    ...
hero_stat={'name':'asdf','hp':10,...}
palyer_character=Hero(**hero_stat)  # unpacking
    

4. doc string과 type hint

함수나 클래스 아래에 복수줄 주석(''')을 달아주면 doc string이 된다. 이걸 적어놓으면 해당 함수에 대한 설명을 함수에 마우스를 올려 읽을 수 있다.
매개변수 뒤에 :type을 적으면 어떤 타입의 인자를 받기위한 함수인지알 수 있다. 괄호 뒤에 -> type을 적으면 어떤 반환형을 가지는지 알 수 있다.

def func(num_a: int, num_b: int) -> int:
    '''
    a+b 반환하는 함수
    '''
    return num_a + num_b


print(func(10, 11))

5. return None

파이선에서 모든 함수는 반환형을 가지며 실제로 반환한다.
따라서 return None 은 그냥 return으로 생략가능하다.

6. mutable, deep copy

파이선의 자료형은 mutable, immutable로 나뉜다. 리스트와 딕셔너리는 mutable에 속하며, 이들은 다음과 같이 다른 변수에 복사하여 저장하거나 함수에 인자로 넣을 때나 실제로는 같은 메모리 공간을 참조하므로 한쪽에 변경사항이 있으면 둘다 변한다. (shallow copy)

list_1=[1,2,3]
list_2=list_1
list_2[0]=0
print(list_1)  # [0,2,3]

dict_1={1:1,2:2}
dict_2=dict_1
dict_2[1]=0
print(dict_1)  # {1:0,2:2}

따라서 실제로 값을 모두 복사하기 위해서는 deep copy를 해야하며 다음과 같이 할 수 있다.

a=[1,2,3]
b=a[::]  # deep copy
c={1:1,2:2}
d=c.copy() # deep copy

이를 이용하면 다음과 같이 오류가 있는 코드를 간단하게 오류없게 바꿀수 있다.

a = {'a': [[1, 2,3], [2, 3,4], [3, 4,5]]}
for i, j in a.items():
    for x in j:
        j.remove(x)
print(a)
# {'a':[[2],[3],[4]]}
a = {'a': [[1, 2,3], [2, 3,4], [3, 4,5]]}
for i, j in a.items():
    for x in j[::]:  # deepcopy한 다른 객체
        j.remove(x)
print(a)
# {'a':[[],[],[]]}

만약 뮤테이블한 객체 안의 뮤테이블한 요소가 또 있는 경우 그것까지 복사해야한다면 copy모듈을 임포트해 deepcopy()함수를 써야한다.

import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)

setattr, getattr

setattr()는 객체에 문자열에 주어진 이름의 데이터(변수)나 메서드가 있으면 그 값을 변경하고 없으면 새로 추가해준다.

class M:
    def __init__(self) -> None:
        self.a = 10

    def foo(self):
        print('f00')


def foo_2():
    print('foo2')


m = M()
setattr(m, 'foo_2', foo_2)
setattr(m, 'a', 20)
m.foo_2() # foo2
print(m.a) # 20

getattr()는 객체가 가지고 있는 이터(변수)나 메서드를 문자열로 접근하여 가져온다.

class M:
    def __init__(self) -> None:
        self.a = 10

    def foo(self):
        print('f00')


m = M()
getattr(m, 'foo')()
print(getattr(m, 'a',10)) # 3번인자: 기본값

문자열로 접근하고, 객체에 없으면 None을 반환한다는 특성이 있어 특정상황에서 유용하게 사용할 수 있다. 예를들어 다음과 같이 동적으로 접근할 수 있다.

attr_list=['foo','bar']
for attr in attr_list:
	getattr(x, attr)

유무 확인만 하는 hasattr(), 삭제하는 delattr() 도 있다.

profile
Hallow Word!

0개의 댓글