✍🏻11일 공부 이야기.
원래는 강의를 들으면서 notion에 간단히 적고 여기에 다시 정리해서 적었었는데.. 몇 번에 걸쳐보니.. 똑같은 내용을 계속 적는 것 같아서.. 지금 내용은 강의를 들으며 바로바로 정리해보았다:) 음.. 시간이 단축이 되나..? 어짜피 또 다시 훑으면서 내용도 덧붙이고 해야해서 드라마틱하게 큰 차이가 나진 않을 것 같다ㅋㅋ
다들 어떻게 공부하시나요??👀👀
이전 글에 변수는 '데이터가 저장되어있는 메모리 공간이다' 라고 말한 적이 있는데 그것과 이어지는 내용이다.
myCar = Car() #Car라는 클래스를 통해 객체 선언
라고 객체를 선언했다면 컴퓨터 내에 객체가 저장되어있고 그 객체가 있는 메모리 주소를 변수 myCar
에 저장하여 객체를 참고하는 것이다.
객체 주소를 복사하는 것으로 객체 자체는 복사되지 않는다.
객체 자체를 복사하는 것으로 또 하나의 객체가 만들어진다.(두 객체의 주소는 다르다.)
아래 예제를 통해 더 알아보자.
class TemCls:
def __init__(self, n, s):
self.number = n
self.str = s
def printClsInfo(self):
print(f'self.number : {self.number}')
print(f'self.str : {self.str}')
#얕은 복사
tc1 = TemCls(10, "Hello")
tc2 = tc1
tc1.printClsInfo()
tc2.printClsInfo()
#tc2의 속성을 변경했지만 실은 tc2에 저장된 메모리 주소의 속성을 변경한 것이므로
tc2.number = 3.14
tc2.str = "Bye"
tc1.printClsInfo() #동일한 주소를 공유하고 있는 tc1의 속성도 변경됨
tc2.printClsInfo()
#깊은 복사
import copy #객체를 복사할 수 있음
tc1 = TemCls(10, "Hello")
tc2 = copy.copy(tc1) #tc1과 tc2는 완전히 다른 객체
#속성을 복사했기 때문에 처음엔 속성값들이 똑같지만
tc1.printClsInfo()
tc2.printClsInfo()
#tc2의 속성을 변경해주면
tc2.number = 3.14
tc2.str = "Bye"
#tc1의 속성은 그대로지만, tc2의 속성은 변경된 값임을 볼 수 있음
tc1.printClsInfo()
tc2.printClsInfo()
실제로 위 실행 파일을 한 번에 돌리면 아래와 같이 출력되지는 않겠지만, 좀 더 보기 편하게 얕은 복사와 깊은 복사를 구분해서 적어뒀다!
얕은 복사
💻출력
self.number : 10
self.str : Hello
self.number : 10
self.str : Hello
self.number : 3.14
self.str : Bye
self.number : 3.14
self.str : Bye
깊은 복사
💻출력
self.number : 10
self.str : Hello
self.number : 10
self.str : Hello
self.number : 10
self.str : Hello
self.number : 3.14
self.str : Bye
리스트를 복사하는 방법은 여러가지가 있는데 궁금하신 분들은 토글 클릭!🖱️
파이썬 리스트를 복사하는 방법scores = [1,2,3]
scoresCopy = []
얕은복사
scoresCopy = scores
깊은복사
for s in scores:
scoresCopy.append(s)
scoresCopy.extend(scores)
import copy
scoresCopy = scores.copy()
scoresCopy = scores[:]
scoresCopy = list(scores)
scoresCopy = [] + scores
다시 공식 홈페이지를 보니 아래와 같이 나와있다.
(🖱️그림을 클릭하면 공식 홈페이지로 이동해요!)
앞서 깊은 복사..로 사용하던 copy가 실은 얕은 복사라고..?!?!??? 다들 멘붕이실거다.🤯
얘기를 시작하면 많이 복잡하긴 하다. 그래도 쉽게 설명하자면 scores = [1,2,3]
와 같은 1차원 리스트는 copy()로도 깊은 복사가 가능한데, scores = [[1,2],[2,3]]
과 같이 2차원 이상의 리스트는 copy()를 하면 얕은 복사가 된다는 점이다.
왜 그렇게 되는지를 설명하려면..
mutable객체와 immuatable객체를 알아야한다.
위의 개념은 🖱️이곳을 클릭해서 참고하시구
결론은 mutable 객체(리스트, 딕셔너리 등)들은 deepcopy()를 해줘야 제대로 된 깊은 복사가 된다는 점이다.
🖱️이 사이트가 이해도 잘 되고 깔끔하게 잘 정리해둬서 같이 첨부한당 🥰
또 다른 클래스를 내 것처럼 사용할 수 있게 하는 것을 상속이라 한다.
class 클래스명1:
def 기능명1():
def 기능명2():
class 클래스명2(클래스명1):
def 기능명3():
def 기능명4():
과 같은 형태로 클래스명2 는 기능명1 ~ 기능명4까지의 기능을 모두 사용할 수 있게 된다.
상속의 기능을 사용하는 방법은
객체명 = 클래스명2(인수들)
로 객체를 선언하고 객체명.기능명1()
처럼 기존 클래스를 사용하는 것과 동일하다.
class Calculator:
def __init__(self):
print('[Calculator] __init__() called')
cal = Calculator()
💻출력
[Calculator] __init__() called
#1
class Calculator:
def __init__(self, n1, n2):
print('[Calculator] __init__() called')
self.num1 = n1
self.num2 = n2
cal = Calculator(10,20)
print(f'cal.num1 : {cal.num1}')
print(f'cal.num2 : {cal.num2}')
#2
class Calculator:
def __init__(self):
print('[Calculator] __init__() called')
self.num1 = 10
self.num2 = 20
cal = Calculator()
print(f'cal.num1 : {cal.num1}')
print(f'cal.num2 : {cal.num2}')
#3
class Calculator:
def __init__(self, n1):
print('[Calculator] __init__() called')
self.num1 = n1
self.num2 = 20
cal = Calculator(10)
print(f'cal.num1 : {cal.num1}')
print(f'cal.num2 : {cal.num2}')
위 #1, #2, #3은 모두 아래와 같이 출력된다.
💻출력
[Calculator] __init__() called
cal.num1 : 10
cal.num2 : 20
상위 클래스의 속성을 초기화하기 위해 사용된다.
__init__ 을 호출해야 속성을 초기화할 수 있으므로 상위클래스의 __init__을 호출하는 방법 중 하나이다.
class P_Class:
def __init__(self, pNum1, pNum2):
print('[P_Class] __init__() called')
self.pNum1 = pNum1
self.pNum2 = pNum2
class C_Class:
def __init__(self, cNum1, cNum2):
print('[C_Class] __init__() called')
##상위 클래스 속성 초기화해주기##
super().__init__(cNum1, cNum2)
#같은 의미로 P_Class.__init__(self, cNum1, cNum2) 도 사용 가능하다.
self.cNum1 = cNum1
self.cNum2 = cNum2
💻출력
[C_Class] __init__() called
[P_Class] __init__() called
super().__init__(cNum1, cNum2)
가 없었다면 상위 클래스의 속성은 초기화되지 않아 아래와 같이 출력되었을 것이다.
💻출력
[C_Class] __init__() called
여러 개의 클래스를 상속받을 수 있다.
class 클래스1():
class 클래스2():
class 클래스3():
#다중상속
class 클래스0(클래스1 , 클래스2, 클래스3):
위와 같은 형태로 ,
를 구분자로 상속받을 클래스명들을 나열해주면 다중 클래스도 상속받을 수 있다.
하지만 상속받을 클래스에 비슷한 코드가 있다면, 개발하는 입장에서 코드가 꼬여 복잡해질 수 있으니 남발하지 않도록 주의가 필요하다!
하위 클래스에서 상속 클래스의 메서드를 재정의(기능을 재정의)하는 것을 오버라이딩이라 한다.
class 클래스1():
def 기능1():
#기능1에 대한 내용
class 클래스0(클래스1):
def 기능1():
#기능1에 대한 내용 재정의
상위 클래스에서 하위 클래스에 메서드 구현(기능 구현)을 강요하는 기능이다. 만약 하위 클래스에서 메서드를 구현하지 못했다면 에러가 뜬다.
#추상클래스 기능을 사용하기 위한 import
from abc import ABCMeta
from abc import abstractmethod
##꼭 작성해줘야 함
class 클래스1(metacalss = ABCMeta):
#하위 클래스에서 메서드 구현을 강요할 기능 위에 작성
@abstractmethod
def 기능1(self):
pass
class 클래스0(클래스1):
def 기능1(self):
#기능1에 대한 내용 구현
예외란, 문법적인 문제는 없으나 실행 중 발생하는 예상하지 못한 문제를 말한다.
프로그램을 짤 때, 예상하지 못한 예외가 프로그램 전체 실행에 영향이 없도록 처리하는 것을 예외 처리라고 한다.
예외 발생 예상 구문을 try ~ except로 감싸주어 예외 처리한다.
n1 = 10
n2 = 0
#ZeroDivisionError가 발생할 수 있는 예외 발생 구문
try:
#정상적으로 작동한다면 다음과 같이 실행되고
print(n1 / n2)
except:
#예외가 발생한다면 다음과 같이 실행됨
print("예상치 못한 예외가 발생했습니다.")
print("다른 프로그램 실행은 문제 없습니다.")
#else:
#예외가 발생하지 않았을 때 실행되는 구문도 else 아래 구현할 수 있음
#else문은 항상 try ~ except와 같이 사용됨
#아래 구문들은 무조건 실행됨
print(n1 * n2)
print(n1 - n2)
print(n1 + n2)
💻출력
예상치 못한 예외가 발생했습니다.
다른 프로그램 실행은 문제 없습니다.
0
10
10
finally 아래에 작성된 코드들은 예외 발생과 상관없이 항상 실행된다.
try:
#정상적으로 작동되어야 하는 부분
inputData = input("input number : ")
numInt = int(inputData)
except:
#int(inputData)를 실행할 때 예외가 발생한다면 아래 코드가 실행됨
print("exception raise")
print("not number")
else:
#정상적으로 int(inputData)기 실행되었다면 아래 코드가 실행됨
if numInt % 2 == 0:
print("even number")
else:
print("odd number")
finally:
#예외가 발생했든 안 했든 무조건 아래 코드는 실행되어 inputData가 무엇이었는지 알려줌
print(f"inputData : {inputData}")
💻출력
input number : 가나다
exception raise
not number
inputData : 가나다
💻출력
input number : 123
odd number
inputData : 123
지금까지는 try ~ except 를 이용해서 개발자가 직접 예외가 발생했을때 실행된 구문들을 작성해주었다. 개발자가 발생될 예외를 미리 예상할 수 있으면 그에 맞게 구문을 작성해주면 되지만 정말.. 예상치도 못한 예외가 발생한다면? 어떤 예외가 발생했는지 알아야한다. 그 때 Exception 클래스를 이용하면 어떤 예외가 발생해서 프로그램의 동작이 멈췄는지 알 수 있다.
num1 = int(input("input number1 : "))
num2 = int(input("input number2 : "))
try:
print(f"num1 / num2 = {num1 / num2}")
except Exception as e: #Exception을 e라 부르겠다
print(f"exception : {e}")
print(f'num1 * num2 = {num1 * num2})
💻출력
input number1 : 10
input number2 : 0
exception division by zero
num1 * num2 = 0
예외를 발생시킬 수 있는 키워드이다.
def divCal(n1, n2):
if n2 != 0:
print(f'{n1} / {n2} = {n1 / n2}')
else:
#강제적으로 예외를 발생시킴
raise Exception('0으로 나눌 수 없습니다.')
#생성한 이 문구 그대로 e에 들어감
num1 = int(input("input number1 : "))
num2 = int(input("input number2 : "))
#try ~ except문을 사용하지 않고 divCal(num1, num2)만 한다면 그냥 에러임.
#하지만 이렇게 작성한다면 프로그램이 예외 처리에 따라 정상 작동한 거임.
try:
divCal(num1, num2)
except Exception as e:
print(f'Exception : {e}')
💻출력
input number1 : 10
input number2 : 0
Exception : 0으로 나눌 수 없습니다.
Exception은 매개변수를 2개 이상 가질 수도 있다.
def sendSMS(msg):
if len(msg) > 10:
#매개변수가 2개 이상도 올 수 있음
#e.args[0], e.args[1]
raise Exception("길이 초과. MMS 전환 후 발송", 1)
else:
print("SMS 발송")
def sendMMS(msg):
if len(msg) <= 10:
raise Exception("길이 미달. SMS 전환 후 발송", 2)
else:
print("MMS 발송")
msg = input("input message : ")
try:
#SMS로 먼저 보내는데,
#길이가 10이하이면 SMS로 발송되고
#길이가 10 초과된다면 Exception이 발생됨.
sendSMS(msg)
except Exception as e:
print(f'e0 : {e.args[0]}')
print(f'e1 : {e.args[1]}')
if e.args[1] == 1:
#MMS로 발송됨
sendMMS(msg)
elif e.args[1] == 2:
sendSMS(msg)
💻출력
input message : 안녕
SMS 발송
💻출력
input message : 안녕하세요. 테스트 중입니다.
e0 : 길이 초과. MMS 전환 후 발송
e1 : 1
MMS 발송
Exception 클래스를 상속해서 사용자 예외 클래스를 만들 수 있다.
관리자 암호가 'admin1234'라 했을때, 비밀번호를 입력하고 다음 상태에 따라 예외 처리하는 예외 클래스를 만드는 프로그램을 완성해라.
#사용자 예외 클래스
class PasswordLengthShortException(Exception):
def __init__(self, str):
super().__init__(f'{str} : 길이 5미만')
class PasswordLengthLongException(Exception):
def __init__(self, str):
super().__init__(f'{str} : 길이 10 초과')
class PasswordWrongException(Exception):
def __init__(self, str):
super().__init__(f'{str} : 잘못된 비밀번호')
cnt = 0 #로그인 횟수 변수
while True:
try:
admin_pw = input('input admin password : ')
if len(admin_pw) < 5:
raise PasswordLengthShortException(admin_pw)
elif len(admin_pw) > 10:
raise PasswordLengthLongException(admin_pw)
elif admin_pw != "admin1234": #관리자 비밀번호가 admin1234라 했을때
raise PasswordWrongException(admin_pw)
elif admin_pw == "admin1234":
print("환영합니다. 관리자님")
break
except PasswordLengthShortException as e_short:
print(e_short)
except PasswordLengthLongException as e_long:
print(e_long)
except PasswordWrongException as e_wrong:
print(e_wrong)
finally:
cnt += 1
if cnt == 5: #로그인 기회를 5번 틀리면 빠져나오게 함
print("로그인 기회 5번을 모두 소진하셨습니다. 다음에 시도해주세요.")
break
강의에서 로그인 시도 횟수도 추가하면 좋겠다는 말씀을 듣고
로그인 시도 횟수를 5회로 지정하여 5번 틀리면
"로그인 기회 5번을 모두 소진하셨습니다. 다음에 시도해주세요." 문구가 나오고 종료되게끔 프로그램을 짜보았다:)
💻출력
input admin password : 1234
1234 : 길이 5미만
input admin password : 123456
123456 : 잘못된 비밀번호
input admin password : 1234567890123
1234567890123 : 길이 10 초과
input admin password : admin
admin : 잘못된 비밀번호
input admin password : admin12
admin12 : 잘못된 비밀번호
로그인 기회 5번을 모두 소진하셨습니다. 다음에 시도해주세요.
💻출력
input admin password : admin1234
환영합니다. 관리자님
저어번 글인가? 문제에 딱히 말이 없어도 예외 처리를 하게 되는 습관을 들이고 있다고 말한 적이 있다. 어우.. 이번에 예외 처리에 대해 다시 또 상기시키고 나니... 지금까지 내가 했던 예외 처리는 예외 처리가 아닌 것 같은 느낌이 들었다ㅋㅋㅋㅋ
이 시간 이후부로는 try ~ except 문을 활용해서 예외 처리하는 습관을 들여보도록 하지 👊🏻👊🏻
점점 코드들의 길이가 길어지고 있다.. 예전부터 주석으로 지금 이 코드는 무엇을 의미하는지, 앞으로는 어떤 처리를 하는 코드를 짜야하는지 등등을 적긴했었는데.. 그나마 이런 습관들이 있었기에 다음 번에 코드를 다시 봐도 어렴풋이나마 코드를 빠른 시간 내에 이해할 수 있었었다.
이전 시간까지만 해도 코드들이 짧아서 주석 처리를 별로 하지 않았었는데 앞으로는 평소대로 주석도 꼼꼼히 써야겠다🥰🥰