- 내일배움캠프 12일차 목차
[오늘 한일]
- 파이썬 보충강의 수강
- 팀과제 진행. 팀과제 완성 후 제출.
- 팀과제 해설 수강.
오늘은 배운점이 정말 많다.
그중 팀과제를 마무리하며 배운점만 오늘 적겠다.
해설이나 강의에서 들은 내용은 주말동안 한번 더 짚어본 다음,
주차 배운점에 남기겠다.
[오늘 배운점]
팀원분들은 주요 기능을 하나씩 맡으셨고,
나는 팀장이여서 그런지 마치 팀의 허브역할을 했다.
각자 작업하신 기능이 전체 구조에 어떻게 조합될지,
어떤 기능이 더 필요할지, 어떤 동작은 어디에서 처리할지 등을 계속 조율했다.
그러다가 코드적으로 막히시면 같이 고민해드리기도 했다.
어제 다같이 노력해서 거의 다 진행이 된 덕분에,
오늘 아침엔 전체적으로 돌아볼 여유가 됬다.
<직접 만들어본 인풋체크 함수>
나는 팀과제 요구사항 중 중복된 코드 사용을 최소화하라는 사항을 생각해봤다.
그리고 우리 팀 코드를 쭉보니 꽤나 자주 반복되서 면적을 차지하고 있는 기능이 있었다.
바로 인풋을 받을때 인풋값이 정확한지 체크하는 부분이였다.
안그래도 이기능에 오작동이 하나 있어 그걸 고치면서, 동시에 함수로 만들어 봤다.
기존 코드는 이렇다.
while len(player_character_list) < 4:
(중략)
select = input("1)냥검사 2)냥법사 3)냥궁수 4)냥힐러 : ")
if select.isdigit() == False:
print("정수를 입력해주세요.")
elif bool(re.search("[1-4]", select)) == False:
print("잘못 입력했습니다. 다시 시도하세요.")
else:
player_character_list.append(characters[select])
if len(player_character_list) == 3:
break
게임 진행상 가장 첫 인풋이 나오는, 주인공 선택 부분이다.
첫 if와 elif 부분이 인풋 체크 동작부분이다.
우리가 기대하는 1부터 4의 숫자가 입력되지 않으면 문구를 표시하고,
자연스레 반복문이 다시 반복되는 구조다.
발견한 오동작은 이렇다.
문자나 1~4 외 숫자는 잘 검출되는데 11, 123 등 숫자가 통과되었다!!
통과되면 당연하게도 아래에서 인덱스 범위에러가 나오면서 게임이 멈춘다.
(예측 밖의 입력으로 인한, 이런 멈춤을 막는게 인풋 체크의 사실상 역할인데 말이다.)
나는 re.search에서 통과시키고 있다고 생각했고,
re.search기능을 자세히 살펴봤다.
re모듈 코드파일도 열어봤지만 이해가 어려웠고 인터넷 자료로 이해할 수 있었다.
re.search는 문자열 앞부터 끝까지 전체에서 1~4중 하나라도 포함되어 있는지를 검사한다.
그러니 11이 통과 된것이다.
re모듈에서 대체할만한 기능이 있나 살펴봤는데 match와 fullmatch가 눈에 띄었다.
match는 문자열 첫부분에 1~4가 있는지 검사한다.
fullmatch는 문자열 전체가 1~4인지 검사한다.
즉, match는 문자열 앞부분이 맞아야하고, fullmatch는 문자열 전체가 일치해야한다.
우리에 경우는 fullmatch가 가장 알맞게 보였다.
elif bool(re.fullmatch("[1-4]", select)) == False:
print("잘못 입력했습니다. 다시 시도하세요.")
time.sleep(1)
이렇게 바꿔주니 제대로 11까지 잡아내며 잘 동작하였다.
※일지를 작성하면서 혹시 search를 쓰면서 정규표현식을 알맞게 쓴다면 방법이 있지않을까 싶었다.
하지만 search로는 어렵다. 정규표현식을 "[1-4]$"로 작성하면 한자리수만 검사하긴 한다.
그런데 11도 한자리수는 1이여서 역시 통과된다. 199는 잡아낸다.
즉, search로는 입력문자열이 한자리수 인지를 검사할수 없다.
그럼 이제 이걸 함수로 만든 부분이다.
def input_check(x, y, text):
pattern = "[{}-{}]".format(x, y)
if text.isdigit() is False:
print("정수를 입력해주세요.")
return False
elif bool(re.fullmatch(pattern, text)) is False:
print("잘못 입력했습니다. 다시 시도하세요.")
return False
else:
return True
함수안에 조건 문을 쏙 넣었다.
프린트문도 떼어다가 집어넣었으니 본문에서 두줄 더 줄인것이다.
우리팀의 모든 인풋의 선택지는 1~4까지가 아니다.
1~3도 있고 1~2도 있기 때문에 검사 값이 가변적인 함수를 만들어야 했다.
그래서 x, y라는 인수로 시작 숫자, 끝 숫자를 받아온다면 어떨까.
그런데 속편하게 fullmatch("[x-y]")안에 이렇게 변수가 들어가진 않았다.
따로 정규표현식 문구를 만들어줄 방법이 필요했다.
이런 방법은 몰라서 고민했는데, 인터넷에서 금방 방법을 찾을 수 있었다.
위처럼 "[{}-{}]".format(x,y)로 포맷이라는 기능으로 정규표현식을 작성할수 있다는 것이다.
그래서 이렇게 작성한 정규표현식을 pattern이라는 변수에 담고,
변수를 fullmatch안에 넣어줬다.
그럼 함수를 이용해서 본문을 어떻게 단축시켰는지는 이렇다.
while len(player_character_list) < 4:
select = input("1)냥검사 2)냥법사 3)냥궁수 4)냥힐러 : ")
if input_check(1, len(characters), select) == False:
continue
else:
player_character_list.append(characters[select])
if len(player_character_list) == 3:
break
4줄이 1줄이 되었다. (컨티뉴는 없었으니까 뺀다고 친다면.)
※이 반복문은 continue가 없어도 반복문 구조상 문제가 없지만,
다른 반복문들에는 필요해서 일괄적으로 continue를 넣었다.
게다가 인풋체크 함수의 가변성 덕분에 입력 선택지 수의 가변성도 생겼다.
위처럼 캐릭터가 한명 선택될때마다, len(characters)가 줄어드니 선택지가 줄어들고,
이는 실제 입력가능한 숫자가 줄어들게 되어, 2,3차 반복에서 4나 3이 입력될 경우도 없앤 것이다.
※원래 코드에서 이렇게 하려면 마찬가지로 정규표현식이라서 참조가 쉽지 않다.
비슷한 인풋 체크 2함수도 있다.
def input_check2(text):
if bool(re.fullmatch("[yn]", text)) is False:
print("잘못 입력했습니다. 다시 시도하세요.")
return False
else:
return True
우리 팀의 선택지 중에는 y혹은 n을 선택하는 선택지도 있다.
그런 경우를 처리하려고 만든게 인풋 체크2 이다.
처음에 y,n 유형도 필요하다는걸 알았을때 팀원분께서는
위에서 작성한 인풋 체크함수에 기능을 추가하면 어떻겠냐고 아이디어를 주셨다.
그런데 고민 끝에 함수를 하나 더 만든 이유는 이렇다.
- y,n 유형은 문자를 입력받는다. 그래서 isdigit인 정수 검출부분이 필요없다.
- 스트링 타입으로 구분해서 크게 두개 분기 동작으로 만들면 어떻겠냐고도 아이디어를 주셨는데,
기존 인풋체크 함수의 숫자들도 스트링 타입으로 넘어온다.
- 당장은, y,n유형은 인수가 필요없다.
나는 본문에서 타입 처리를 바꾸지 않으면서 알맞은 함수를 만드는게 좋은 방법이라고 생각했고,
인풋체크2 함수를 만들어 y,n유형 선택지 체크도 잘 동작시킬수 있었다.
buy_again = input('구매를 계속 진행하시겠습니까?\n y/n로 선택해주세요:')
if input_check2(buy_again) == False:
continue
실제 본문에서 y/n함수가 사용된 부분이다.
결론, re모듈의 세가지 기능의 차이를 알고 적절한 기능으로 대체할수 있었다.
조건문 부분을 함수로 빼내며 간략화 할수 있었다.
정규표현식을 가공하는 법을 알수 있었다.
<파일 나누기의 난제>
나는 팀과제 요구사항중 기능별로 파일을 나눠야 한다는 사항을 보고,
팀원분들께 파일을 나누자고 제안했다.
나는 회의 전에 미리 구상안을 생각했는데 굵직한 기능별로 나누는 것이다.
- 메인
- 오브젝트
- 전투
- 아이템
- 샵
- 유틸리티
내 제안이 수용됬고 우리는 이렇게 파일을 나누기로 했다.
파일을 나누려고 보니 문제점 들이 있었다.
그래서 팀원 한분이, 복사본으로 문제점들을 해결해가며 파일을 나눠 보신 다음에
실제 팀파일도 나눠주셨다.
나도 팀원분이 작업하시는 동안 내 로컬(즉 내 복사본)으로 따로 나누는 작업을 해봤다.
어려운 문제점이 있으면 함께 고민해보기 위해서 였다.
내가 스스로 나눠보면서, 그리고 팀원분이 수행해주신 방법을 보면서 배운 점들이 많다.
우선 메인 파일에서 자식 파일을 불러오는 방법은 간단하다.
from utility import *
이렇게 메인 파일 맨위에 적어주면된다.
그럼 내가 만든 utility.py의 모든 걸 잘 불러온다.
함수와 클래스, 그리고 인스턴스까지 불러와진다.
1. 여기서 주의할 첫번째는 자식 파일에서는 메인 파일을 import하면 안된다.
그러면 왜그러는지 몰라도, 자식 파일의 함수, 클래스등이 메인 파일에서 실행할 때
유효한 인자값이 없다는 에러가 나온다.
2. 이제 메인 파일의 전역 변수를 자식 파일에서 쓸 수 없다.
필요하다면 지역 파일에 있는 함수를 부를때 인수로 실어 보냈다가,
가공이 끝난 데이터를 리턴으로 반환 받아 대입해줘야 한다.
이렇게 작성되었던 모든 자식 파일의 전역 변수들을 바꾸는 작업이 있었다.
전역 변수명 대신 인수의 명칭으로 바꾸고,
처리가 다 끝나서 돌아갈 때는 리턴으로 값을 돌려주는 부분도 다 추가하셨다.
※ 추후에 최종 오작동 검사를 할때 이부분에서 에러를 몇번 찾기도 했다.
elif status == 'prebattle':
status, player_money = prebattle(
player_character_list, player_money, character_skills)
예를 들면 이런 부분인데, 함수를 실행하며 주는 인수들과 리턴으로 돌아오는 인수가 차이가 날수도 있다.
필요한 값들만 주고 필요한 값들만 받는 것이다.
거꾸로, 전투에서 가공된 캐릭터 리스트를 반환했다가,
전투원이 죽은채 마을로 돌아올때 에러가 나서 그 부분을 알맞게 바꾸기도 했다.
한편, 저런 식으로 변수, 변수로 한번에 여러 값을 변수에 대입하는,
방법이 있다는 것도 새로 배운점중 하나이다.
3. 자식 파일에서 선언된 인스턴스는 메인 파일이나,
필요하다면 자식 파일간에서도 마음껏 쓸수 있다.
자식 파일도 해당 자식 파일을 임포트 하면 된다.
예를 들면 shop.py에서는 items.py를 임포트해서,
items.py에 있는 인스턴스를 그대로 불러와서 사용했다.
4. 메인 파일에서 자식 파일로 빼면서 필요없어진 모듈은 임포트를 제외해도 된다.
utility.py를 만들어서 re, os, platform 모듈 옮겨가는 등,
자식 파일을 만들면서 메인에서는 해당 모듈이 사용되지 않는다면 import를 지워도 된다.
결론, 중간에 나누자니 까다로웠다.
하지만 덕분에 파일을 나눌때 주의점을 잘 알게 된것 같다.
<화면 클리어도 함수로 빼기>
os_check = platform.system()
clear = ''
if os_check == 'Windows':
clear = 'cls'
else:
clear = 'clear'
기존에 작성해주신 화면 클리어 코드부분이다.
os를 체크한 다음에 해당 os에 알맞은 문구를 clear변수에 넣는다.
os.system(clear)
그럼 본문에서는 이렇게 화면 클리어를 적합하게 할수있다.
나는 위쪽 덩어리를 자식 파일로 빼자는 생각에 저걸 함수로 바꿔봤다.
내가 작성한 함수는 이렇다.
def screen_clear():
os_check = platform.system()
if os_check == 'Windows':
os.system("cls")
else:
os.system("clear")
screen_clear()
본문에서는 이렇게 함수를 실행했고 잘 실행되어 내 방법으로 대체했다.
그리고 utility.py로 함수를 옮겨서 본문에서 덜어냈다.
그런데 지금 생각해보니 참 부족한 부분이 보인다.
기존 방법으로는 본문에서 platfrom.system 동작을 한번만 하고,
전역변수 선언과 조건문 동작도 처음 한번만 동작하게 된다.
그런데 내가 만든 함수는 함수가 동작될때마다,
platfrom.system동작과, 조건문 동작이 된다.
본문을 간결하게 한다고, 오히려 동작중복을 만들어 낸것이다.
지금 생각해보면 utility.py에 이렇게 작성했으면 가장 좋았을뻔 했다.
os_check = platform.system()
if os_check == 'Windows':
clear = "cls"
else:
clear = "clear"
def screen_clear():
os.system(clear)
이렇게 하면 os체크와 조건문은 한번만 동작하고,
함수의 동작마다는 실제 필요한 화면 클리어 동작만 하게 된다.
결론, 잘 만들었다고 생각해도 다시 돌아보자.
동작을 상상하며 되짚어보자.