-Summary-
file, 회문 판별, N-gram, 함수(def), 재귀 호출, lambda 표현식, 클로저, class, 상속, 두 점 사이 거리 구하기
01-29 (Unit 27.1~ Unit 29.8)
-file-
-open
파이썬에서는 파일객체를 가져오는 방법으로 open 함수가 있다.
<변수명> = open(<파일 경로 및 이름>, <파일 모드>)로 사용하면 <변수명>에 file 객체가 담기게 된다.
-파일 모드 종류(출처 : withcoding.com/86)
r - 읽기모드
w - 쓰기모드, 파일이 있으면 모든 내용을 삭제
x - 쓰기모드, 파일이 있으면 오류 발생
a - 쓰기모드, 파일이 있으면 뒤에 내용을 추가
+ - 읽기쓰기모드
t - 텍스트 모드, 텍스트 문자 기록에 사용
b - 바이너리 모드, 바이트단위 데이터 기록에 사용
파일모드는 정리해 둔다면 나중에 도움이 될 것 같아 구글링을 통해 알아왔다.
사진은 코딩도장 Unit 27.3에 나온 파일 모드 조합 사진이다.
-write
write는 open 함수로 file 객체가 담긴 변수에 사용한다. write를 사용하면 객체에 원하는 문자열을 저장할 수 있다. 단 반드시 쓰기모드로 가져와야지 오류없이 된다.
f = open('1.txt','w')
f.write('Hello World!')
f.close()
실습 캡처가 번거로워서 코드로 대체했다. 위 코드가 1.txt를 읽어와서 txt 파일 안에 Hello World!를 저장하는 코드다. 3행의 close는 안써줘도 오류는 없지만 파일 객체를 더이상 안 쓸때 close를 해주면 최적화가 좋아진다.
-read
read는 write와 반대로 파일 객체의 문자열을 읽어서 반환해준다. 예를 들어 파일에 'Hello World!'가 적혀있다면 그 문자열을 읽은 후 'Hello World!' 문자열을 반환해 준다. 이 read 함수 역시 모드는 'r'로 지정해줘야한다.
f = open('1.txt','r')
print(f.read())
f.close()
위에서 write를 이용해 1.txt 파일에 'Hello World!' 문자열을 저장했었는데, 위 코드는 write로 만들어진 1.txt를 읽기 모드로 불러와서 read를 통해 'Hello World!'를 읽어 온다.
-with as
파일 객체를 다 쓰고 close를 적는 일이 번거롭게 느껴질 수 있는데, 이는 with, as를 이용한다면 파일을 다 썻을 때 자동으로 close를 해주도록 할 수 있다.
사용은 with open(<파일 경로 및 이름>, <파일 모드>) as <변수명>: 으로 해주면 된다.
with open('1.txt','r') as f:
print(f.read())
위는 read 실습 코드를 with, as를 이용해 2줄로 줄인 모습이다. with, as 파일 객체를 불러온 후 파일 객체를 사용하는 코드는 들여쓰기를 해주는데, 더이상 들여쓰기된 코드가 없다면 자동으로 해당 객체를 close 해준다.
-파일에 여러줄 쓰기
반복문을 이용해서 여러줄을 파일에 쓸 수 있다. 물론 반복문을 쓰지 않아도 read()를 여러번 사용해도 되긴한다.
with open('1.txt','w') as f:
for i in range(10):
f.write("{0} \n".format(i))
위는 range(10)만큼 for문을 돌려서 그 값들을 1.txt에 입력하는 코드다. 여기서 주의할점은 print에 쓰는 것처럼 f.write에
i,"\n"을 적으면 인자 오류가 뜨므로 format을 써줘야한다. \n은 행 바꿈을 위해서 사용한다.
temp = ['d\n', 'y\n', 'p\n', 'a\n', 'r\n']
with open('1.txt','w') as f:
f.writelines(temp)
writelines는 write와 달리 리스트에 들어있는 문자열을 파일에 저장해준다. 다만 문자열에 \n을 안써준다면 1줄로 쭉 저장하니 행 변환을 원할 경우 \n을 써줘야한다.
-파일 한줄씩 읽기
readlines 함수를 이용해 파일의 문자열들을 한 줄씩 리스트 형태로 가져올 수 있다. 참고로 readlines는 파일에 저장된 모든 문자열을 읽어온다.
a = []
with open('1.txt', 'r') as f:
a = f.readlines()
위 코드는 readlines로 파일에서 읽어온 문자열 리스트를 a에 저장해준다.
readline은 readlines와 달리 파일에 저장된 문자열을 한 줄씩 차례대로 읽는다. 따라서 readlines처럼 사용하기 위해서는 반복문으로 반복해서 readline을 사용해야한다.
temp = " "
with open('1.txt', 'r') as f:
while temp != "":
temp = f.readline()
print(temp)
파일에 더이상 읽을 문자열이 없으면 공백을 가져오게 되는데, 이를 이용해 temp != ""로 식을 세워서 while로 반복해준다. 또한 temp의 초기값은 " "으로 설정해놔 while이 시작하자마자 탈출하지 않도록 했다. temp에 None을 넣어도 된다.
for문으로도 파일에 저장된 문자열을 줄 단위로 읽어 올 수 있다.
with open('1.txt', 'r') as f:
for temp in f:
print(temp)
단순히 in <파일 객체 변수명>을 써주더라도 자동으로 문자열을 한줄로 읽어오는 것을 알 수 있다.
-pickle-
pickle은 파이썬 객체를 파일에 저장해주는 모듈이다. 객체를 파일에 저장하는 과정을 피클링이라 부르고, 파일에 저장한 객체를 읽어오는 과정을 언피클링이라한다.
-dump
파이썬 객체를 파일에 저장할때는 pickle 모듈의 dump를 사용하면 된다.
사용은 dump(<저장할 객체>,<파일 객체>)로 하면 파일에 객체가 저장된다.
import pickle as pic
a = 'abcd'
b = '1234'
with open('1.txt', 'wb') as f:
pic.dump(a, f)
pic.dump(b, f)
dump로 객체를 저장할 때는 바이너리 쓰기 모드인 'wb'모드를 사용해야한다. 또한 파일 확장자는 예시로 txt로 했지만 다른 확장자로해도 상관없다.
객체를 저장한 1.txt를 열어보면 저렇게 알 수없는 문자로 저장된 것을 볼 수 있다.
-load
load는 dump로 저장된 객체를 읽어와준다. dump와 마찬가지로 파일 모드는 'r'이 아닌 'rb'를 사용해야지 오류가 안난다. 사용 방법은 load(<파일객체>)로 해주면 된다. 또한 읽는 방식은 readline과 비슷하게 한줄씩 차례대로 읽어 오는 것 같다.
dump로 저장한 값을 load를 사용하니 제대로 불러와진 것을 볼 수 있다.
문자열을 저장할때는 쓰기모드로 열어야하므로 'w' 옵션을 줘야한다.
답 : d
문자열을 한줄씩 리스트 형태로 가져오는 메서드는 readlines다.
답 : c
pickle 모듈로 읽거나 저장할 때는 바이너리 모드가 반드시 필요하다. 객체를 읽어오는 경우이므로 읽기 모드인 'r'+바이너리 모드'b'를 추가해 'rb' 모드로 파일을 열면 된다.
답 : b
줄단위로 저장된 words.txt에서 10자 이하인 단어의 개수를 출력되게하는 것이 목적이다.
with open('words.txt','r') as f:
count = 0
for i in f:
if len(i.strip('\n')) <= 10:
count += 1
print(count)
1행에서 words.txt를 읽기모드로 불러오고, 3행에서 for i in f로 문자열을 한줄씩 i에 넣는다. 처음 했을 때는 len(i) <=10으로 썻는데, 잘되지않아 확인해 보니 개행 문자인 \n도 문자수에 포함되서 제대로 결과가 나오지 않는 거였다. 그래서 strip('\n')을 써주니 개행문자가 제거되어 제대로된 결과가 나왔다.
words.txt에서 c가 포함된 단어를 각줄에 , . 없이 출력하는 것이 목적이다.
with open('words.txt','r') as f:
a = (f.readline()).split()
for i in a:
if i.find('c') != -1:
print(i.strip(',.'))
먼저 1행에서 words.txt를 읽기 모드로 불러오고 2행에서 a에 readline으로 불러온 문자열을 공백 기준으로 split해서 리스트 형태로 값을 넣어준다. 그 다음 for i in a로 단어 하나씩 가져오는데 find 함수가 지정 문자열이 존재하지 않다면 -1을 반환해주는 특성을 이용해, i.find('c') != -1을 넣어 줬다. 또한 ,. 없이 출력해야하므로 i.strip(',.')로 ,.을 제거해서 출력해줬다.
-N-gram-
-회문 판별
이번에 배울 것을 회문을 판별하는 방법, N-gram을 만드는 법을 배우는데, 회문을 판별하는 코드는 먼저 혼자 작성하고, 강의를 들으며 부족한 점을 보완하는 방식으로 정리하겠다.
먼저 회문이란 순서를 거꾸로 해도 똑바로 읽는 것과 같은 단어나 문자을 말한다. 한국어로는 '소주 만병만 주소' 등이 있으며 영어로는 SOS, rotator, nurses run 등이 있다.
string = input().replace(" ","")
palindrome = True
for i in range(len(string)):
if string[i] != string[(len(string)-1)-i]:
palindrome = False
break
print("회문입니다." if palindrome == True else '회문이 아닙니다.')
위 코드가 혼자 작성해본 코드다. 먼저 1행에서 input을 받는데 회문의 경우 공백은 포함하지 않으므로(예-nurses run) replace(" ", "")로 공백을 제거해줬다. 2행의 palindrome이 회문 여부를 판단하는 bool 변수인데, True일 경우 회문, False일 경우 회문이 아니다. 3행에서는 입력받은 문자열의 길이만큼 for 반복문을 돌려준다. 4행에서 string의 i번째, (len(string)-1)-i 번째의 문자를 서로 비교하고 만약 다르다면 palindrome을 False로 만들고 break로 반복문을 종료하는데, (len(string)-1)-i 이렇게 써준 이유는 먼저 문자열의 끝-i가 i번째와 문자가 같아야하는데 문자열의 끝 인덱스를 구하는 방법이 len(string)-1이라서 저렇게 써준것이다.
마지막행에서 if문을 통해 palindrome True,False 여부에 따라 회문인지 아닌지를 출력해준다.
word = input('단어를 입력하세요: ')
is_palindrome = True # 회문 판별값을 저장할 변수, 초깃값은 True
for i in range(len(word) // 2): # 0부터 문자열 길이의 절반만큼 반복
if word[i] != word[-1 - i]: # 왼쪽 문자와 오른쪽 문자를 비교하여 문자가 다르면
is_palindrome = False # 회문이 아님
break
print(is_palindrome) # 회문 판별값 출력
위 코드가 코딩도장의 첫번째 코드다.
내 코드와 똑같이 bool 변수를 하나 만들어 회문 여부를 판단해주고 for문을 돌리는데 이 for문은 len(word)//2 즉 문자열 길이의 절반 만큼 반복해준다. 그리고 if문으로 회문 판별을 해주는데, 내 코드와 달리 끝 인덱스 값에 해당하는 -1에 -i를 해줌으로써 좀더 간결하고 가독성 있게 문자를 비교해준다. 내 코드와 똑같이 만약 아니라면 False로 바꾸고, 반복문은 더 돌릴 필요없으므로 break을 해준다음 결과를 출력한다.
보면 회문의 예시에 nurses run이라는 문장이 있는데, 위 코드는 공백을 처리해주지 않으므로 nurses run은 회문이 아니라고 뜬다. 이 부분은 따로 고쳐야될 것 같다.
훨씬 더 간단한 방법이 있었다. 리스트로 형변환 한 후 [::-1]이나 reverse로 문장을 아에 뒤집은 후 검사하거나, 뒤집은 후 join 함수로 다시 문자열로 바꿔서 검사할 수도 있었다.
-N-gram
N-gram은 문자열을 N개씩 추출하는 방법이다. 예를 들어 dypark를 2-gram으로 추출한다 가정하면
dy
yp
pa
ar
rk
로 추출된다.
temp = 'dypark'
for i in range(len(temp) - 1):
print(temp[i], temp[i + 1], sep='')
2-gram을 구현해본 모습이다.
실행을하면
dy
yp
pa
ar
rk
위에 적은 것과 똑같이 출력된다.
원리는 간단한데, 길이만큼 반복하고, 출력을 인덱스 i, i+1하면 현재값과 현재 + 1값이 출력되서 문자가 2개씩 출력된다.
이를 응용하면 3-gram 역시 쉽게 할 수 있다. len(temp) - 2로 for문 식을 만들고, temp[i], temp[i+1], temp[i+2]를 출력해주면 3개씩 출력이 된다.
지금까지는 단어 단위로 N-gram을 사용했는데, 문장 단위로도 구현해 보겠다.
temp = 'I am dy park.'.split()
for i in range(len(temp) - 1):
print(temp[i], temp[i + 1])
문장은 그저 공백 단위로 자른 다음 i, i+1로 출력해줄 수 있다.
zip 함수로도 n-gram을 만들 수 있다.
temp = 'dypark'
print(list(zip(temp, temp[1:])))
위 코드처럼 zip의 첫 인자는 temp, 두번재 인자는 한칸씩 밀리도록 temp[1:]로 해준다면 2-gram이 구현된다.
문장 단위도 비슷한 원리이므로 생략하도록 하겠다.
위 코드의 경우 n이 늘어날 수록 temp, temp[1:],temp[2:] 등 일일이 써줘야하므로 번거롭다. 이를 해결하기 위해 리스트 표현식을 사용하면 좀더 편리하게 사용가능하다.
temp = 'dypark'
print(list(zip(*[temp[i:] for i in range(3)])))
위 코드처럼 리스트 표현식과 zip를 사용한다면 슬라이스를 해줄 필요없이 range의 값 범위만 바꿔준다면 n-gram 구현이 가능하다.
원리는 [temp[i:] for i in range(3)]이 부분에서 0~2의 값을 i가 하나씩 가져가는데 이걸 temp[i:]에 적용하면 temp[0],temp[1],temp[2]가 된다. 이상태에서 zip을 적용하면 n-gram이 구현되는데, 이때 중요하것은 리스트 표현식 앞에 *을 붙여야한다는 것이다. 이 *을 붙이지 않는다면 한글자식 잘려서 나오는게 아닌 단어 하나하나로 나오게 된다. 리스트 앞에 *을 붙이는 것을 리스트 언패킹이라 한다는데, 이 관련 내용은 뒤에서 다룬다.
입력된 숫자에 해당하는 N-gram을 튜플로 출력하는 것이 목적인데, 입력된 문자열의 단어 개수가 입력된 정수 미만이면 wrong을 출력하라 한다.
n = int(input())
text = input()
words = text.split()
if len(text) < n:
print('wrong')
else:
n_gram = list(zip(*[words[i:] for i in range(n)]))
for i in n_gram:
print(i)
입력된 정수 미만을 판별하는 방법은 len(text) < n, 즉 N-gram 보다 텍스트 길이가 더 작다면 wrong을 출력하면 된다. 리스트 표현식은 위에서 배운것 처럼 list(zip(*[words[i:] for i in range(n)])) zip을 이용해 사용하되 *을 붙여주는 것을 잊으면 안된다.
words.txt 파일을 읽고 회문인 단어만 출력하도록 만든느 것이 목적이다. open으로 읽기 모드를 줘서 파일을 열면 될 것 같고 회문은 reverse를 이용해서 확인하면 될 것 같다. 또한 \n이 붙어 있기 때문에 strip으로 \n을 제거해줘야겠다.
a = []
with open('words.txt','r') as f: a = f.readlines()
for i in a:
i = i.strip('\n')
if i == ''.join(reversed(i)):
print(i)
open으로 txt파일을 열고 readlines()로 문자열들을 한행씩 리스트형태로 a에 담은 후 for문을 돌린다. 이때 개행문자는 제외해 줘야하믈 strip함수를 써주고 비교부분은 ''.join(reversed(i))로 단어를 뒤집은 상태와 일반 상태를 비교해서 일치한다면 그 단어를 출력해준다.
-함수-
코드를 작성하다 보면 값만 바뀌면서 반복되는 코드들이 많이 보이는데 이런 코드들을 일종의 메크로 같이 쓸 수 있도록 하는 것이 함수다. 정확히는 함수는 특정 용도의 코드를 작성해두고 필요할 때 마다 불러올 수 있다. 예를 들어 sum, input 등도 내가 작성해둔건 아니지만 파이썬 자체에서 미리 작성해둔 함수를 호출하는 것이다.
함수 작성법은 간단하다.
def <함수이름> (<매개변수>):
<함수 코드>
위처럼 작성하면 되는데, 매개변수는 함수로 전달할 값이 있다면 적어주면 되고, 전달할 필요가 없다면 안적어줘도된다.
이 만들어진 함수는 <함수이름>(<매개변수>)로 호출할 수 있는데, 함수에 매개변수가 존재할 경우 ()안에 해당 값을 넣어줘야하고 없다면 ()상태로 호출하면 된다.
위 사진은 print("Hello World!")를 실행해주는 func1() 함수를 만들고 func1()으로 호출해준 모습이다. 결과창을 보면 정상적으로 Hello World!가 출력이 됨을 알 수 있다.
위 코드의 실행 순서는 다음과 같다.
1. 파이썬 스크립트 실행
2. 3행의 func1()으로 func1() 호출
3 . func1() 1행~2행 실행
4. func1() 함수 종료
5. 파이썬 스크립트 종료
위 사진 같은 경우는 함수를 맨 위에 만들고 호출을 아래서 해줬는데, 호출을 함수보다 위에서 하면 어떻게 될까?
위 결과화면처럼 func1이 정의되지 않았다는 오류문구가 뜬다. 이 이유는 파이썬은 1행부터 차례대로 실행되기 때문에 fun1()이 호출되는 시점에는 def func1이 아직 정의되지 않았기 때문이다. 따라서 함수 호출은 최소한 그 함수 아래에서 해야지 정상적으로 작동된다.
빈 함수를 만드는 법은 함수 코드에 pass를 써주면 된다.
-매개변수 활용
이번에는 매개변수를 활용해 보겠다.
함수 정의시 ()안에 원하는 수의 매개변수를 적어주면 되는데, 이때 호출할 때는 수만큼의 값을 ()안에 넣어서 호출해야한다.
위처럼 매개변수 a,b를 지정해주고 호출 시 값을 넣어주니 그 값이 매개변수 a,b에 각각 들어가는 것을 알 수 있다. 위 함수는 2값을 매개변수로 받고 매개변수들을 더한 값을 출력해주는 역할을한다.
-return
지금까지는 함수를 호출하고 함수 내 코드를 실행만 했지만, 함수에서는 return이라는 반환 기능이 있다.
예를들어 위에서 add함수로 a,b 매개변수를 받아 그 두수를 더한 값을 출력했는데, print대신 return a+b를 해주면 함수를 호출한 곳에 a+b 한 값을 보내주는 것이다.
위처럼 answer = add(3,4)로 add함수를 호출하면 매개변수 두 값을 더한 후 리턴해서 answer 변수에 넣어주는 것을 볼 수 있다.
또한 return은 값을 반환해주는 기능도 있지만 함수를 중단하는 역할도 해준다. return이 실행되면 그 함수를 중단한다. 만약 리턴 값이 있다면 그 값을 반환해주지만 리턴 값이 없다면 함수 중단 기능만 수행한다.
return으로 값을 여러개 반환하는 것 또한 가능하다.
위 사진처럼 return <반환값1>, <반환값2>...으로 써주고 호출할 때 반환 값을 담을 변수를 2개 써주면 된다. 위 addTen 함수는 매개변수들에 각각 10을 더해서 반환해주는 역할을 한다.
위 사진처럼 return <반환값1>, <반환값2>...으로 써주지 않아도 리스트나 튜플로 반환해 줄 수 있다.
-함수 호출 과정
위 코드는 a,b에 각가 3,4를 넣은 후 그 값을 매개변수로 맨 처음 add함수를 호출하고 그 add 함수는 받은 매개변수로 더한 것을 출력한 다음 minus 함수를 매개변수를 넣어 호출한다. 이 minus 함수는 매개변수 두 값을 뺀 값을 리턴해주고 그 리턴 값을 add 함수의 d변수에서 받아 출력해준다.
위 코드를 통해 함수 호출과정을 정리하겠다.
먼저 파이썬은 위에서부터 순차적으로 진행이 되므로, add,minus 함수의 정의, 그리고 변수 ab 그리고 값 3,4가 전역 프레임에 들어간다. 그 다음 add(a,b)함수를 호출하는데 이때 add 함수 스택 프레임이 생성되면서 매개변수 a,b에 3과 4가 들어간다. 그리고 c에 그 둘을 더한 값이 저장된다. 마지막으로 d로 minus 리턴 값을 받아오는데, 이 minus도 스택프레임이 만들어진다. minus 스택프레임에는 먼저 매개변수 a,b와 값인 3,4가 들어간다. 그 다음 c가 들어가는데 c의 값은 매개변수 a-b인 -1이 들어가게 되고 이 값이 return 된다. 이때 return 되면서 만들어진 minus 스택 프레임은 사라진다. 이 return 값을 add 스택 프레임 d에 값으로 들어가게 되는데, 이 d를 출력하고 함수 호출이 끝나게 되면서 add 스택 프레임 또한 사라진다.
전역 프레임의 경우 스크립트가 끝날 때까지 남아있고 스크립트가 끝나는 순간 사라진다.
매개변수가 없을 경우 <함수명>()로 호출하면 된다.
답 : c
두수를 매개변수로 받고 곱한 결과를 반환 하는 것이 목적이므로
def mul(a,b) :
return a*b
로 함수를 작성하면 될 것 같다.
따라서 답은 d다.
값을 3개 반환하는 함수를 만드는 것이 목적이므로 return을 이용해서 값들을 콤마로 구분 져서 반환하거나, 리스트나 튜플을 이용해서 값을 3개 반환하면 될 것 같다.
따라서 ,나 리스트, 튜플을 이용해서 리턴하는 a,c,d가 답이다.
참고로 사이트에 표기가 잘못돼 있어 unit 29.3이라 적혀있는데 Unit 29.7이 맞다.
목적은 주어진 x를 y로 나눴을 때 몫과 나머지를 출력되게 하는 것이 목적이다. 함수 하나 생성하고 매개변수를 받아서 나눈 몫과 나머지를 ,로 구분해 return하면 될 것 같다.
x = 10
y = 3
def get_quotient_remainder(a,b):
return a//b, a%b
quotient, remainder = get_quotient_remainder(x, y)
print('몫: {0}, 나머지: {1}'.format(quotient, remainder))
get_quotient_remainder 함수를 만들고 매개변수를 2개 받아서 그 값을 각각 //, % 연산을 수행해서 리턴하게 만들었다.
숫자 두개가 입력되는데 이 숫자들을 덧셈, 뺄셈, 곱셈, 나눗셈을 출력되게 해야한다. 단 나눗셈의 결과는 실수로 나와야한다. 함수를 하나 만들고 매개변수를 2개 받은 다음 그 값들을 각각의 연산들을 수행해서 4 값을 리턴해주면 될 것같다.
x, y = map(int, input().split())
def calc(a,b):
return [a+b,a-b,a*b,a/b]
a, s, m, d = calc(x, y)
print('덧셈: {0}, 뺄셈: {1}, 곱셈: {2}, 나눗셈: {3}'.format(a, s, m, d))
이번에는 리스트를 이용해서 리턴해 보았다. 각 요소를 받은 매개변수 a,b로 a+b, a-b, a*b, a/b를 수행한 후 결과 값을 리턴하면 된다. 참고로 a//b를 안써준 이유는 실수로 결과가 나와야하기 때문에 실수 결과값을 주는 / 연산자를 사용한 것이다.
01-30 (Unit 30.1~ Unit 33.6)
-위치인수, 리스트 언패킹-
-위치 인수
print(10,20,30)과 같이 함수에 인수들을 순서대로 넣는 것을 위치 인수라고 한다.(이 print 함수 경우 넣은 순서대로 값이 출력된다.)
위처럼 3개의 매개변수를 받아 각각 출력하는 printNum 함수에 1,2,3을 넣으면 이 순서대로 출력되는 것을 볼 수 있다. 이처럼 함수 인수에 순서대로 값을 넣는 것을 위치 인수라고 부른다.
-언패킹
앞서 집합 자료형인 세트를 배울 때 언패킹을 사용했었다. 위 코드에서는 매개변수 a,b,c를 받는 printNum을 호출하기 위해 printNum(1,2,3)을 사용했는데, 리스트 언패킹을 사용한다면 인수에 리스트를 넣고 앞에 *을 붙이면 리스트 안 요소들이 자동으로 매개변수 a,b,c에 넣어준다.
위처럼 1,2,3 요소를 가진 list a를 만들고 printNum 인수에 *a를 써주니 요소들이 언패킹 되어 a,b,c에 각각 들어가는 것을 볼 수 있다. 단 중요한 것은 리스트 언패킹으로 요소를 넣을 때는 매개변수의 수와 요소 수가 동일해야 한다는 것이다. 만약 매개변수는 2개인데 요소가 3개일 경우 언패킹을 한다면 오류가 난다.
-가변 인수
가변 인수는 매개변수에 *을 붙인 형태인데, 가변 인수를 사용할 경우 인수의 수가 제한이 없어진다. 앞에서 함수의 인수를 넣을 때는 매개변수의 수만큼 넣어줬는데 가변 인수를 사용한다면 원하는 만큼 인수를 넣을 수 있다.
위는 앞에서 만들었던 printNum 함수의 매개변수를 *num으로 바꿔준 모습이다. *num으로 바꾸니 인수 제한이 없는 가변 인수가 되서 1개를 인수로 넣든 5개를 넣든 제대로 값이 전달됨을 알 수 있다. 참고로 가변인수는 튜플 형태로 값을 받아온다. 이는 type 함수를 사용한다면 확인할 수 있다.
위에서는 num 이름으로 가변 인수로 사용한다 했지만 관례적으로는 arguments를 줄여서 args로 사용한다고 한다.
위처럼 고정 인수와 가변 인수를 같이 사용할 수도 있다.
-키워드 인수-
지금까지 함수를 호출할 때 인수를 그대로 넣었다. 위처럼 def printNum(a,b,c)가 있을 때 호출을 printNum(1,2,3)으로 넣어서 각각 인수가 어느 변수에 들어가고 어떤 용도로 사용되는지를 알기가 어려울 수 있다. 또한 순서도 맞춰야해서 불편한점이 있었다.
물론 위는 매개변수명을 단순 a,b,c로 만들어서 용도랄것도 없지만 학생정보를 출력하는 함수를 만든다 칠때, def printStuInfo(classNum,name,grade): 이 함수를 printStuInfo(12345,'test',3) 이런식으로 호출한다면 코드가 많아질 때 각각의 요소가 어느 용도로 쓰일지 짐작하기가 어렵다. 이때 사용하는 것이 키워드 인수다. 호출할 때 printStuInfo(classNum=12345,name='test',grade=3)이런식으로 키워드 인수를 사용해 호출한다면 훨씬 편해지는 것을 알 수 있다.
또한 뒤에서 매개변수의 초깃값을 배우는데, 원하는 매개변수만 키워드 인수로 값을 지정해준다면, 지정되지 않은 매개변수는 자동으로 초깃값이 들어간다.
-딕셔너리 언패킹
위에서는 키워드 인수를 이용해 classNum="30101" 이런식으로 적어줬는데, 이를 딕셔너리 언패킹으로 사용한다면 딕셔너리의 키는 키워드, 값은 인수로 해서 편하게 키워드 인수를 이용한 함수 호출을 할 수 있다. 딕셔너리 언패킹은 **을 사용한다.
당연하지만 딕셔너리 키와 매개변수이름이 같아야하고 매개변수 개수와 키의 개수 또한 같아야한다. *이 아닌 **을 사용하는 이유는 리스트는 값 하나씩만 가져왔지만 딕셔너리는 키와 값 2개를 가져와야하기 때문인 것 같다. 실제로 *하나만 써서 해본다면 키가 매개변수의 값으로 들어가는 것을 볼 수 있다.
위는 출석부를 출력하는 코드에 딕셔너리 언패킹을 넣어본 모습이다.
위처럼 **를 이용해서 가변 인수 또한 만들 수 있다. 원리는 위에 적은 가변 인수와 똑같다. 또한 고정 인수와 가변 인수는 같이 사용할 수 있다.
-매개변수 초기값-
지금까지는 매개변수들을 인수들을 이용해 값을 다 넣어줬는데 초기값을 이용한다면 다 넣어주지 않아도 된다. 값을 넣어주지 않는다면 초기값이 대신 들어가게 된다. 초기값을 넣는 방법은 간단하다. 예를들어 매개변수 a가 있다 치면 a=3 같이 함수 정의에서 값을 넣어준다면 그 값이 초기값이 된다.
위처럼 grade에 초기값을 넣어주니 인수를 2개만 넣어도 잘 작동하는 것을 볼 수 있다.
주의할점이 있는데 함수 정의에서 초기값이 있는 변수 뒤에 초기값이 없는 변수가 올 수 없다.
예를 들어 위 코드에서 def printStuInfo(classNum,grade=3,name) 같이 초기값이 정해진 grade 뒤에 초기값이 없는 name이 올 수 없다는 뜻이다.
따라서 초기값이 정해진 매개변수는 뒤쪽에 몰아줘야한다.
호출 방법은 위치 인수를 사용해 순서대로 3개의 값을 넣어주거나 키워드 인수를 사용해 a=<값>... 또는 리스트, 딕셔너리 언패킹을 통해 넣어줄 수 있다.
d : **은 딕셔너리 언패킹에 사용한다 X
리스트 언패킹을 이용해서 호출했기에 3개의 매개변수를 갔는 함수를 만들거나 *args로 가변 인수를 사용하는 함수를 만들면 된다.
따라서 답은 b,c다.
딕셔너리를 이용한 키워드 인수 호출 방식이다. name, age라는 매개변수에 값이 들어가므로 ** 가변인수로 해주거나 name, age라는 이름의 매개변수가 존재하면 된다.
a : ** 가변 인수이므로 가능하다. O
c : name, age 매개변수가 존재한다. O
인수로 주어진 과목중 가장 높은 점수를 구하는 함수를 만드는 것이 목적이다. 주어진 호출코드를 보면 가변적으로 인수가 주어지는데, 초기값을 설정해두던가 가변 인수로 함수를 만들면 될 것 같다.
korean, english, mathematics, science = 100, 86, 81, 91
def get_max_score(*args):
return max(args)
max_score = get_max_score(korean, english, mathematics, science)
print('높은 점수:', max_score)
max_score = get_max_score(english, science)
print('높은 점수:', max_score)
가변 인수를 사용하는 방식으로 코드를 짯다. 가변인수로 인수들을 리스트로 받아오고 max 함수로 리스트의 큰 값을 구한 후 return을 해줬다.
4과목의 점수가 입력되는 이 과목중 가장 높은, 낮은 점수와 평균 점수를 출력하는 것이 목적이다. 주어진 호출코드를 보면 함수 2개에서 각각 높고 낮은 점수, 평균 점수를 호출하므로 함수 2개를 만들면 될 것 같다.
korean, english, mathematics, science = map(int, input().split())
def get_min_max_score(*args):
return [min(args),max(args)]
def get_average(**args):
return sum(args.values())/len(args)
min_score, max_score = get_min_max_score(korean, english, mathematics, science)
average_score = get_average(korean=korean, english=english,
mathematics=mathematics, science=science)
print('낮은 점수: {0:.2f}, 높은 점수: {1:.2f}, 평균 점수: {2:.2f}'
.format(min_score, max_score, average_score))
min_score, max_score = get_min_max_score(english, science)
average_score = get_average(english=english, science=science)
print('낮은 점수: {0:.2f}, 높은 점수: {1:.2f}, 평균 점수: {2:.2f}'
.format(min_score, max_score, average_score))
get_min_max_score 함수는 값이 가변적으로 들어오므로 *args로 값을 받고 min,max 함수로 최대 최소 값을 return해줬다.
get_average 함수는 값이 키워드 인수로 가변적으로 들어오므로 가변 인수 **args로 값을 딕셔너리 형태로 받았다. 그리고 sum함수와 values로 args의 값들을 모두 합친값을 구했고 len(args)로 길이를 구해서 나눠줬다.
-재귀호출-
함수 안에서 함수 자기자신을 호출하는 것을 재귀호출이라한다. 사용법은 그저 함수 안에서 자기 자신을 호출해주면 되지만 단순히 이렇게 구현한다면 계속해서 자기 자신을 호출하게 되기 때문에 반복문처럼 탈출 조건을 줘야한다.
위 printHello 함수는 print("Hello")를 실행하는 함수인데 함수 코드 마지막 줄을 본다면 printHello()로 자기자신을 다시 호출하고 있다. 이대로 실행한다면 결과창처럼 Hello가 쭉 출력되다가 오류가 뜨는 것을 볼 수 있다. 오류가 뜨는 이유는 최대 재귀 깊이가 1000으로 저장돼 있어서 이를 초과하게 되면 위 오류가 뜨는 것이다.
따라서 위 현상을 해결하려면 조건을 걸어서 재귀를 탈출하게 해줘야한다.
위는 간단히 if문을 이용해서 조건을 건 모습이다. 먼저 7행으로 printHello를 호출하는데 매개변수 count에 10을 인수로 전달한다. 2행에서 만약 count가 0일시 return으로 함수를 종료해준다. 4행에서 Hello를 출력해주고 5행에서 count를 -=1 해준다. 그리고 6행에서 자기자신을 호출하는데 여기서 초점은 count를 인수로 호출한다는 점이다. 이 count는 -=로 1 줄인 값이 들어간다. 재귀 호출이 일어날 수록 count는 -=이 계속되어 언제가 0이 되는 시점이 오는데 이때 if문 코드를 실행해서 return으로 재귀를 끝내게 되는 것이다.
-팩토리얼 알고리즘
def factorial(n):
if n == 1:
return 1
return n * factorial(n - 1)
print(factorial(5))
위 코드는 팩토리얼을 구해주는 알고리즘이다. factorial 함수가 n 매개변수로 구할 팩토리얼 수를 받아온다. 먼저 n이 1인지 검사하고 만약 맞다면 1을 return해 준다. 만약 n이 1이 아니라면 그 다음 코드는 n과 factorial(n-1)을 곱한 값을 return하는데 결구 n-1이 factorial 재귀 호출을하게 된다.
팩토리얼이란 1~주어진 수까지 모두 곱하는 것이다. 예를들어 4 팩토리얼이라 치면 1,2,3,4를 모두 곱한값이 4팩토리얼 값이다. 수학기호로는 <수>!로 표기한다. 예) 4!
위 코드의 원리는 계속해서 n-1의 값을 인수로 factorial을 호출하는데 이 n이 1이 되는 순간 1을 반환해서 호출한 시점의 n과 1을 곱해서 리턴하고 그 값을 또 호출한 시점에서 n과 곱해서 리턴하는 식으로 계속해서 곱해서 팩토리얼 결과값이 만들어진다.
위 사진이 위의 코드 동작을 그림으로 나타낸 것이다.
a : 재귀호출은 함수에서 자기 자신을 호출하는 것이다.
c : 재귀호출은 반환값을 사용할 수 있다.(팩토리얼 알고리즘도 반환값을 이용했다.)
팩토리얼 표기는 !로 할 수 있다.
답 : a
4 팩토리얼을 1*2*3*4이다. 손이나 계산기로 계산할 수 있겠지만 위에서 적은 팩토리얼 알고리즘로도 계산 가능하다.
답 : 24
def is_palindrome(word):
if len(word) < 2:
return True
if word[0] != word[-1]:
return False
return is_palindrome(word[1:-1])
print(is_palindrome('hello'))
print(is_palindrome('level'))
생각보다 재귀 함수를 사용하는게 어려워 시간이 많이 걸렸다. 원리는 간단하다. is_palindrome 함수는 매개변수 word를 받는다. 이 word의 길이가 2보다 작다면 True를 리턴 그리고 word[0]하고 word[-1]이 일치하지 않는다면 False를 리턴하는 조건이 있다. 그리고 마지막으로 이 함수를 다시 재귀호출하는데 인수는 word[1:-1] 즉 앞과 뒷글자 하나씩 슬라이스한 상태로 재귀호출을한다. 이렇게되면 다시 2번째 조건문으로 같은지 확인하는 것을 len(word) < 2일 때까지 반복하는데 word의 길이가 2보다 작기전에 앞글자와 맨 뒷글자가 서로 다르다면 False를 바로 리턴해줘서 결과가 False가 된다. 만약 word의 길이가 2보다 작을 때까지 False가 리턴되지 않았다면 True를 리턴해서 결과가 True가 된다.
def fib(n):
if n <= 1:
return n
return fib(n-1)+fib(n-2)
n = int(input())
print(fib(n))
이번 문제는 어려워서 코딩도장의 힌트를 참고하여 풀었다.
fib(n-1) + fib(n-2)로 재귀를 호출해줘서 풀었는데 이유는 피보나치 n번째 수는 앞의 n-1, n-2의 합을 의미하고 n-1의 수는 n-2, n-3의 합을 의미하므로 위처럼 짠 것이다.
-람다 표현식-
지금까지는 def로 함수로 정의했다. 람다 표현식은 def와 달리 함수명이 필요없는 익명 함수를 만들 수 있다. 람다 표현식은 이름 그대로 식 형태로 되어있어서 간편하게 작성할 수 있다.
작성 방법은 lambda <매개변수> : <식>이다. def 함수 정의와 달리 (), return을 써줄필요 없이 자동으로 매개변수 사용, 반환을 해준다. 만약 매개변수에 아무것도 입력하지 않는다면 매개변수를 사용하지 않고 리턴할만한 값이 없다면 리턴도 안해줄 것이다.
앞에서 만들었던 add 함수가 lambda로 좀더 간결하게 구현이 되는 것을 볼 수 있다.
표현식 자체를 호출하는 방법은 람다식을 쓰고 뒤에 (<인수>)를 붙여주면 된다.
람다 표현식에서 주의할 점은 def처럼 람다 안에서 변수를 새로 못만든다는 것이다. 람다 표현식에서 변수를 만든다면 오류가 뜨는 것을 볼 수 있다. 다만 기존에 정의된 변수들은 사용이 가능하다.
람다 표현식을 사용하는 이유는 함수의 인수 부분에서 간단하게 함수를 만들기 위해서라고 한다.
위처럼 map은 전에는 int 등으로 자료형 변환에 사용했지만 리스트 내 값들을 인수로 함수를 실행해주는 역할도 한다.
def 함수를 사용하면 함수를 하나 만들어야하므로 코드가 좀 길어지는데 람다 표현식을 사용한다면 간단하게 구현할 수 있다.
위 코드처럼 def보다 훨씬 코드량이 준 것을 알 수 있다.
람다 표현식에 조건식을 넣을 수 있다.
lambda <매개변수> : <식1> if 조건식 else <식2>로 사용하면되는데 if 조건이 맞을 시 <식1>이 실행되고 아닐시 <식2>가 실행된다. 람다 표현식에서 if를 쓴다면 반드시 else도 써줘야한다.
위 is_one 람다 표현식은 입력된 수가 1인지 확인한다. 1이라 Yes, 1이 아니라면 No를 출력해준다.
마치 리스트 표현식처럼 조건을 사용해서 특정 값만 골라낼 수도 있다.
이렇게 사용하면 코드가 간결해지긴 하지만 가독성이 좋지 않아지므로 길어지더라도 def로 함수를 정의하는게 더 좋다고 한다.
이제까지 map으로 객체 하나만 사용했는데, 2개 이상도 가능하다.
위처럼 두개 이상 객체를 map 할 경우 변수 람다 표현식에서 변수를 2개를 두면 된다.
-filter
filter는 map과 비슷한 역할을 한다. fileter(<함수>,<객체>)로 사용하면 객체를 함수의 매개변수로 전달해 리턴값을 요소로 가져온다.
위처럼 함수에 조건을 넣고 filter에 an,a를 넣어준다면 조건에 만든 요소만 리스트에 들어가는 것을 볼 수 있다.
위처럼 람다 표현식으로도 사용가능하다.
-reduce
reduce는 functools를 import해야지 사용할 수 있는 함수다. 이 함수는 각 요소들을 지정 함수로 처리한 뒤 결과를 누적해서 반환하는 함수다.
위 코드는 람다 표현식과 reduce를 이용해 a의 요소를 더한 것을 누적해서 출력한 것이다.
위 사진이 위 코드의 동작과정이다. 보면 결론적으로 1+2+3+4+5 연산을 수행하는 것을 알 수 있다.
이번에 배운 람다 표현식은 간단한 함수를 만들 때 주로 사용된다 한다.
람다 표현식은 lambda <매개변수> : <식>으로 작성한다. 매개변수가 3개이므로 3개의 매개변수가 존재해야하고 모두 곱해서 반환하는 것이지만 람다 표현식에서는 return을 쓸 필요가 없다. 따라서 d를 제외한 다른 보기들은 문법상으로 잘못됐다.
람다 표현식 자체를 호출하는 방법은 (<람다표현식>)(<매개변수>)로 호출하면 된다.
따라서 답은 b다.
요소중 7로 끝나느 숫자만 다시 리스트로 만드므로, 10으로 나눴을 때 나머지가 7인지를 판단하면 될 것 같다. 끝나는 숫자만 다시 리스트로 만드는 것이므로 filter함수가 적합할 것 같다. map이나 reduce는 각각 값을 바꿔주거나 누적하는 것이므로 적합하지 않다. 따라서 답은 c다.
확장자가 .jpg, png인 이미지 파일을 람다표현식과 문자열 메서드를 이요해 출력되게 해야하므로 find 함수를 람다식 안에 넣주면 될 것 같다. 또한 filter 함수를 이용해 조건에 맞는 요소만 list로 저장하면 된다.
files = ['font', '1.png', '10.jpg', '11.gif', '2.jpg', '3.png', 'table.xslx', 'spec.docx']
print(list(filter(lambda x : x.find('.png') != -1 or x.find('.jpg') != -1,files)))
find를 했을 때 해당 문자열이 없다면 -1을 반환해 준다는 점을 이용해 작성한 람다식이다. find 결과가 -1이 아닌 문자열들만 list로 만들어 출력해줬다.
숫자.확장자 형식으로 파일 이름들이 입력되는데, 파일 이름이 숫자 3자리이고 앞에 0이 들어가는 형식으로 출력되게 만드는 것이 목적이다. zfill 함수와 map을 이용해서 출력하면 될 것 같다.
files = input().split()
print(list(map(lambda x : x[:x.find('.')].zfill(3)+x[x.find('.'):],files)))
확장자와 파일 이름을 구분하는 문자인 '.'을 find로 인덱스를 알아내 이름 부분만 슬라이스를 한 다음 zfill(3)을 이용해 001,099 등의 형식으로 맞춰줬다. 그 다음 확장자만 슬라이스를 한 상태에서 zfill 한 문자열을 합치면 결과처럼 출력이 된다.
-지역 변수, 전역 변수-
지역 변수는 함수 내에서 생성되는 변수로 그 함수 내에서만 접근 가능하다. 이 지역 변수에 접근할 수 있는 범위를 지역 범위라고 부른다. 전역변수는 스크립트 전체에서 사용, 접근 가능한 변수다. 이 전역변수에 접근할 수 있는 범위를 전역 범위라고 부른다.
위처럼 1행에 a를 선언하고 값 1을 할당하니 함수 aVal 내에서도 접근이 가능한 것을 볼 수 있다. 당연하지만 함수 밖에서도 접근이 가능하다. 이때 전역에서 접근 가능한 변수 a를 전역 변수라고 부르고 a에 접근 가능한 1~5행을 전역 범위라고 부른다.
전역 변수 예시 코드와 달리 이번 코드에는 a를 함수 aVal안에서 선언했다. 보면 함수 aVal에서는 a에 접근할 수 있지만 함수 밖, 즉 5행에서 a에 접근하려 할 때 not defined라는 오류가 뜨는 것을 볼 수 있다. 여기서 함수 안에서 선언된 a를 지역 변수라고 하고 aVal을 지역 범위라고 한다.
위 코드는 1행에서 a를 선언하고 값을 1 넣고 3행 aVal 함수안에서 a에 2를 넣어준 모습이다. 당연히 나는 전역 변수 a의 값을 2로 바꿨다 생각했지만 결과를 보니 1행의 a와 3행의 a는 다른 변수인 것을 알게 되었다. 3행은 전역 변수 a에 값을 2로 바꿔준게 아니라 aVal 함수내의 2의 값을 가지는 a라는 지역 변수를 생성한 것이다.
-global
전역 변수의 값을 함수 내에서 바꾸려면 global을 사용하면 된다. 사용은 전역 변수를 함수 내에서 사용하기 전에
global <변수명>을 적어주면 된다.
위처럼 aVal 함수 안에서 a를 사용하기 전에 global a를 써주니 전역, 지역 변수 코드처럼 a라는 새로운 지역 변수를 생성하지않고 전역 변수 a의 값을 바꿔주는 것을 볼 수 있다.
위처럼 전역 변수 a가 없는 상태에서 global a를 써준다면 함수 내의 변수 a가 전역 변수가 된다. 이 변수 a를 함수 밖에서 접근하니 잘 접근이 되는 것을 볼 수 있다.
-nonlocal
마치 중첩 if문처럼 함수에서도 함수 안의 함수를 만들 수 있다.
위 사진은 함수 a안에 함수 b를 하나 더 만든 모습이다. 호기심이 생겨서 함수 a 밖에서 b를 호출하려 해보니 정의되지 않았다는 오류가 뜬다. 함수 안의 함수는 해당 함수에서만 호출이 가능한 것 같다.
위 전역 변수처럼 함수에서 지역 변수를 만들고 그 함수 내의 함수에서 지역 변수의 값을 바꾸려고 시도해보면 함수 내의 함수에서 새로운 지역 변수를 생성하는 것을 볼 수 있다. 이를 방지하기 위해 쓰는 것이 nonlocal이다. global처럼 함수 내 함수에서 지역 변수를 사용하기 전에 nonlocal을 써주면 함수의 지역 변수를 함수 내 함수에서 그대로 쓰는 것을 볼 수 있다.
위 코드의 함수 a와 그 안의 함수 b는 nonlocal을 사용한 것이고 함수 c와 그 안의 함수 d는 nonlocal을 사용하지 않은 것이다. 결과를 확인해보면 둘다 함수 내의 함수가 val을 10에서 20으로 바꿔주는 역할을 하는데, nonlocal을 사용한 a는 val이 20이 됐고, 사용하지 않은 c는 결과가 10 그대로인 것을 알 수 있다.
위 사진은 함수를 a안에 b, b안의 c로 중첩시키고 c에서 nonlocal을 써본 모습이다. 변수 y를 보면 함수 a에도 하나 있고, b에도 하나 있어서 함수 c에서 nonlocal을 하면 어떤 변수를 사용할지 햇갈렸는데, 실행시켜보니 nonlcoal은 가장 가까운 함수의 지역 변수를 사용하는 것을 알 수 있다. x같은 경우는 b에 없으니 b의 전인 a의 지역 변수를 사용한다.
실무에서는 중첩 함수를 쓸 일이 거의 없지만 쓴다면 변수 이름을 다르게 하는 것을 추천한다고 한다.
참고로 nonlocal 대신 global을 사용한다면 무조건 전역변수를 사용한다고 한다. 위처럼 y가 여러 중첩의 함수에 선언되어 있지만 global을 쓰니 다 무시하고 전역인 1행 y를 사용하는 것을 볼 수 있다.
-클로저-
클로저는 함수를 둘러싼 환경을 유지하다가 함수를 호출할 때 다시 꺼내 쓰는 것이다.
위 코드를 보면 지금까지 배웠던 규칙이랑 많이 다른 것을 볼 수 있다. add는 매개변수를 받지 않지만 7행에서 인수를 넣어서 호출하고, 5행을 보면 rtnAddVal은 매개변수가 있는 함수지만 ()와 인수를 써주지 않고 return 해준다. 또한 6행에서는 변수에 함수형태를 넣어주는 것을 볼 수 있다.
일단 실행 과정은 add 함수의 지역변수 x = 1을 정의하고 5행에서 rtnAddVal을 리턴하는데 rtnAddVal은 add의 지역변수 x와 매개변수 a를 더한 것을 반환해준다. 그리고 6행에서는 fun1 = add()로 함수를 변수에 담아버리는데 이렇게 한다면 함수의 환경을 유지하다가 다시 함수를 호출할 때 그 환경 그대로 쓰이는 것 같다. 뭔가 설명이 이상한데 저 코드에서는 딱히 상관이 없지만 만약 중첩 함수에서 클로저의 변수를 바꾼다면 그 바꾼 변수는 지역 변수라 사라지는 것이 아닌 fun1 안에 add 함수가 아닌 rtnAddVal가 들어 갔기 때문에 add 함수의 환경은 유지되어 다음에 호출 될 때 변한 지역 변수로 중첩 함수를 수행한다는 것으로 이해했다. 이는 아래 연습 문제를 풀어볼 때 다루게 된다. 한가지 확실한건 변수 없이 바로 호출을 해보니 오류가 뜬다는 것을 알 수 있다. 7행은 func1에 요소를 넣어서 호출하는데 이 요소가 rtnAddVal의 매개변수 a의 값으로 전달된다. 따라서 결과는 요소에 넣은 값 +1이 출력된다.
중요한 점은 return rtnAddVal처럼 ()를 써주면 안된다는 것인데 왜냐하면 return rtnAddVal은 rtnAddVal의 반환값을 반환하는게 아니라 이 rtnAddVal 함수 자체를 반환하기 때문이다.
클로저에 속하는 지역변수는 바깥에서 접근이 안되기 때문에 데이터를 숨길때 사용한다고 한다.
마찬가지로 클로저의 변수 또한 지역 변수이므로 rtnAddVal에서 nonlocal을 사용해 클로저의 지역 변수에 접근할 수 있다.
위처럼 nonlocal을 사용해 바깥 함수의 total 변수에 결과값을 누적시킬 수 있다.
이 total은 지역변수지만 결과를 보면 누적이 유지되는 것을 알 수 있는데, 이유는 fun1 = add()를 해줌으로써 add의 환경이 계속 유지되기 때문이다.
1. total(0) = 1+1
2. total(2) = 2+1
3. total(5) = 3+1
위 순서대로 total이 누적된다.
-lambda
앞에서 배웠던 lambda로도 클로저를 구현할 수 있다.
앞의 클로저 코드를 람다 표현식으로 사용한다면 훨씬 더 간결하게 작성할 수 있다. 결과는 똑같이 요소+x(1)로 출력된다.
-namespace-
파이썬에서 변수는 namespace라는 곳에 저장된다고 한다. 이 namespace를 확인하려면 locals() 함수를 사용하면 된다.
locals 함수를 사용하면 딕셔너리 형태로 값들을 가져올 수 있다.
위 사진처럼 a,b,c를 선언하고 locals 함수를 사용하니 변수명과 값이 출력되는 것을 볼 수 있다.
호기심이 생겨서 함수안에서 변수를 선언하고 locals, 함수 밖에서 locals()를 써봤는데 보면 함수 안에서는 그 함수 안의 변수들만 존재하는 것을 볼 수 있다. 아마 함수의 스택 프레임을 출력해주는 것 같다. 그리고 밖에서 locals를 써주니 전역의 모든 값들을 출력해준다.
b : 전역 변수 접근은 nonlocal이 아니라 global이다. X
d : global을 사용한다면 얼마나 중첩되있든 간에 전역 변수에 접근할 수 있다. X
c : 지역 변수는 함수 바깥쪽에서 사용할 수 없다. X
d : 바깥쪽의 지역 변수면 뭐든 사용 가능하다. X
c : 클로저는 클로저다. 람다는 함수를 한줄로 나타낼때 사용하는 것이다.
d : 클로저는 람다 표현식으로 만들 수 있다.
def counter():
i = 0
def count():
nonlocal i
i += 1
return i
return count
c = counter()
for i in range(10):
print(c(), end=' ')
몇번 호출되는지 호출 횟수를 누적시켜야한다. 클로저를 보면 i라는 지역변수가 있는데 이를 nonlocal로 가져와서 i += 1로 횟수를 누적시켰다. 클로저의 환경은 유지되므로 i는 사라지지 않고 계속 값을 누적해준다. 그리고 count 함수에서 return i를 해주면 누적된 횟수가 출력된다.
정수가 입력되는데 이 정수를 함수가 호출될 때 마다 숫자가 1씩 줄어드게 만들어야한다. 클로저에 지역변수를 만들어 매개변수를 저장하고 중첩 함수에서 nonlocal로 지역변수에 접근해 1씩 -해주면 될 것같다.
def countdown(n):
i = n+1
def minus():
nonlocal i
i -= 1
return i
return minus
n = int(input())
c = countdown(n)
for i in range(n):
print(c(), end=' ')
2행에 매개변수를 담을 지역 변수 i를 만들었는데, 이 i에 n+1을 넣어준 이유는 중첩 함수인 minus에서 바로 1씩 빼주기 시작하는데, 문제의 예시 결과를 보면 입력한 값이 먼저 출려되고 -가 되므로, 문제 결과와 일치시키기 위해서 +1을 해줬다. minus 함수에서 i에 접근하고 이 i에 1을 빼준 후 i의 값을 return해주도록 했고, 또한 7행에서 클로저를 구현하기 위해서 minus의 반환값이 아닌 minus 함수 자체를 반환해줬다.
01-31 (Unit 34.1~ Unit 37.3 end!)
-클래스(class)-
클래스는 객체를 표현하는 문법이다.
코딩도장에 나와있는 설명은 아니지만 예전에 다른 언어에서 클래스를 배울 때는 클래스란 붕어빵 틀과 같은 것이라 비유해서 이해했다. 클래스는 붕어빵 틀이 있는 공장이다. 붕어빵 틀이 있는 공장(클래스)으로 붕어빵(객체)을 생산하면 똑같은 모양(속성)의 붕어빵(객체, 정확히는 인스턴스)들이 찍혀나온다. 여기서 생산할 붕어빵의 맛, 쫄깃함 등을 클래스의 속성이라 부른다. 또한 팔린다,먹힌다는 행동이 존재하는데, 이게 메서드에 해당한다.
여기서 객체란 사람, 나무, 붕어빵 등 특정한 개념이나 모양으로 존재하는 것을 객체라 부른다.
즉 이 객체를 생성하는 문법(속성, 메서드 등이 포함)을 클래스라 부르는 것이다. 당연하게도 같은 클래스에서 만들어진 인스턴스(객체)는 같은 속성의 종류, 같은 메서드 들이 있게된다. 마치 붕어빵처럼 말이다.
코딩도장의 설명은 rpg 같은 게임 속에 존재하는 게임 캐릭터에 비유했다. 게임 캐릭터가 객체에 해당하고 게임 캐릭터의 체력, 공격력 등이 속성, 그리고 캐릭터의 스킬, 예를 들어 리그오브레전드 게임의 qwer 스킬들 같은 것이 메서드에 해당한다. 이 캐릭터의 속성, 메서드가 담겨 있는 문법을 클래스라 부르는 것이다.
빈클래스도 만들 수 있는데, 빈클래스는 pass를 넣어주면 된다.
이렇게 클래스를 사용해서 프로그래밍하는 언어를 객체지향언어라고 부른다.
객체지향프로그래밍은 복잡한 문제를 나눠서 객체들로 만들어 문제를 해결하는데, 문제가 생길 때마다 해당 객체만 수정하면 되므로 유지보수에 좋다.
객체와 인스턴스는 같은 것을 의미하지만 그냥 객체만 지정할 때는 객체, 클래스와 연결지을 때는 인스턴스라고 표현한다.
-class
클래스 사용방법은 다음과 같다.
class <클래스명>:
<클래스 속성, 메서드>
여기서 메서드의 의미는 클래스 안에 들어있는 함수를 의미한다. 함수라는 단어가 좀 더 포괄적인 의미를 갖는 것 같다.
위에서 예시로 정리했던 붕어빵을 직접 클래스로 만들어본 모습이다.
일단 클래스 이름은 FishBread, 안의 메서드는 eated라는 붕어빵이 먹히는 동작을 구현했다.
메서드는 함수 정의와 똑같이 def로 하는데 이때 첫번째 매개변수에는 사용하든 안사용하든 반드시 self를 넣어줘야한다.
정의한 클래스로 인스턴스를 생성하는 방법은 <인스턴스명> = 클래스()를 해주면 된다. 위 4행을 본다면 fb1이라는 이름으로 FishBread 인스턴스를 만들었다.
클래스의 메서드를 호출하는 방법은 인스턴스를 만들고 <인스턴스>.<메서드명>()을 해주면 된다. 위 코드 5행에서 만들어진 인스턴스 fb1에 .eated()를 붙여서 eated 함수를 호출하는 것을 볼 수 있다. 또한 이 코드에는 없지만 클래스의 메서드에서 다른 메서드를 호출할 수 있다.
이처럼 인스턴스를 통해 호출하는 메서드를 인스턴스 메서드라고 부른다.
우리가 전에 흔히 사용했던 int, dict, list 등도 사실 클래스였다.
a = int(5)
이 코드도 int라는 클래스에 5를 넣어서 인스턴스 a를 만든 것이다.
변수를 선언할 때 보통 자료형을 생략해서 값을 넣는데, 이는 클래스가 아닌게 아니라 클래스를 넣는 것을 생략시켜준 것이다. 예시로 예전에 type로 자료형을 사용했었는데 그때 int 자료형 변수를 type로 확인한다 치면 <class 'int'>와 같이 나왔다.
위에서 만든 fb1도 type로 확인하면 클래스명이 나온다.
-속성
속성 정의 방법은 클래스에서 변수처럼 선언하면 방법이 있지만, 코딩도장에서는 __init__을 이용해서 생성과 동시에 메서드 안에서 속성을 만든다.
위 코드에서 2행을 본다면 __init__ 메서드에서 FishBread의 속성을 정의하는 것을 볼 수 있다.
여기서 __init__은 일반 메서드가 아니라 인스턴스가 생성될 때 호출되는 특별한 메서드다. initialize 즉 초기화를 뜻한다.
매개변수 self는 인스턴스 자기 자신을 의미한다. 즉 self.flavor는 fb1 인스턴스에 "red bean"이라는 문자열을 가진 flavor라는 속성(변수)를 만드는 것이다.
4행의 flavorInfo 함수에서 self.flavor를 출력하는데, 이 self.flavor는 __init__에서 정의된 "red bean" 문자열을 가지고 있다. 따라서 결과는 red bean맛입니다. 가 출력된다.
-self
self는 위에서 말했듯이 인스턴스 자기 자신을 의미한다.
위 사진의 코드는 코딩도장의 예제 코드로 Person이라는 클래스에 '안녕하세요' 문자열을 가진 hello라는 속성을 __init__에서 생성, greeting 함수에서 이 hello를 출력하는 것이다.
여기서 self는 인스턴스 자기 자신이라 했는데, 이 인스턴스는 결국 Person이라는 클래스로 생성 됐으므로 self의 값은 Person()이라 할 수 있다. 이 self가 완성되면 인스턴스인 james에 할당된다 하는데, 솔직히 이해가 잘 안되서, 그냥 self는 처음에는 Person()과 같지만 __init__을 통해 속성을 추가하면서 자신만의 인스턴스로 변하는 것이라 이해했다.
-인스턴스 속성 값 받기
위에서는 클래스 자체에 속성을 만드는 코드와 값을 정해뒀는데, __init__ 함수에 매개변수를 둔다면 인스턴스를 생성할 때 인수로 값을 받아 그 값으로 속성을 만들 수 있다.
위 코드처럼 __init__에 flavor 매개변수를 추가하여 8행의 인스턴스 생성과정에서 인수를 받는 것을 볼 수 있다. 이 인수는 __init__의 flavor의 값으로 들어가 속성 flavor에 추가된다.
위 코드에는 매개변수를 하나만 만들었지만 필요에 따라 여러개를 만들어서 사용할 수 있다. 또한 앞서 배운 가변 인수 또한 사용할 수 있는데 사용방법은 똑같이 *를 이용하면 되니 자세한 설명은 생략하겠다. 딕셔너리 언패킹, 리스트 언패킹 등 그냥 메서드에 매개변수로 되는 것은 모두 되는 것 같다.
위 코드처럼 init으로 속성을 만들지 않아도 <인스턴스>.<속성이름> = <값>으로도 속성을 만들 수 있다.
물론 이렇게 하면 해당 인스턴스에만 속성이 만들어지므로 다른 인스턴스를 만들었을 때 그 속성은 존재하지 않는다.
이와 마찬가지로 꼭 __init__을 통해 속성을 생성할 필요 없이 다른 메서드에서 속성을 만들 수 있다.
-__slots__
__slots__는 클래스의 속성을 제한해준다.
위 코드 2행처럼 __slots__ = [<속성>,<속성2>...]을 해준다면 해당 이름의 속성들만 만들 수 있다.
-비공개 속성
속성을 만든후 <인스턴스>.<속성>이란 코드를 작성하면 해당 속성의 값을 자유롭게 접근할 수 있다. 다른 언어의 private 같은 기능이 파이썬에도 존재한다. 이 private란 비공개 속성인데, 클래스 안 메서드를 제외한 곳에서는 호출이 불가능한 속성을 뜻한다.
이 비공개 속성을 만드는 법은 간단하다. 속성의 이름 앞에 __을 붙여준다면 클래스 밖에서는 접근이 안된다.
위처럼 __을 붙여서 속성을 만드니 8행처럼 클래스 밖에서 접근하려 시도하니 오류가 뜨는 것을 볼 수 있다. 하지만 7행의 클래스 내 메서드로 접근하면 오류가 제대로 출력이 되는 것을 볼 수 있다.
위처럼 __로 함수를 비공개로 만드는 것도 가능하다. 이럴경우 클래스 내 다른 메서드에서는 호출이 가능하지만 클래스 밖에서는 호출이 안된다.
이처럼 클래스 내에서만 접근 가능한 속성을 비공개 속성, 클래스 밖에서도 접근 가능한 속성을 공개 속성이라한다.
class FishBread:
def __init__(self, flavor, price, amount):
self.flavor = flavor
self.price = price
self.amount = amount
self.isSell = False
def selling(self):
if self.isSell == True:
print("이미 팔렸습니다.")
return
print('{0}원에 붕어빵이 팔렸습니다.'.format(self.price))
self.isSell = True
def eated(self,eatedAmount) :
if self.isSell == False:
print("아직 팔리지 않았습니다.")
return
if self.amount <= 0:
print("이미 다 먹었습니다.")
return
self.amount -= eatedAmount
if self.amount <= 0:
print("다 먹었습니다.")
else:
print("{}만큼 남았습니다.".format(self.amount))
fb1 = FishBread("read bean",1000,100)
fb1.selling()
for i in range(11):
fb1.eated(10)
위 코드는 배운 것을 응용해 만들어본 붕어빵 클래스다.
__init__으로 flavor,price,amount 속성을 정의하고 selling에서 팔림 여부를 판단하는 isSell이 True라면 이미 팔렸다는 문구를, False라면 <price 값>원에 붕어빵이 팔렸다는 문구를 출력하고 isSell을 True로 바꿔준다. eated는 먹히는 기능을 하는 메서드인데 매개변수로 먹은 양을 받는다. 먼저 팔림 여부를 체크하고 붕어빵의 양(amount)가 0보다 작다면 이미 다 먹은 것이므로 다 먹었다는 문구를 출력한다. 그 다음에는 매개변수로 주어진 eatedAmount 만큼 amount에서 빼주고 만약 amount가 0 이하가 된다면 다먹었다는 문구를 0보다 크다면 amount만큼 남았다고 출력한다.
클래스의 메서드를 호출하는 방법은 먼저 <인스턴스 명> = Person()으로 인스턴스를 만들고 <인스턴스>.<메서드명>()을 해주면 된다. 따라서 답은 d다.
인스턴스를 만들 때 호출되는 메서드는 intialize 즉 __init__이다.
답 : __init__
클래스 내의 메서드에서 속성에 접근하는 방법은 self.<속성명>을 써주면 된다. 이 경우 속성이 name이므로 self.name을 해주면 된다.
답 : e
비공개 속성은 속성 이름 앞에 __을 붙이면 된다.
답 : c
게임 캐릭터 Knight의 클래스를 만드는데 이때 _init__의 매개변수로 health, mana, armor를 받아서 각각의 속성을 만들어야한다. 그리고 slash라는 메서드를 만들어 print("베기")라는 코드를 작성하면 될 것 같다.
class Knight:
def __init__(self,health,mana,armor):
self.health = health
self.mana = mana
self.armor = armor
def slash(self):
print("베기")
x = Knight(health=542.4, mana=210.3, armor=38)
print(x.health, x.mana, x.armor)
x.slash()
게임 캐릭터 능력치가 입력되는데 이 값을 속성으로 class Annie를 작성하여 티버 스킬의 피해량을 출력하는 메서드를 작성하면 된다. 애니, 티버라 하니 리그오브레전드의 그 캐릭터가 생각나는 것 같다..ㅋㅋ
__init__으로 매개변수 health, mana, ability_power를 속성으로 만들어 값을 넣어주고 메서드 tibbers를 작성해 ability_power * 0.65 +400을 출력하면 될 것 같다. 물론 결과 형식과 맟춰서 출력해야한다.
class Annie:
def __init__(self, health, mana, ability_power):
self.health = health
self.mana = mana
self.ability_power = ability_power
def tibbers(self):
print('티버: 피해량 {}'.format(self.ability_power * 0.65 + 400))
health, mana, ability_power = map(float, input().split())
x = Annie(health=health, mana=mana, ability_power=ability_power)
x.tibbers()
__init__으로 매개변수들을 이용해 속성을 정의하고 tibbers 함수를 만들어 format으로 결과 형식과 맞춰서 피해량을 출력했다.
-클래스 속성, 인스턴스 속성-
속성은 클래스 속성과 인스턴스 속성으로 나뉜다. 지금까지 __init__을 통해 속성을 만들었는데 이 방법으로 만들어진 속성은 인스턴스 속성이다. 클래스 속성은 그냥 변수를 선언하는 것처럼 클래스에 속성을 만드는 방법을 통해 만들어진 속성이다.
위에 클래스 속성으로 tmp, 인스턴스 속성으로 tmp2를 각각 list형으로 만들고 appendTmp, appendTmp2 메서드로 요소를 append 시켜준다. 14~15행으로 Tmp에 1,2를 각각 fb1, fb2에서 추가해주고 print하는데 결과를 보면 이상하게 각각 ['1'] ['2']가 아닌 ['1','2'] ['1','2']를 가지고 있다.
이 이유는 클래스 속성은 클래스에 속해 있기 때문에 모든 인스턴스에 공유되는 것이다.
위처럼 클래스 속성에 접근할 때는 self라 쓰면 인스턴스 속성에 접근하는 것처럼 느껴지므로
<클래스이름>.<속성>명으로 접근해주면 좀더 명확해진다.
위 코드로 예를 들면 print(FishBread.tmp)로하면 된다.
위 코드에서 인스턴스 속성 접근 방법으로 했는데 클래스 속성이 접근이 되는 이유는 파이썬에서 속성, 메서드 이름을 찾을 때 인스턴스 클래스 순으로 찾기 때문이다. 만약 인스턴스에 속성이 존재하지 않는다면 클래스 속성에서 찾게 되는 것이다.
19~24행에서 접근하는 속성은 인스턴스 속성이다. 인스턴스 속성을 클래스 속성과 똑같이 append를 시켜주니 결과는 ['1'],['2']로 따로따로 공유되지 않는 상태로 나오는 것을 알 수 있다.
-비공개 클래스 속성
클래스 속성 또한 비공개 속성으로 만들 수 있는데, 인스턴스 속성이랑 똑같이 앞에 __를 붙여주면된다.
-독스트링
함수 파트 때 배웠던 독스트링을 클래스에도 사용이 가능하다.
그저 클래스 바로 아래에 ''' '''을 써주면 독스트링이 된다. 독스트링 접근은 <클래스>.__doc__으로 하면 된다.
-정적 메서드-
위에서는 클래스 내의 메서드를 호출할 때 인스턴스를 만들고 그 인스턴스를 통해 호출했는데, 정적 메서드의 경우 인스턴스 없이 바로 클래스에서 호출이 가능하다.
이 정적 메서드는 메서드의 코드가 인스턴스의 상태(속성 등)을 변화시키지 않는 메서드를 만들 때 사용한다. 인스턴스의 상태와는 상관없기 때문에 메서드에 매개변수로 self를 넣어줄 필요가 없다.
정적 메서드 사용 방법은 해당 메서드 위에 @static method를 붙여주면 된다.
@static method
def fun():
위처럼 인스턴스를 만들지 않아도 메서드가 호출되는 것을 볼 수 있다.
파이썬의 자료형 함수들도 인스턴스 메서드와 정적 메서드로 나눠진다. 변수의 내용을 변경할 때는 <변수명>.<메서드>를 사용하는데 이 변수가 곧 인스턴스이므로 인스턴스 메서드 방식으로 사용하는 것이다. 변수의 내용을 변경하지 않을 때는 <자료형>.<메서드>로 사용한다. 이는 클래스.메서드 방식이므로 정적 메서드를 호출한다는 것으로 볼 수 있다. 대표적인 인스턴스 메서드 방식은 split, zfill 등이 있고 정적 메서드 방식은 join, union 등이 있다.
-클래스 메서드-
클래스 메서드는 정적 메서드와 비슷하지만 약간의 차이가 있다. class 메서드는 정적 메서드와 비슷하게 @classmethod를 해당 메서드 위에 붙여서 사용할 수 있다.
클래스 메서드는 정적 메서드처럼 인스턴스 없이 호출이 가능하다. 다만 클래스 메서드는 메서드 안에서 클래스 속성, 클래스에 접근해야할 때 사용한다.
위 코드는 인스턴스가 만들어진 수를 세는 기능을 한다. 인스턴스가 만들어질 때 마다 __init__이 호출되면서 count += 1이 된다. 여기서 주목할 점은 count 변수는 클래스 속성으로 만들어져서 클래스끼리 공유가 된다는 점이다. 7~9행은 클래스 메서드로 만들어서 매개변수 cls를 통해 클래스 속성 count에 접근할 수 있다. 이 값을 출력해보면 3이 나온다. 생성한 인스턴스의 수를 보면 3, 정확한 걸 알 수 있다.
이 매개변수 cls는 클래스 cls1과 같기 때문에 이 메서드 안에서 <인스턴스명> = cls()로 인스턴스를 만들 수도 있다.
클래스 바깥에서 클래스 속성 x에 접그하는 방법은 인스턴스를 생성해도 접근이 가능하지만 좀더 올바른 방법은 <클래스명>.<속성명>, 즉 Person.x다.
답 : a
정적 메서드는 메서드 위에 @staticmethod를 붙여주면 된다. 또한 매개변수로 self를 쓰지 않아야한다. 따라서 답은 c다.
c : 첫 번째 매개변수는 cls다. 인스턴스가 아닌 클래스가 들어온다. X
e : @classmethod를 붙여야한다. X
주어진 코드에서 class Date안에 bool 값을 리턴하는 is_date_valid를 만들어야한다. 이 메서드는 문자열이 올바른 날짜인지 검사해야하고 또한 <클래스>.<메서드>로 호출하는 것을 보아 정적 메서드로 만들어야하는 것 같다.
class Date:
@staticmethod
def is_date_valid(date):
year, mon, day = map(int, date.split('-'))
if 0 < mon <= 12 and 0 < day <= 31:
return True
return False
if Date.is_date_valid('2000-10-31'):
print('올바른 날짜 형식입니다.')
else:
print('잘못된 날짜 형식입니다.')
일단 정적 메서드로 is_date_valid를 만들었고 매개 변수 date를 받아 -를 기준으로 split한 후 int형으로 만들어줘서 '12월까지 일은 31일까지 있어야 합니다.'라는 조건에 맞는지 부등호로 검사했다. 만약 조건에 맞는다면 True를 조건에 맞지 않으면 False를 리턴해준다.
시:분:초 형식으로 입력된 문자열이 올바른지 검사하는 is_time_valid 메서드를 만드는 것이 목적이다. 시간은 24시까지 분은 59분까지 초는 60초까지 있어야하는 것이 조건이다. is_time_valid 메서드는 그냥 : 기준으로 split을 하고 부등호와 if를 이용해 검사하면 될 것 같다.
class Time:
def __init__(self, hour, minute, second):
self.hour = hour
self.minute = minute
self.second = second
@staticmethod
def is_time_valid(time):
hour, minute, second = map(int,time.split(':'))
if 0 <= hour <= 24 and 0 <= minute <= 59 and 0 <= second <= 60:
return True
return False
@classmethod
def from_string(cls,time):
hour, minute, second = time.split(':')
t = cls(hour, minute, second)
return t
time_string = input()
if Time.is_time_valid(time_string):
t = Time.from_string(time_string)
print(t.hour, t.minute, t.second)
else:
print('잘못된 시간 형식입니다.')
문제만 보고는 간단할 줄 알았지만 주어진 코드와 매칭하며 코딩하니 정적 메서드와 클래스 메서드를 적절히 활용해야지 풀 수 있는 문제였다. 일단 is_time_valid는 클래스에 접근할 필요없이 올바른 형식 여부만 반환해주면 되므로 정적 메서드로 선언했다. 이 안의 조건식은 위의 연습문제와 비슷하니 생략하겠다. from_string이 처음에는 결과 형식으로 되도록 단순 split만 하는줄 알았지만 보면 인스턴스를 생성하는 부분이 없으므로 이 메서드에서 class에 접근해 인스턴스를 생성한 후 반환을 해줘야 되는 것이었다. 이 메서드는 클래스에 접근하므로 클래스 메서드를 줬다. __init__의 매개변수에 맞도록 split을 해준 후 잘라진 값들을 각각 인수로 넣어주니 잘 작동한다.
-클래스 상속-
-클래스 상속이란
클래스 상속이란 클래스가 가진 속성, 메서드 같은 기능들을 물려주는 것이다.
기능을 물려주는 클래스는 기반 클래스, 상속을 받아서 새롭게 만드는 클래스를 파생 클래스라고 한다.
보통 기능을 물려주는 기반 클래스는 부모 클래스나 슈퍼 클래스라고 부르고 파생 클래스는 자식 클래스나 서브 클래스라고 부른다.
위는 코딩도장에 나와있는 클래스 상속의 예시 사진이다. 보면 예시로 생물 분류를 사용했는데, 일단 맨 처음 생물을 부모, 슈퍼 클래스다. 생물이면 누구든지 숨을 쉰다. 식물 조차도 말이다.
이 같이 생물이면 누구나 사용하는 기능인 호흡을 생물에서 구현했다치면 이를 상속받는 자식 클래스인 동물, 식물 또한 숨쉬는 기능을 사용할 수 있다. 또한 동물은 동물만의 기능인 움직이기, 먹기 등이 있으니 동물 클래스에서 움직이기, 먹기를 구현한다. 식물은 광합성을 하므로 식물 클래스에서 광합성을 구현하겠다.
동물 그 아래는 척추동물, 무척추동물이 있는데 이들은 이들만의 특징은 있지만 결국 상속 받은 호흡, 움직이기, 먹기의 기능이 필요로하고 추가로 이들만의 특징적인 기능이 필요하다. 꽃식물, 민꽃식물 역시 호흡, 광합성 기능을 필요로 하고 그 외의 특징적인 기능이 필요하다.
이처럼 보면 생물이라는 큰 틀 아래 중복적으로 사용하는 특징, 기능들이 있는데 이를 생물이라는 클래스에서 구현하고 그 식물, 동물 등에 상속하면 이 기능, 특징들을 구현할 필요 없이 동물, 식물에 해당하는 기능만 새롭게 만들면 된다. 만약 상속을 사용하지 않는다면 중복해서 여러번 구현해야할 것이므로 비효율적이다. 상속은 기존 기능을 재사용 가능하기 때문에 효율적이므로 상속을 사용한다.
-클래스 상속
클래스 상속은 class <클래스명>(<상속할 클래스명>)으로 할 수 있다.
생물로 클래스 상속을 구현하기는 조금 힘든면이 있어서 rpg 게임처럼 전사, 사제 등의 직업을 대상으로 구현해 보겠다.
위는 attack, defence, dead 기능이 구현돼 있는 Character 클래스를 Warrior, Priest로 상속받은 모습이다. Warrior, Priest에는 attack, defence, dead 기능이 없고 warriorSkill, priestSkill만 구현했다. Warrior와 Priest를 인스턴스를 만들고 호출해보면 상속받은 기능인 attack, defence 등이 호출가능한 것을 볼 수 있다. 또한 당연히 warriorSkill과 priestSkill도 호출 가능하다.
-부모(기반)클래스의 속성 사용
자식 클래스가 부모 클래스에 있는 인스턴스 속성을 사용할 수 있다. 평범한 방법으로 <인스턴스>.<부모 클래스 속성>이렇게 접근한다면 에러가 뜨지만 super를 사용한다면 접근이 가능하다.
-super
super는 부모 클래스의 함수를 __init__ 함수를 호출할 수 있게 해준다.
super().__init__()
위처럼 코드를 짜면 자식클래스에서 부모 클래스의 __init__을 호출한다.
보면 super().__init__()을 __init__에 써준 Warrior는 인스턴스를 생성할 때 Character의 __init__또한 같이 호출하여 hp 속성을 사용할 수 있지만, 이를 안해준 Priest에서 hp에 접근하면 오류가 뜨는 것을 볼 수 있다.
자식클래스에서 속성을 찾는 과정은 다음과 같다.
해당 클래스에 속성이 있는지 -> 있다면 접근, 없다면 super().__init__으로 부모 클래스의 __init__이 호출됐는지 확인한다. -> 호출 됐다면 부모 클래스에서 속성을 찾고, 없다면 에러가 뜬다.
만약 자식클래스에 __init__을 안써줬다면 부모 클래스의 __init__도 자식클래스의 인스턴스가 생성될 때 호출되므로 super를 사용할 필요가 없다.
위에서 에러가 떳던 Priest 클래스에 __init__을 없애주니 정상적으로 작동되는 것을 볼 수 있다.
-overriding
오버라이딩은 자식 클래스에서 부모 클래스에 존재하는 함수를 재정의 하는 것이다. 예를 들어 부모 클래스에 hello world를 출력하는 print 함수가 있다 칠때 자식 클래스에서 오버라이딩을 해 함수 내용을 print("overrided")로 바꿔준다면 자식 클래스의 인스턴스에서 해당 함수를 호출하면 hello world가 아닌 overrided가 출력이 나온다.
이 오버라이딩은 작동 기능이 같은 메서드 이름으로 계속 사용될 때 오버라이딩을 사용한다. 예를 들어 rpg 게임의 캐릭터에 attack 함수를 만들어 print("일반 공격")을 써놨다 할 때, 마법사 같은 경우 일반 공격이 아닌 마법 공격을 한다. 이름을 바꿔서 구현하는 것은 혼동을 주거나 보기 불편할 수 있으므로(주관적인 생각이다) 이 attack 함수를 오버라이딩해 print("마법 공격")으로 바꿔주는 것이다.
혼동, 불편한 점 외에도 메서드를 재활용 할 수 있기 때문에 중복을 줄일 수 있다는 점도 장점이다.
사용방법은 간단하다. 그저 오버라이딩할 함수의 이름을 자식 클래스에 그대로 써준 후 새로운 코드를 작성하면 된다.
보면 attack 함수가 오버라이딩 돼 마법 공격이 출력된 것을 알 수 있다.
오버라이딩 되기 전 기능을 하고 싶다면 앞서 부모의 init을 호출할 때 사용했던 super()를 사용하면 된다.
위의 경우 super().attack()을 사용하면 된다.
-issubclass
issubclass는 상속관계를 확인할 때 쓰인다. issubclass(<자식클래스>,<부모클래스>)로 사용하면 서로 부모 자식 관계인지 여부를 확인해 True, False로 값을 리턴한다.
앞서 만든 Warrior와 Priest를 Character와 부모 자식 관계를 issubclass로 확인해보면 True가 나온다.
-상속 관계, 포함 관계-
-상속 관계
위에서 만든 Warrior 클래스는 결국 게임 내 Character이므로 부모 클래스 Character와 같은 종류다. 상속 관계는 이처럼 같으 종류고 동등한 관계일 때 사용한다. 쉽게 체크하는 방법은 "전사는 캐릭터다."라고 했을 때 말이 되면 동등한 것이다. 위 클래스 상속에서 정리했던 생물로 예를 또 들어보면 "식물은 생물이다", 말이 되는 것을 보니 동등한 관계다. 이런 상속 관계를 영어로 하면 is-a로 표현 가능하다. 전사 is a 캐릭터
-포함관계
코딩 도장에 나온 것처럼 전사 클래스가 아니라 캐릭터 목록을 관리하는 클래스를 만든다 치면 동일한 기능, 속성이 없으므로 상속을 받을 필요 없이 인스턴스를 가져와서 목록에 추가하도록 구현하면 된다.
보면 캐릭터 인스턴스를 가져와서 캐릭터 목록에 포함하므로 이를 포함관계라고 부른다. 쉽게 체크하는 방법은 "캐릭터 목록은 캐릭터를 가지고 있다."가 말이 된다면 포함 관계다. 포함 관계를 영어로 하면 has-a로 표현한다. 캐릭터 목록 has a 캐릭터
정리하자면 상속 관계는 상속 사용, 포함 관계는 상속 사용 대신 인스턴스를 넣어준다.
-다중 상속-
위에서는 클래스 하나당 한 클래스만 상속을 받았었는데, 다중 상속은 여러 부모 클래스로부터 상속을 받을 수 있다.
다중 상속 방법은 class <클래스명> (<부모클래스1>,<부모클래스2>....) 이렇게 ','로 구분해서 넣어주면 된다.
위 코드처럼 여러개의 클래스를 부모 클래스로 상속 받을 수 있다. 물론 클래스들의 메서드, 속성등을 상속받았기에 부모클래스들의 메서드, 속성등에 접근 가능하다.
-다이아몬드 상속
class A:
def sayHello(self):
print('A : 안녕')
class B(A):
def sayHello(self):
print('B : 안녕')
class C(A):
def sayHello(self):
print('C : 안녕')
class D(B, C):
pass
cls1 = D()
cls1.sayHello()
위는 코딩 도장의 예제코드를 조금 바꿔서 실습한 코드다. 이 경우 상속 관계가
위 사진처럼 모양이 다이야몬드 같아서 다이아몬드 상속이라 부른다.
-메서드 탐색 순서
위의 다이아몬드 상속 코드의 D가 sayHello를 호출한다면 어떻게 될지 의문이 든다. 왜냐하면 sayHello는 총 3개가 존재하기 때문이다. 이때 mro라는 함수를 사용하면 메서드의 탐색 순서를 알 수 있다.
사용은 <클래스명>.mro()로 하면 된다.
클래스 D의 mro 결과를 보면 먼저 class D -> class B -> class C -> class A 순으로 메서드 탐색이 되는 것을 볼 수 있다. 따라서 sayHello를 호출하면 메서드 탐색 순서가 가장 높은 B에서 호출이 되는 것이다. 결과를 보면 B : 안녕이 출력된다.
mro 결과를 보면 class A 뒤에는 object라는게 존재하는데, 이 object는 모든 클래스의 조상이라 한다. 어느 클래스든 항상 이 object를 부모 클래스로 갖는다.
-추상 클래스-
추상 클래스는 메서드 목록만 가진 클래스를 말한다. java에서 interface에 해당한다. 추상 클래스로 만들면 상속 받는 클래스는 추상 클래스의 메서드 목록의 메서드들을 반드시 구현해야한다.
추상 클래스를 만드는 방법은 먼저 abc 모듈을 임포트 해야한다. 간결하게 작성하기 위해 from abc import *로 임포트 하면 된다.
class <클래스명> (metaclass=ABCMeta):
@abstractmethod
def <메서드명>(self):
<코드>
임포트 후 위처럼 작성하면 추상 클래스가 만들어진다.
위 사진처럼 추상 클래스 CharacterBase를 작성한 후, Warrior 클래스는 attack을 구현해주고, Archor 클래스는 attack을 구현해 주지 않았다. 실행해보면 Warrior 인스턴스는 잘 작동하지만 Archor는 attack 구현을 안해줬기에 오류가 뜨는 것을 볼 수 있다.
추상 클래스는 인스턴스로 만들 수 없다. 만드려 시도하면 에러가 발생한다. 추상 메서드에 pass로 빈 코드를 작성한 이유는 애초에 인스턴스로 만들 수 없기에 추상 메서드를 호출할리가 없기 때문이다.
결국 추상 클래스는 상속에만 사용한다는 것을 알 수 있다.
상속 받는 방법은 class <클래스명> (<부모 클래스>): 로 작성하면 된다. 이 문제에서는 class Student(Person):로 작성하면 된다.
답 : c
기반(부모)클래스의 메서드 호출 방법은 super().<메서드명>()이다. 이 문제는 __init__ 메서드 호출이므로 super().__init__()으로 작성하면 된다.
답 : d
a : 메서드 오버라이딩은 해당 함수의 이름과 똑같이 작성한 후 오버라이딩 해야한다. X
d : super()를 사용하면 호출할 수 있다. X
추상 클래스는 abc를 임포트한 후 만들 수 있는데 이때 import abc만 써주면 metaclass=abc.ABCMeta를 괄호에 써줘야하고 from abc import *를 써주면 ""=ABCMeta만 써주면 된다. 클래스 정의 형식은 class PersonBase(metaclass=abc.ABCMeta 또는 metaclass = ABCMeta)다.
답 : b,e
리스트 자료형도 결국 클래스이므로 list 클래스를 상속 받아서 replace 함수를 새롭게 만들면 될 것 같다.
class AdvancedList(list):
def replace(self, past,future):
returnList = []
for i in self:
if i == past:
returnList.append(future)
else:
returnList.append(i)
for i in range(len(returnList)):
self[i] = returnList[i]
x = AdvancedList([1, 2, 3, 1, 2, 3, 1, 2, 3])
x.replace(1, 100)
print(x)
list를 상속 받아서 replace라는 함수를 추가했다. 동작은 리스트형 returnList를 하나 만들고 for문으로 past와 i가 같을 때 returnList에는 future가 append되게하고 아닐 경우에는 그대로 append 시켰다. 만들고 self = returnList로 써줬지만 이상하게 복사가 안되길래(copy까지 써봤다.) 그냥 for문을 한번 더 돌려서 인덱스를 지정 한 후 값을 넣어줬다.
Animal, Wing을 다중 상속 받아 새 클래스 Bird를 작성하여 '먹다', '파닥거리다', '날다' 그리고 issubclass를 이용한 True,True를 출력하게 만드는 것이 목적이다. 간단한 문제인 것 같다. Bird 클래스를 작성하여 2개 클래스를 상속받고 새로운 메서드 fly를 '날다'가 출력되게 작성하면 끝일 것 같다.
class Animal:
def eat(self):
print('먹다')
class Wing:
def flap(self):
print('파닥거리다')
class Bird(Animal, Wing):
def fly(self):
print('날다')
b = Bird()
b.eat()
b.flap()
b.fly()
print(issubclass(Bird, Animal))
print(issubclass(Bird, Wing))
굉장히 쉬운 문제였다. 설명할 것도 없이 그저 다중 상속 받는 Bird 클래스를 만들고 fly 메서드를 만들어 날다가 출력되는 코드를 작성한게 끝이다.
-두 점 사이의 거리 구하기-
평면에서의 점은 위치 값 x, y를 가진다. 이를 리스트로도 표현 가능하겠지만, 햇갈릴 수 있기 때문에 class로 만들겠다.
위처럼 Position2D를 만들어 x,y 값을 속성으로 담았다.
평면에서 보면 pos1과 pos2의 위치는 위 사진처럼 되어 있다.
이 두 점 사이의 거리를 구하는 방법은 간단하다. 중학교 시절에 배운 피타고라스의 정리를 이용하면 된다.
a^2 + b^2 = c^2
위 식이 피타고라스의 정리식이다. 여기서 a가 pos2.x - pos1.x, b가 pos2.y - pos1.y에 해당하는데 이 두식의 결과를 더한 값이 두 점 사이의 거리 제곱이다.
위 식을 통해 구해진 (pos2.x - pos1.x)^2 + (pos2.y - pos1.y)^2를 루트 씌운다면 c를 구할 수 있다. 파이썬에서는 math 모듈의 sqrt 함수로 제곱근을 구할 수 있다.
사용은 sqrt(<값>)으로 하면 된다. 이 값에 위의 식의 결과 값을 넣어주면 된다.
위처럼 math.sqrt 안에 식을 넣으니 거리가 구해진 것을 볼 수 있다.
여기서 제곱 또한 간단하게 math 모듈의 pow 함수로 구할 수 있다.
사용 방법 : pow(<값>,<지수>)
pow 함수를 사용하니 좀더 간결하게 코드가 작성됐다. 결과 역시 동일하다.
pow 말고도 파이썬 연산자 **또한 사용할 수 있다.
**를 사용한다면 식이 math.sqrt((pos2.x - pos1.x)**2 + (pos2.y - pos1.y)**2)가 될 것이다.
위에서는 po2 - pos1으로 했지만 pos1에서 pos2를 빼도 상관 없다. 왜냐하면 제곱을 해주기 때문에 결국 결과값은 같아지기 때문이다.
절대 값을 반환해주는 함수도 있다.
내장 함수 abs, math 모듈 fabs가 있는데 abs는 정수를 인수로 주면 정수로 반환, 실수로 주면 실수로 반환되지만 fabs는 무조건 실수로 반환해 준다.
-nametuple
각 요소에 이름을 지정해 주는 튜플이다. collections를 임포트하면 사용할 수 있다. 이 nametuple을 사용하면 자료형 이름을 클래스명 요소를 속성으로 클래스를 생성해주는 독특한 자료형이다. 사용은 클래스 = collections.namedtuple(<클래스 이름>, ['<속성 이름>', '<속성 이름2>']....)로 하면 된다.
사각형의 넓이는 가로의 길이 * 세로의 길이이다. 따라서 가로의 길이와 세로의 길이를 구한 후 곱해주면 될 것 같다.
class Rectangle:
def __init__(self, x1, y1, x2, y2):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
rect = Rectangle(x1=20, y1=20, x2=40, y2=30)
x = abs(rect.x1-rect.x2)
y = abs(rect.y1-rect.y2)
area = x*y
print(area)
나는 abs 함수를 이용해 x1-x2의 절대 값, y1-y2의 절대 값을 각각 x,y 변수에 넣어주고 area에 x*y를 넣어 사각형의 넓이를 구했다.
x,y 좌표 4개가 입력되는데, 이 점 4개는 1~2, 2~3, 3~4가 각각 선으로 연결돼 있다고 한다. 목적은 이 1~4까지 연결된 선의 길이를 구하는 것이다. 그냥 두 점 사이의 거리 구하기를 응용하면 될 것 같다.
import math
class Point2D:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
length = 0.0
p = [Point2D(), Point2D(), Point2D(), Point2D()]
p[0].x, p[0].y, p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y = map(int, input().split())
for i in range(3):
length += math.sqrt((p[i].x - p[i+1].x)**2 +(p[i].y - p[i+1].y)**2)
print(length)
for문을 3번 반복해서 선의 길이를 담는 length에 p[i], p[i+1]의 거리를 계산해서 더해줬다. 4번이 아닌 3번 반복하는 이유는 p[i+1]에 접근해서 인덱스 범위가 넘치지 않게 하기 위해서고 또한 마지막 인덱스에서는 더 이상 구할 길이가 없기 때문이다.
'Old (2021.01 ~ 2021.12) > Programming' 카테고리의 다른 글
생활코딩 HTML 버튼 ~ meta (0) | 2021.02.09 |
---|---|
생활코딩 HTML 기술소개 ~ 선택 (0) | 2021.02.07 |
파이썬 코딩도장 Unit 23 ~ 26 (0) | 2021.01.27 |
파이썬 코딩도장 Unit 20 ~ 22 (0) | 2021.01.26 |
파이썬 코딩도장 Unit 12 ~ 19 (0) | 2021.01.24 |