-Summary-
try except else finally 예외처리, iterater-iteraterble, 정규 표현식
02-12 (Unit 38.1 ~ Unit 38.7)
-exception(예외) 처리-
예외 처리란 예외가 발생했을 때, 실행을 중지하지 않고 계속 실행하게 해주는 것이다.
-exception(예외)
예외란 코드를 실행하는 도중에 발생한 에러를 뜻한다.
첫 번째 2/0은 ZeroDivisionError(0으로 나눠져서 생기는 에러) exception을 일으키고, 두 번째는 ValueError exception을 일으킨다.
ZeroDivisionError, ValueError 등 실행 중에 발생하는 에러는 모두 예외에 해당된다.
-try, except
try, except는 예외 처리를 해주는 역할을 한다.
try:
(실행할 코드)
except:
(예외가 발생할 경우 실행하는 코드)
위처럼 사용한다. 만약 try의 코드에서 예외가 발생한다면 즉시 except의 코드를 실행해주고, 만약 예외가 발생하지 않는다면 except 코드는 실행하지 않는다.
ZeroDivisionError가 일어났던 계산식인 2/0을 print 하도록 하고 그다음 '예외 없음'을 출력해주는 print문을 적었다. 결과를 보면 2/0 결과 값과 '예외 없음'이라는 문구는 출력되지 않고, '예외 발생'만 출력됐다. 이 2/0을 연산할 때 예외가 발생하자마자 try의 나머지 코드들을 실행하지 않고, 바로 except의 코드만 실행하는 것이다.
당연하지만 2/0을 예외를 발생하지 않는 식(2/2 같은)으로 바꿔준다면 except 코드는 실행되지 않고, 대신 '예외 없음'이 출력된다.
위에서는 어떤 예외든 발생하기만 하면, 바로 except 코드로 넘어갔는데, except 뒤에 특정 예외 이름을 적어준다면, 해당 예외가 발생했을 때만 except가 실행되도록 할 수 있다.
위 사진처럼 except 뒤에 2/0을 실행할 때 발생하는 예외인 ZeroDivisionError를 써주니, 해당 에러가 발생할 때 except 코드를 실행해 주는 것을 볼 수 있다.
반대로 ZeroDivisionError를 except에 지정한 상태에서 ValueError를 주니 except 코드가 실행되지 않고, 에러를 발생시키는 것을 볼 수 있다.
위 사진처럼 except에 특정 예외를 지정해줬을 경우에는 위처럼 여러개의 except를 써줄 수 있다.
-except as
except (예외명) as (변수명)로 써준다면 발생한 예외의 에러 메시지를 변수로 받아올 수 있다.
위처럼 as e로 ValueError의 에러문구를 받아와서 출력하는 것을 볼 수 있다. 이 as 뒤에는 원하는 변수명을 써주면 되는데, 보통 exception의 e를 따서 as 뒤의 변수명을 e로 짓는다고 한다.
참고로 except as (변수명) 이렇게 예외명 없이 에러 메시지를 받아올 수 있나 시도해 보면, 예외명 없이는 안된다고 뜨는데, 이는 except 뒤에 exception을 써준다면 특정 예외명 없이 에러 메시지를 받아올 수 있다.
except Exception as (변수명)
ValueError, ZerorDivisionError 등 예외들은 클래스 상속으로 구현된다. 파이썬에서 새로운 예외를 만들 때도 Exception을 상속받아서 구현한다고 한다.
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
위는 파이썬 홈페이지에 나와있는 Exception 계층도의 일부다.
-else
else는 예외가 발생하지 않을 경우 실행하는 기능을 한다.
try:
(실행할 코드)
except:
(예외 발생 시 실행할 코드)
else:
(예외가 발생하지 않을 시 실행할 코드)
위처럼 사용한다. 만약 try의 코드에서 예외가 발생한다면 발생한 즉시 except 코드를 실행하고, try안의 모드 코드에서 예외가 발생하지 않는다면 else의 코드를 실행한다.
위 사진처럼 try의 코드에 예외가 발생하지 않도록 작성하니, try의 코드를 실행한 후, else의 코드를 실행하는 것을 알 수 있다.
-finally
finally는 예외 발생 여부와 상관 없이 무조건 실행하는 기능을 한다.
try:
(실행할 코드)
except:
(예외 발생 시 실행할 코드)
else:
(예외가 발생하지 않을 시 실행할 코드)
finally:
(무조건 실행할 코드)
위처럼 사용한다. 예외가 발생한다면 except, 발생하지 않는다면 else가 실행되겠지만, 항상 마지막에는 finally의 코드가 실행된다.
위 else 실습 코드에 finally를 넣으니 else 실습 코드의 결과 + finally의 '예외처리 완료'가 결과로 출력되는 것을 볼 수 있다.
참고로 try는 if문이나 method 같이 스택프레임을 만드는게 아니라서, try 안에서 선언된 변수는 try 밖에서도 사용이 가능하다.
-raise
raise는 예외를 발생시켜주는 역할을 한다. 처음에는 왜 예외를 발생시켜주는지 의문이 들었지만, 생각해보니 프로그램이 의도대로 동작하지 못하게 하는 값이 입력되면 예외를 처리해줄 필요가 있기 때문에 이와 같은 상황에, raise를 사용하는 것 같다.
raise <예외명 혹은 Exception>('<에러 문구>')
위처럼 사용하면 된다.
위처럼 raise를 써주니 의도적으로 예외를 발생시킬 수 있고, 또한 예외 문구도 정할 수 있는 것을 볼 수 있다.
raise 뒤에 특정 예외명을 적는다면, 위 사진처럼 해당 예외를 발생시켜준다.
이 raise는 꼭 try 코드 내에서만 사용하는 것이 아니다. 실습 결과 try, except 같은 예외처리가 없는 경우에는, 아래 사진처럼 그냥 프로그램을 중단하고 에러 문구를 보여준다.
위 사진처럼 중첩 예외처리, 중첩 예외 발생 또한 가능하다. 1행의 try 안, 2행의 try에서 발생하는 예외는 4행의 except 코드를 실행시키고, 4행의 except 코드 안의 예외 발생은 7행의 except 코드를 실행 시킨다.
-assert
assert도 raise와 비슷한 역할로 예외를 발생시켜주는데, raise와 다르게 assert에 지정된 조건식이 거짓이라면 AssertionError라는 예외를 발생시킨다.
사용은 assert <조건식>로 하면 되는데, 만약 에러 문구를 지정해주고 싶다면 assert <조건식>, <'에러문구'>로 써주면 된다.
if a == 0:
raise Exception('a가 0이면 안됩니다.')
위 조건식을 assert를 사용해서 간결하게 바꿔보겠다.
assert는 참일 경우 넘어가고 거짓일 경우 예외를 발생시키므로, ==이 아닌 !=을 주어 a가 0일 경우 예외가 발생하도록 작성했다. 에러 문구는 'a가 0입니다.'를 넣어줬다.
-사용자 정의 예외
사용자 정의 예외란 프로그래머(코드 작성자)가 직접 만든 예외를 말한다.
앞서 새로운 예외를 받을 때, Exception을 상속받는다고 했는데, 사용자 정의 예외도 Exception을 상속받아 원하는 예외 클래스를 만들어주면 된다. 그리고 에러 메시지는 부모 클래스(Exception)의 __init__을 호출하면서 인자로 넣어주면 된다.
먼저 Exception을 상속받는 사용자 정의 예외로 쓰일 AisZeroError를 만들었다. 이 클래스의 __init__ 메서드에 부모 클래스의 __init__을 호출할 수 있도록 앞서 코딩도장에서 배운 super()를 써줘 Exception의 __init__을 호출하는데 이 인자로 원하는 에러 문구를 넣었다.
그리고 try, except 예외처리문을 통해 AisZeroError를 발생시키니, except에서 AisZeroError의 에러 문구가 출력되는 것을 볼 수 있다.
위에서는 __init__함수를 만들어 에러 문구를 지정해줬지만, 클래스만 만들고 pass를 써줘 아무것도 구현해주지 않아도 예외가 만들어진다. 이럴 경우 에러 문구가 없기 때문에, raise AisZeroError('에러 발생') 이런식으로 에러 문구를 따로 지정해줘야한다.
b : 예외는 try 안의 코드에서 발생할 경우 처리할 수 있다.
e : finally는 예외 발생 여부와 상관없이 무조건 실행한다.
여담으로 d 또한 except, else에서도 예외가 발생하지 않으면 else가 실행된다길래 말이 이상해 틀린 답이라 생각했지만, 결과를 보니 틀린 답이 아니었다.
예외는 raise <예외명 또는 Exception>, assert <조건식>으로 발생시킬 수 있다.
답 : c,d
주어진 코드는 NotEvenNumberError라는 사용자 정의 예외를 만들고, 에러 문구를 '짝수가 아닙니다.'로 정한다.
사용자 정의 예외를 발생시키는 방법은 간단한데, 예외를 발생시켜주는 rasie 뒤에 NotEvenNumberError를 써주면 된다.
답 : c
앞서 배운 파일 입출력과 예외처리를 응용하는 문제다. maria.txt 파일을 읽어오는데, 만약 maria.txt라는 파일이 없다면 '파일이 없습니다'를 출력, 파일이 있다면 파일의 내용을 출력하는 것이 목적이다.
try:
file = open('maria.txt', 'r')
except FileNotFoundError:
print('파일이 없습니다.')
else:
s = file.read()
file.close()
먼저 try를 써줘서 예외가 발생할 수 있는 코드를 넣어줬고, except는 파일이 없을 때 발생하는 예외인 FileNotFoundError를 써줬다. 생각해보면 그냥 except를 써줘도 상관은 없겠지만, 그럴경우 FilleNotFoundError가 아닌 다른 예외 또한 print('파일이 없습니다.')로 처리되므로, FileNotFoundError를 지정해줬다.
만약 예외가 없다면 파일을 읽어서 내용을 출력하는 것이 목적이므로, 예외가 없을 때 실행되는 else를 써줬다.
회문이 아닐 경우 발생하는 사용자 지정 예외 NotPalindromeError와 입력 단어를 매개변수로 받아, 회문인지 판별하고 회문이 아닐경우 NotPalindromeError를 발생시키는 함수 palindrome을 작성하는 것이 목적이다.
class NotPalindromeError(Exception):
def __init__(self):
super().__init__('회문이 아닙니다.')
def palindrome(word):
if list(word) != list(reversed(word)):
raise NotPalindromeError
print(word)
try:
word = input()
palindrome(word)
except NotPalindromeError as e:
print(e)
먼저 Exception을 상속받는 NotPalindromeError 클래스를 작성하고 __init__ 메서드에서 super().__init__를 사용해 에러 문구를 '회문이 아닙니다.'로 지정해줬다. 그다음 입력 값 word를 매개변수로 받는 palindrome 함수를 작성하고 입력 값을 리스트로 형 변환 시킨 list(word)와 입력 값을 거꾸로 한 후 리스트로 형 변환 시킨 list(reversed(word))를 비교해서 만약 같지 않다면 NotPalindromeError를 발생시키게 했다. 아닐 경우 단어가 그대로 출력된다.
02-13 (Unit 39.1 ~ Unit 39.7)
-iterator(이터레이터)-
iterator는 값을 차례대로 꺼낼 수 있는 객체다.
for 문으로 특정 횟수만큼 반복시킬 때, 사용했던 것이 바로 range였는데, 사실 이 range는 iterator를 만들어 주는 역할을 했다. 예를들어 range(1,100)을 쓰면 1~99까지의 숫자가 만들어지는게 아닌, 1~99까지의 숫자를 차례대로 꺼낼 수 있는 iterator 객체 하나만 만들어지는 것이다. 이 iterator를 반복자라고 부른다.
만약 iterator 없이 range로 1~99까지 숫자를 한번에 만든다 가정하면, 아주 많은 메모리를 사용하기 때문에, 성능이 안좋아진다. 그래서 iterator만 만들어, 값들이 필요한 시점에 해당 값을 생성해주는 것이다.(예를들어 for i in range(100)로 for문을 100번 돌릴 때, iterator 객체 하나만 range를 통해 만들어지고, 첫 번째 반복에 필요한 0 생성 -> 두 번째 반복에 필요한 1 생성 이렇게 필요할 때 생성하는 것이다.)
이는 데이터 생성을 바로 하는게 아닌, 나중으로 미루는 것인데 이를 지연 평가(lazy evaluation)이라고 한다.
-iterable(반복 가능한 객체)
iterable이란 반복 가능한 객체를 말한다. list나 str, dictionary 등등 지금껏 for i in <변수(객체)>로 쓸 수 있었던 모든 것은 iterable이었다.
iterator와 iterable의 차이는 iterator는 뒤에 정리한 __next__()로 차례대로 요소를 꺼낼 수 있는 객체고, iterable은 요소를 한 번에 하나씩 가져올 수 있는 객체다. 이 iterable에서 뒤에 정리한 __iter__()로 iterator를 얻는 것이다.
해당 객체가 iterable인지 확인하는 방법은 dir 함수를 사용하면 알 수 있다. 이 dir 함수는 앞에서 정리했던 것으로 객체를 인자로 주면 해당 객체가 어떤 변수, 메서드를 가지고 있는지 반환해주는 역할을 한다.
문자열 '1234', 리스트 [1,2,3,4], 딕셔너리 {'1':1,'2':2}를 dir 함수로 확인해 보면 모두 공통적으로 __iter__ 메서드가 들어있는 것을 볼 수 있다.
공통적으로 들어 있는 __iter__를 호출해보면 각각 (자료형)_iterator 이런식으로 iterator가 나오는 것을 볼 수 있다. 딕셔너리 같은 경우 key 값의 iterator로 나온다.
이런식으로 딕셔너리의 키만 가져오는 keys(), 값만 가져오는 values()를 사용하면 keyiterator, valueiterator를 각각 만들 수 있다.
이런식으로 변수로 해당 iterator을 저장할 수 있다.
-__next__()
__next__()는 iterator 객체의 요소를 차례대로 꺼내주는 역할을 한다.
이런식으로 해당 값을 차례대로 꺼내고, 더 이상 요소가 존재하지 않다면 StopIteration 예외를 발생시킨다.
-for와 iterator
def functionFor(ran):
it = list(range(ran)).__iter__()
while True:
try:
i = it.__next__()
except StopIteration:
break
else:
print(i)
functionFor(5)
위 코드는 for문의 원리를 생각해서 while문으로 for i in range(5) : print(i)를 구현해본 것이다. functionFor는 매개 변수로 range의 인자로 들어갈 ran을 받고 이 ran을 이용해 iterator 객체를 만든다. 그리고 try, except 예외처리문으로 StopIteration 예외가 나올 때까지 i = it.__next__(), print(i)를 반복하고, StopIteration이 나오면 반복문을 끝낸다.
이게 내가 생각했던 for문의 원리였다.
코딩도장의 설명을 보니 내가 생각했던 것과 얼추 비슷함을 알 수 있다. range로 iteration를 만들고 매 반복마다 __next__()로 요소들을 하나씩 꺼낸 후 StopIteration 예외가 발생하면 반복을 끝내는 것이다.
지금까지는 반복 가능한 객체에서 __iter__() 메서드로 iterator를 얻은 후 __next__()로 요소를 가져왔는데, 클래스에 __iter__()와 __next__()를 모두 구현하면 iterator를 만들 수 있다고 한다. 이 두 메서드를 모두 가진 객체를 iterator protocol을 지원한다고 말한다.
-시퀀스 객체와 iterable의 차이
요소의 순서가 정해져 있고, 연속적으로 이어져 있다면 시퀀스 객체, 요소의 순서와는 상관없이 요소를 하나씩 꺼낼 수 있으면 iterale이다.
위 사진을 보면 딕셔너리와 세트는 시퀀스 객체가 아님을 알 수 있다. 그 이유는 시퀀스 객체는 요소의 순서가 정해져 있고 연속적으로 이어져 있어야 하는데, 딕셔너리와 세트는 요소(키)의 순서가 정해져 있지 않기 때문이라고 적혀 있지만 잘 이해가 안가는 것 같다. 이부분은 따로 구글링을 해봐야할듯 하다.
--2021-02-13 수정
시퀀스는 int형 index로 값에 접근할 수 있는 iterable이라고 한다. 즉 딕셔너리와 세트의 경우 키로 접근하기 때문에 시퀀스에 해당되지 않는 것이다.
또한 iterable은 자신의 요소들을 한 번에 하나씩 리턴할 수 있는 객체라고 한다.
출처 : iterator & sequence
-iterator class
앞서 클래스에 __iter__와 __next를 모두 구현하면 iterator를 만들 수 있다고 했는데, 이번에는 클래스에 __iter__와 __next__를 구현해서 이터레이터를 만드는 법을 정리하겠다.
class Counter:
def __init__(self, stop):
self.current = 0
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current < self.stop:
r = self.current
self.current += 1
return r
else:
raise StopIteration
for i in Counter(3):
print(i, end=' ')
위는 코딩 도장의 예시 코드다. 먼저 Counter라는 클래스를 만들고, 시작값인 current를 0 인자로 stop을 받아 끝값 속성의 값을 넣어준다. 메서드로는 __iter__, __next__가 있는데, 반복 가능한 객체에서 iterator를 가져오는 기능인 __iter__() 메서드는 이미 Counter 자체가 iterator로써 쓰이므로 별다른 기능 없이 자기 자신의 인스턴스를 반환해준다. __next__()는 만약 current(시작 값)이 stop(끝 값)보다 작다면 current를 1씩 더해주며 해당 값을 return해 주는 것이다. 만약 current가 stop보다 같거나 크면 StopIteration 예외를 발생시킨다.
실행을 해보기전 rasie StopIteration 예외 때문에 0~2까지 출력하고 프로그램이 멈출줄 알았는데 의외로 멈추지 않고, 에러문구도 발생시키지 않았다. 생각해보니 이 __next__ 메서드를 직접 호출한게 아닌 for i in Counter(3) 이런식으로 for문에서 자동으로 __next__ 메서드를 호출해주는걸 보면, for문 안에 자체적으로 예외처리문이 있어 StopIteration이 발생할 경우, 에러 문구를 띄우는게 아니라 반복을 종료해주는 것 같다.
class Range:
def __init__(self, startNum, endNum = 0, pmValue = 1):
if endNum == 0:
self.endNum = startNum
self.startNum = 0
self.pmValue = pmValue
else:
self.startNum = startNum
self.endNum = endNum
self.pmValue = pmValue
def __iter__(self):
return self
def __next__(self):
if self.startNum < self.endNum:
n = self.startNum
self.startNum += self.pmValue
return n
else:
raise StopIteration
위의 코드를 응용해서 시작 값, 끝 값, 증감폭을 사용할 수 있는 range를 구현해 보았다. 매개변수가 1개가 주어질 경우 끝값만 주어진 것이고, 2개가 주어지면 시작 값과 끝 값, 3개가 주어지면 시작 값, 끝 값, 증감폭이 주어진 것이므로, 일단 주어지지 않을 수도 있는 두 변수의 기본 값을 지정해준다. 만약 매개 변수가 1개가 주어진다면 첫번째 값이 끝 값이 되기 때문에 이를 고려해 if문으로 만약 두 번째 매개변수 값이 기본 값이면 시작 값은 두 번째 값(기본 값), 끝 값은 첫 번째 매개변수의 값, 증감폭은 세번 째 값(기본 값)이 된다. 만약 아니라면 최소 시작 값과 끝 값은 입력된 것이므로 입력 그대로 첫 번째 값이 시작 값, 두 번째 값이 끝 값, 세 번째 값(기본 값 혹은 입력 값)이 증감폭이 된다.
__iter__()는 똑같이 구현했고, __next__()는 조금 바꿔서 값에 1을 더해주는 대신 증감폭인 pmValue를 더해주게 했다. 만약 startNum(시작 값)이 endNum(끝 값)보다 작지 않다면 StopIteration 예외를 발생시키도록 작성했다.
만든 Range 클래스를 for문에 직접 써보니 range처럼 제대로 결과가 나오는 것을 볼 수 있다.
참고로 iterator는 언패킹이 가능하다고 한다.
신기한 사실인데 위 a,b,c = Range(3) 대신 _,b,c = Range(3) 이렇게 작성하면 _에 해당하는 값이 무시된다고 한다.
-__getitem__()
예전에 정리해 뒀지만 __getitem__(<인덱스>)는 list, str 등에서 인덱스번째의 값을 출력해 주는 것으로 <변수명>[<인덱스>] 또한 결국 __getitem__을 사용해서 호출하는 것이라고 정리했다. 이번에는 이 __getitem__()을 iterator에 구현할 것이다.
class Range:
def __init__(self, endNum):
self.endNum = endNum
def __getitem__(self, index):
if index < self.endNum:
return index
else:
raise IndexError
앞서 구현한 Range 클래스의 메소드를 조금 수정해서 __getitem__을 구현했다. 먼저 __init__ 메서드에서 매개변수로 끝 값을 받아와 endNum에 저장한다. 그리고 __getitem__에서 매개변수로 index를 받아오는데, range(<값>)처럼 지정 값까지 수가 요소이므로 0번째 값은 0, 1번째 값은 1.. 일 수 밖에 없다. 따라서 지정한 index가 endNum보다 작으면 해당 index를 결과로 반환해주면 되고, 만약 같거나 큰 경우 index가 초과되므로 IndexError를 발생시키면 된다.
참고로 이번 코드는 __next__를 구현했던 코드와 달리 __iter__(), __next__() 메서드를 구현 안했는데, __getitem__() 메서드만 구현해도 iterator가 되어서 생략해줘도 상관없다고 한다.
-iter
iter는 반복 가능한 객체에서 이터레이터를 반환해 준다. __iter__()에서 반환 기능 등이 추가된 것이다.
iter(<반복 가능한 객체>)
위처럼 반복 가능한 객체 range(5)를 iter로 감싸서 반환 된 iterator를 변수 a에 넣어줬다. 그 결과 iterator 메서드인 __next__()등을 사용했을 때 제대로 결과가 나온다.
iter에 반복을 끝낼 값을 인자로 줘, 해당 값이 나올 때 반복을 끝낼 수 있다. 이 경우에는 iter(<반복 가능한 객체>) 형태가 아닌 iter(<호출 가능한 객체>, <반복을 끝낼 값>) 이렇게 써줘야한다. 처음에는 호출 가능한 객체가 뭐가 있지 생각했다. 쉽게 생각하니 호출 가능한 객체므로 함수를 하나 제작해서 넣어줘도 해당이 된다. 다만 함수를 넣을 경우 해당 함수가 매개변수를 받는 함수면 오류가 발생한다. 함수 말고도 예전에 정리했던 lambda 표현식 또한 사용이 가능하다 한다.
호출 가능한 객체에 함수를 하나 만들어서 넣어줬다. 반복을 끝낼 값은 10으로 지정하고, 해당 함수는 전역 변수 i에 1을 더한 후 해당 값을 반환해준다. 이렇게 작성하고 실행을 해보니 10이 나오기 전인 9에서 반복을 멈추는 것을 볼 수 있다.
lambda 표현식도 써봤다. 반환 값의 변동이 필요하므로 random 모듈을 불러와서 해당 범위 안의 랜덤한 수를 반환해주는 randint 함수를 썻다. 결과를 보면 랜덤한 값이 출력되다가 랜덤 값이 5가 되는 순간 반복을 멈추는 것을 볼 수 있다.
-next
next는 iterator의 값을 꺼내서 반환해주는 함수로 __next__()에서 반환 기능 등이 추가된 것이다.
next(<iterator>)
위처럼 iter 함수로 [1,2,3]의 iterator를 변수 it에 넣은 후, 이 it를 next()로 감싼다면 iterator의 값이 차례대로 출력해주는 것을 볼 수 있다. 역시나 iterator에 더 이상 출력할 요소가 없다면 StopIteration 예외를 발생시킨다.
next에 기본값 요소를 줘서 더 이상 출력할 요소가 없어도 StopIteration 예외를 발생시키는 대신 기본값을 출력하도록 할 수 있다. next(<iterator>,<기본값>) 이렇게 작성하면 된다.
위처럼 기본값을 'end'로 지정해주고 next 함수를 호출하니, 반복이 더 이상 출력할 요소가 없을 경우 StopIteration 예외를 발생시키는 대신 'end'를 출력해주는 것을 볼 수 있다.
iterator는 __iter__(), __next__()를 구현해서 만들 수 있다.
답 : a, c
인덱스로 접근할 수 있는 iterator를 만들 때 필요한 메서드는 __getitem__()이다.
답 : __getitem__
a : 반복을 끝낼 때는 StopIteration 예외가 발생된다.
e : 리스트 [1,2,3]은 그저 반복 가능한 객체일 뿐이다. 이를 이터레이터로 사용하려면 [1,2,3].__iter__()를 해줘야한다.
특정 수의 배수를 만드는 iterator를 작성하는 것이 목적이다.
class MultipleIterator:
def __init__(self, stop, multiple):
self.startNum = 0
self.stop = stop
self.multiple = multiple
def __iter__(self):
return self
def __next__(self):
if self.startNum + self.multiple < self.stop:
self.startNum += self.multiple
return self.startNum
else:
raise StopIteration
for i in MultipleIterator(20, 3):
print(i, end=' ')
print()
for i in MultipleIterator(30, 5):
print(i, end=' ')
먼저 __init__ 메서드에서 시작 값(startNum), 끝 값(stop), 배수(multiple)의 값을 0과 매개 변수 값으로 각각 지정해주고, __next__에서 startNum + multiple이 stop보다 작은 경우에는 startNum에 multiple을 더한 후 그 값을 반환해 주고, 만약 stop보다 큰 경우 StopInteration 예외가 발생하도록 작성했다.
시작 초, 끝낼 초, 인덱스가 입력되는데, 해당 시작 초부터, 끝낼 초까지 1씩 증가 시켜서 값들을 출력하고, 해당 인덱스의 시간도 출력하는 것이 목적이다. 23:59:59를 넘길 경우 00:00:00부터 다시 시작해야한다. 형식은 시 : 분 : 초 형식이고 각각 값이 한자리 수일 경우 앞에 0을 붙여서 출력해야한다.
class TimeIterator:
def __init__(self, start, stop):
self.s_min = ((start // 60) % 60)
self.s_hour = (start // 60) // 60
self.s_sec = start % 60
self.presentNum = 0
self.count = stop - start
def __iter__(self):
return self
def __next__(self):
if self.presentNum < self.count:
if self.s_sec == 60:
self.s_sec = 0
self.s_min += 1
if self.s_min == 60:
self.s_min = 0
self.s_hour + 1
if self.s_hour == 24:
self.s_hour = 0
sec, minute, hour = self.s_sec, self.s_min, self.s_hour
self.presentNum += 1
self.s_sec += 1
return '%02d:%02d:%02d' % (hour, minute, sec)
else:
raise StopIteration
def __getitem__(self, index):
self.s_sec += index
if self.s_sec >= 60:
self.s_sec = self.s_sec - 60
self.s_min += 1
if self.s_min == 60:
self.s_min = 0
self.s_hour += 1
if self.s_hour == 24:
self.s_hour = 0
return '%02d:%02d:%02d' % (self.s_hour, self.s_min, self.s_sec)
start, stop, index = map(int, input().split())
for i in TimeIterator(start, stop):
print(i)
print('\n', TimeIterator(start, stop)[index], sep='')
<__init__>
먼저 초, 분, 시간을 담는 변수 s_sec, s_min, _s_hour를 각각 만들어준다. 초는 주어진 초 % 60하면 나오고, 분은 (초 // 60) % 60하면 나온다. 마지막으로 시간은 (초 // 60) // 60을 해주면 구해진다. 그리고 presentNum, count 변수도 만들어 줬는데, presentNum은 count와 비교해서 StopIteration 예외를 호출할 지 여부를 결정하는 변수고, count는 끝 초 - 시작 초를 해서 몇번 반복을 해야하는지 구해준다.
<__next__>
먼저 presentNum과 count를 비교하는데, 만약 count보다 presentNum이 작지 않다면 StopIteration 예외를 발생시킨다.
count보다 presentNum이 작다면 먼저 s_sec, s_min, s_hour를 검사해 각각 60, 60, 24를 같다면 해당 변수를 0으로 만들어 주고, s_sec가 60과 같다면 s_min += 1, s_min이 60과 같다면 s_hour += 1, s_hour가 24와 같다면 s_hour = 0.
해당 시, 분, 초 부터 출력해야하므로 임시변수 sec, minute, hour를 만들어 조건식을 거쳐 각각 60, 60, 24 보다 작아진 s_sec, s_minute, s_hour를 각각 넣어준다. 그 후 presentNum += 1해서 카운팅을 해주고, s_sec에 1을 더함으로써 1초 추가해준다. 마지막으로 임시변수들에 넣어진 시, 분, 초를 1의 자리만 있는 경우 앞에 0을 붙여야 하기 때문에 '%02d:%02d:%02d' 포맷으로 리턴해준다.
<__getitem__>
index 번째의 시간을 구하는 것이므로, s_sec(초)에 index를 더해준다. 더했을 때 s_sec이 60을 넘는다면 s_sec은 s_sec - 60으로 바꿔주고, s_min(분)을 1 더해준다. 만약 s_min이 60이 된다면, s_min을 0으로 바꿔주고 s_hour(시간)에 1을 더해준다. 만약 s_hour가 24일경우에는 s_hour를 0으로 바꿔준다. 위 조건문들을 다 거친 후 '%02d:%02d:%02d' 포맷으로 리턴해준다.
02-14 (Unit 43.1 ~ Unit 43.6)
-정규표현식-
정규표현식은 일정한 규칙(패턴)을 가진 문자열을 표현하는 방법(특정한 규칙을 가진 문자열의 집합을 표현하는 데 사용하는 형식 언어 참고 사이트)이다. 복잡한 문자열 속에서 특정 패턴의 문자열을 검색하고 추출하거나 바꿀 수 있고, 문자열이 정해진 규칙에 맞는지도 판단할 수 있다.
정규표현식은 re 모듈을 가져와서 사용한다.
-match
match는 find와 비슷하게 해당 패턴이 문자열에 있는지 확인해준다. 만약 해당 패턴이 있다면 SRE_Match 객체가 반환되고 없다면 아무것도 반환되지 않는다.
match('<패턴>','<문자열>')
string을 'Hello dypar'로 지정해주고 match('Hello',string), match('dypar',string), match('H',string)을 해봤다. 'Hello' 패턴을 넣었을 때, 해당 패턴이 문자열 안에 있으므로 SRE_Match 객체가 반환된다. 내용을 보면 re.Match object(SRE_Match 객체라는 것을 뜻하는 것 같다.)와 span=(0, 5) (해당 패턴의 위치 인덱스 값인 것 같다. 첫 번째 인덱스부터 두 번째 인덱스 -1 까지..), match='Hello' (지정 패턴)가 있는 것을 볼 수 있다.
이상하게도 'dypar'를 패턴으로 입력했을 때는 값이 반환되지 않는데, 아직 이유를 모르겠다. -> 이유는 match는 문자열 처음부터 매칭되는지 판단하기 때문에, 첫 문자가 'd'가 아니라면 패턴이 존재하지 않는 것으로 처리하는 것이다.
-search
search는 match와 달리 처음부터 패턴과 매칭되는지 확인하지 않고, 문자열 일부분이 매칭되는지 판단한다.
search('<패턴>','<문자열>')
보면 search 역시 SRE_Match 객체를 반환해준다. Match와는 달리 'dypar' 포함 여부도 판단할 수 있는 것을 볼 수 있다.
-^, $, |
문자열안의 패턴에 인덱스 값을 구해주는 것은 find도 할 수 있는 일인데, 정규표현식으로 특수 문자를 사용한다면 find가 할 수 없는(예를 들어 문자열이 맨 앞으로 오는지 여부 판단) 것들을 판단할 수 있다.
^<문자열> : 문자열이 맨 앞에 오는지 판단
<문자열>$ : 문자열이 맨 뒤에 오는지 판단
<문자열>|<문자열> ... : 지정 문자열이 하나라도 포함되는지 판단(OR 조건과 유사하다 보면 될 것 같다.)
^, $ 같은경우 맨 앞, 맨 뒤에 오는지 판단하는 기능을하기 때문에, 맨 처음부터 검사하는 match에는 사용해도 의미가 없다. 따라서 search에 사용해야하고, |의 경우는 search, match에 둘다 사용할 수 있다.
^, $, | 모두 해당 기능대로 잘 동작하는 것을 볼 수 있다. |의 경우 줄친 부분을 보면, |로 나눠준 패턴 중에서 해당되는 문자열을 SRE_Match 객체의 match='<문자열 안에 있는 패턴>' 이렇게 표시해주는 것을 볼 수 있다.
-[ ], *, +, ?, ., { }
[ ]는 범위 패턴을 판단해주는 기호다. 숫자 범위로 예를 들면 [0-9]를 넣으면 0부터 9까지의 숫자가 있는지 판단가능하고, [0-5]까지 넣으면 0~5까지의 숫자가 있는지 판단해준다. 문자의 경우는 [a-z]를 넣으면 알파벳 a부터 알파벳 z까지 있는지 판단, [A-Z]를 넣으면 대문자 알파벳 A부터 대문자 알파벳 Z까지 있는지 확인한다. 신기한 사실인데 한글 또한 가능하다. [가-핳] 이런식으로 나올 수 있는 한글 조합을 정해주면 제대로 판단된다.
<문자>* : 해당 문자가 0개 이상 있는지 판단(0개 이상이기 때문에 어떤 경우에도 존재한다고 판단이 되는 것 같다.)
<문자>+ : 해당 문자가 1개 이상 있는지 판단(0개일 경우 존재하지 않다 판단한다.)
<문자>? : 해당 문자가 1개 혹은 0개 있는지 판단
<문자>. : .의 위치에 문자가 1개 있는지 판단(문자가 0개인 경우가 있는지 잘 모르겠다. -> 테스트 결과, 'dypar'일 경우 'dypar.'을 패턴으로 하면 .의 위치에는 아무 문자, 공백까지도 없기 때문에 이에 해당되는 것 같다. 또한 'dypar'일 경우 'dyp.ar'를 해도 안된다. p와 ar 사이 아무 문자도 존재하지 않기 때문이다.)
<문자 혹은 문자열>{<개수>} : 해당 문자가 주어진 개수만큼 있는지 판단
[^<범위>]<+ or *> : [ ] 범위 앞에 ^를 붙여주면 해당 범위가 아닌지를 판단
[ ]로 범위를 지정해서 해당 범위 안의 숫자가 문자열에 포함되고 있는지 확인할 수 있고, *는 무조건 SRE_Match 객체를 반환, +는 1개 이상 있을 경우 반환 해주는 것을 볼 수 있다. 또한 [ ]와 *,+를 같이 쓸 수 있는 것을 볼 수 있다.
문자도 역시 가능하고, 또한 대문자, 숫자, 소문자를 한번에 판단할 경우 [A-Z0-9a-z] 이런식으로 붙여써주면 제대로 판단이 되는 것을 알 수 있다.
위의 또한 *, +는 이렇게도 쓸 수 있다.
'dypar' 문자열을 담고 있는 string을 'z*d'를 패턴으로 확인한다면 z는 0개 이상, d는 1개가 있어야지 SRE_Match 객체를 반환해 준다. z+d 패턴일 경우 z 1개 이상, d 1개 있어야지 SRE_Match 객체를 반환해준다. d+m* 패턴의 경우 d 1개 이상, m은 0개 이상 있으면 SRE_Match 객체를 반환해 준다.
?는 해당 문자가 0개 또는 1개인지 판단해주는 것을 볼 수 있다. .은 해당 위치에 문자가 존재하는지 판단하는데 'dypar'의 경우 'dy.ar'은 SRE_Match 객체를 반환해주지만, 'dypar.'이나 'dyp.ar'의 경우 반환을 안해주는 것을 볼 수 있다.
{<개수>}로 지정해주면 해당 개수 만큼 존재할 시 SRE_Match 객체를 반환하는 것을 볼 수 있다. 또한 문자열도 가능한데 이 경우 문자열 전체를 ( )로 묶어주고 {<개수>}를 써줘야한다.
이렇게 범위 앞에 ^를 붙여주면 해당 범위에 포함되지 않는지를 판단한다.
이 [ ]과 { }등을 이용하면 사용자가 입력한 전화번호가 형식이 맞는지 체크할 수 있다.
-\<특수문자>
정규 표현식에 사용되는 *, +, ?, . 등을 판단하려고 보면 이를 표현식으로 인식해서 제대로 판단이 안되는 것을 볼 수 있다. '\'를 출력하고 싶을 때 '\' 앞에 \를 써준 것 처럼, 이럴 때는 그저 판단하고 싶은 특수문자 앞에 \를 붙여주면 된다.
-\d, \D, \w, \W, \s, \S
\d : [0-9]와 같다.(decimal의 약자 d)
\D : [^0-9]와 같다.
\w : [a-zA-Z0-9]와 같다. (넓음을 뜻하는 wide의 약자라고 조심스럽게 추측해본다.)
\W : [^a-zA-Z0-9]와 같다.
\s : [ \t\n\r\f\v]와 같다.(공백 + 탭 + 줄 바꿈, 캐리지 리턴(?) + 폼피드(?) + 수직 탭)참고
\S : [^ \t\n\r\f\v]와 같다.
-compile
compile 함수는 객체에 패턴을 저장해주는 함수다. 해당 패턴이 필요시 <해당 객체>.match 혹은 search를 써주고 괄호안에 해당 문자열만 적어주면 된다.
import re
def check_phoneNumber(phoneNumber):
print(re.match('\d{3}-\d{4}-\d{4}',phoneNumber))
def check_email(email):
print(re.match('\w+@[a-z]+\.[a-z]',email))
def check_koreanName(name):
print(re.match('[가-핳]+',name))
check_koreanName(input('이름 : '))
check_phoneNumber(input('전화번호(010-1111-1111 형식) : '))
check_email(input('이메일 : '))
배운 것을 응용해서 만들어본 이름, 전화번호, 이메일 형식 확인 기능을 하는 코드다. 여기다 조건식만 추가해서, 형식 여부를 확인하면 완벽해질 것 같다.
웹 사이트의 회원가입 페이지에서 입력 형식 확인용으로 정규표현식을 써도 괜찮을 것 같다.
-group
이제까지 정규표현식 하나로 하나의 문자열의 매칭을 판단했는데, ()로 정규 표현식을 묶고 문자열을 구분해주고 match 시키면 해당 문자열을 해당 표현식((표현식1) (표현식2),'1234 123'로 작성한다면 표현식 1이 1234를 표현식 2가 123을 각각의 표현식으로 판단한다.)으로 판단한다. 이 각각의 판단 결과들을 따로 그룹을 만들어서 저장한다. 이 group 함수는 해당 그룹의 문자열을 가져와 준다.
<객체> = re.match('(<첫 번째 정규 표현식>) (<두 번째 정규 표현식>)... ', <문자열>)
<객체>.group(<번호>)
'1234 Dypar33'을 \d+와 \w+로 각각 매칭을 판단했다. \d+는 1234를 판단했기에 1234 그대로 저장, \w는 Dypar33을 판단했기에 Dypar33 그대로 저장한다. \d는 그룹 1, \w는 그룹 2에 저장하는데, 이를 group 함수로 숫자를 지정해서 해당 결과를 반환할 수 있다. 또한 group 괄호 안에 아무것도 안넣거나 0을 넣어주면 그룹 전부를 출력해준다.
-?P<이름>
group을 숫자로 구분하면 많아질 수록 혼동이 되는데, 이때 ?P<이름>으로 해당 그룹의 이름을 지정해줄 수 있다. 또한 이름을 지정해줬을 때, group(<해당 이름>)으로 해당 이름의 그룹을 반환해줄 수 있다.
-findall
지금까지는 그룹에 해당하는 문자열을 가져왔는데, findall 함수를 사용하면 그룹 지정 없이 패턴에 매칭되는 모든 문자열을 가져올 수 있다.
match라면 못가져왔을 Dypar 뒤에 33까지 가져오는 것을 볼 수 있다.
-sub
sub는 정규표현식으로 패턴에 맞는 문자를 다른 문자로 바꿔주는 함수다.
sub('<패턴>', '<바꿀문자열>', '<문자열>', <바꿀 횟수>)
위처럼 사용하면 되는데, 바꿀 횟수를 생략하는 경우 패턴에 해당되는 문자열을 모두 바꿔준다. 또한 바꿀 문자열에 함수 객체를 넣어줘서, 함수의 리턴값에 해당하는 문자열로 바꿔줄 수 있다. 람다 표현식 역시 가능하다.
3행은 일반적인 sub 형태로 '3 + 6 + 9'에서 \d에 해당하는 문자들을 '숫자'로 바꿔줬다. 바꿀 횟수는 지정해주지 않았으므로 모두 바꿔준다.
5~9행은 코딩도장의 예제 코드다. multiple10 함수가 매개변수로 매치 객체를 받는다고 하는데, 이부분은 완벽히 이해는 못한 것 같다. 내가 이해한 바로는 [0-9]에 해당하는 문자를 '1 2 Fizz 4 Buzz Fizz 7 8'에서 찾고, 하나를 찾을 때마다 그것을 인자로 multiple10에 넘겨주고, group으로 값을 빼낸 후 int 형으로 만들어 10을 곱해준 값을 문자열로 리턴해 그 값으로 바꿔주는 것 같다.
re.sub('\d', lambda m: str(int(m.group()) * 10), '1 2 Fizz 4 Buzz Fizz 7 8')
이는 람다표현식으로 위 코드처럼 작성할 수 있다.
-\\<숫자>
\\<숫자>는 정규표현식으로 찾은 문자열을 가져와서 다시 사용할 수 있도록 해준다. group 함수와 비슷한 기능이라 보면 된다.
위는 찾은 결과를 \\2, \\1로 바꿀 문자열에 넣어서 공백을 기준으로 '1234 dypar'를 순서를 바꿔서 출력한 모습이다.
re.sub('({\s*)"(\w+)":\s*"(\w+)"(\s*})', '<\\2>\\3</\\2>', '{ "name": "james" }')
위는 코딩도장의 예시 코드다. 솔직히 알고 봐도 너무 보기 싫어지는 형식인데, 이를 해석해보겠다.
첫 번째 그룹의 정규 표현식은 {\s* 이다. '{' 와 공백을 찾는 것이므로 '{ "name": "james" }' 이 문자열 중에서 '{ '가 해당된다. 그리고 두 번째 그룹은 \w+다. " "사이의 문자, 숫자를 가져오는 것이므로 name이 해당된다. 세 번째 그룹은 \w+다. 앞에 :\s*이 있었으므로 이 부분은 james를 가져온다. 그 다음 네 번째 그룹 \s*}은 ' }'를 가져온다.
이렇게 가져온 1,2,3 그룹을 '<\\2>\\3</\\2>' 이렇게 바꿔주는데 \\2는 name이므로 <name>가되고 \\3은 james이므로 <name>james가 된다. </\\2> 이부분은 </name>이 되서 결과는 '<name>james</name>'가 된다.
만약 그룹에 이름을 지었다면 \\g<이름 혹은 숫자>로 불러올 수 있다.
주어진 이메일 주소가 올바른지 판단하도록 패턴을 짜는 것이 목적이다. 이메일들을 보면 대충 규칙이 (영어, 숫자, _, -, . +)@(영어, -).(영어,-,.]으로 되어 있는 것을 볼 수 있다. 이를 이용해 정규 표현식을 짜면 된다.
import re
p = re.compile('[a-z0-9+-_.]+@[a-z-]+\.[a-z-.]+')
emails = ['python@mail.example.com', 'python+kr@example.com',
'python-dojang@example.co.kr', 'python_10@example.info',
'python.dojang@e-xample.com',
'@example.com', 'python@example', 'python@example-com']
for email in emails:
print(p.match(email) != None, end=' ')
'[a-z0-9+-_.]+@[a-z-]+\.[a-z-.]+' 이렇게 짯다. @ 앞은 a~z, 0~9, +, - 포함으로 짯고 @ 바로 뒤는 a~z, - 포함으로, . 뒤는 a~z, -, .이 포함되도록 짯다.
url이 입력되면 해당 url이 올바른지 검사하는 것이다. url의 조건은 http:// 또는 https://로 시작해야하고 도메인.최상위도메인 형식(대소문자, 숫자, - 만 허용), 그리고 도메인 이하는 /로 구분한다.(대소문자, 숫자, -, _, . , ?, = 만 허용)
import re
url = input()
print(True if re.match('(https?)://[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+[a-zA-Z0-9?=_/.-]+',url) != None else False)
1행에서 정규표현식을 사용하기 위해 re를 임포트 해준다. 2행에서는 url을 input 받는다.
3행은 1줄 if문을 써주는데, 맨처음 http or https가 나오므로 s뒤에 ?를 붙여줘 http, https 둘다 가능하도록 해줬다. 그 다음 ://는 무조건 나오므로 써준다. 도메인은 영문 대소문자, 숫자, -로 이루어 지는 것이 조건이므로 a-zA-Z0-9-를 범위로 지정해준다. 원래는 여기에 .까지 포함해서 도메인 부분을 범위 하나로 읽으려고 했지만, 틀린 답도 맞다고 인식하는 버그가 있어서 그냥 \.으로 도메인의 .을 구분해서 첫 번째 두 번째 도메인 까지 읽었다. 세 번째 도메인의 범위를 따로 지정해주지 않은 이유는 어쩌피 세 번째는 뒤에 . 대신 /이 붙기도하고 네 번째까지 있다고 가정해도 어쩌피 원래 범위 하나로 도메인을 끝낼려 했지만 잘 못된 입력 까지 True로 인식하기 때문에, 이를 방지하려고 두 번째까지는 범위로 지정해준 것이다. 따라서 세 번째 도메인... + 도메인 이하 경로는 영문 대소문자, 숫자, -, _, ., ?, =, /를 범위로 지정해줬다.
만약 매치하지 않는다면, None을 리턴하므로 None과 비교해서 None이라면 False를 None이 아니라면 True를 출력하도록 구현했다.
정규 표현식은 지금까지 배웠던 파이썬 문법 중에서 가장 이해하기 어려웠던 것 같다.(현재도 어느정도 이해는 한 것 같지만, 암기식으로 학습한 것 같다.) 파이썬 알고리즘 문제의 모범 답안을 보면 정규 표현식으로 간결하게 작성한 것들이 많다. 쓰임도 많고 중요하므로 이부분은 복습하면서 보완해야겠다.
'Old (2021.01 ~ 2021.12) > Programming' 카테고리의 다른 글
c언어 코딩도장 Unit 12 ~ 23 (4) | 2021.02.17 |
---|---|
c언어 코딩도장 Unit 1 ~ 11 (0) | 2021.02.16 |
생활코딩 HTML 의미론적 태그 ~ 검색엔진 최적화 (0) | 2021.02.11 |
생활코딩 HTML 버튼 ~ meta (0) | 2021.02.09 |
생활코딩 HTML 기술소개 ~ 선택 (0) | 2021.02.07 |