TIL (2020.05.31)

Awesome·2020년 5월 31일
0

TIL

목록 보기
7/46

Python

Exception

예외처리와 관련하여 코드의 진행을 멈추고 싶지 않을 때, try/exception 구문을 사용한다.

그 중에서도 사용자 정의 에러를 만들어 보겠다.

방법은 파이썬에 내장된 기본 Base Exception을 상속받아 원하는 에러명과 메시지로 출력하는 것이다.

class UserDefinedError(Exception):
    def __init__(self):
        super().__init__("내가 만든 에러메시지다.")
        
def errorcheck(value):
    if type(value) == "str"
        print(value)
    else:
        raise UserDefinedError

try:
    errorcheck(123)
except UserDefineError as e:
    print(e)

> 내가 만든 에러메시지다.

먼저, 정의하고자 하는 에러명으로 클래스를 만든다. 이 때, 파이썬 내장 에러를 인자로 받아야하므로 Exception을 받는다. 그 다음 __init__ method 안에는 Exception으로부터 상속 받은 __init__ method를 호출하여 새로 정의하고자하는 에러메시지를 넣는다. super() 는 상속 받을 부모 클래스를 호출하는 역할을 한다.
클래스와 상속에 대한 개념이 있다면 생각보다 쉽게 사용자 정의 메시지를 만들 수 있다.

또 다른 방법으로는 에러를 발생시키는 시점에 메시지를 정의할 수 있다.

class UserDefinedError(Exception):
    pass
 
# 조건문에서 errror 를 발생시킬 때 에러 메시지 정의 가능
raise UserDefinedError("내가 만든 에러메시지다 임마")

Iterator

Iterator 란 값을 차례로 꺼낼 수 있는 객체이다. range를 사용하여 반복문을 실행하면, iterator가 생성되고 하나씩 숫자가 꺼내지는 형식이다.
숫자를 미리 만드는 것이 아니라, iterator를 생성해놓고 필요할 때마다 호출하는 방식이다. 따라서 미리 다 만들어놓는 것보다는 메모리 부담이 적다고 할 수 있다. 이러한 방식을 lazy evaluation 이라고 한다.

Iterator가 생성되기 위한 반복 가능한 객체들은 리스트, 딕셔너리, 문자열, 세트가 있다. 해당 객체들은 __iter__ method와 __next__를 통해서 요소들을 꺼낼 수 있다.

list_iter = [1,2].__iter__()
print(list_iter)

> <list_iterator object at 0x7f63ec40fdc0> # iter 객체 생성

print(list_iter.__next__())
print(list_iter.__next__())
print(list_iter.__next__())

> 1
> 2
> StopIteration

더 이상 꺼낼 요소가 없는 경우, StopIteration 에러를 발생시키고 반복을 종료한다.

즉, 반복 가능한(iterable) 객체(리스트, 튜플 등) 에서 __iter__ method를 통해서 iterator 객체를 생성하고, __next__ method를 통해서 하나씩 요소를 꺼낸다.

Iterator 만들기

iter, next method

이번에는 간단한 iterator를 직접 만들어보겠다.

for i in range(4):
    print(4, end=" ")
    
> 0 1 2 3

위와 같은 결과가 나오게끔 iterator를 만들면 아래와 같다.

class Iterator:
    def __init__(self, stop):
        self.current = 0
        self.stop = stop
        
    # __iter__ method로 인스턴스 자신을 반환 -> iterator 생성
    def __iter__(self): 
        return self
        
    def __next__(self):
        if self.current < self.stop:
            n = self.current
            self.current +=1
            return n
        else:
            raise StopIteration
            
for i in Iterator(3):
    print(i, end=" ")
    
> 0 1 2 3

한 번 뜯어보자.

일단, __init__ method를 통해서 인스턴스의 속성을 정의하였다. 시작점은 0이고, stop 이라는 속성을 통해 멈출 시점을 정한다.

그 다음은 __iter__ method를 통해서 iterator 객체를 생성한다. 이 때, 리스트나 튜플 같은 반복 가능한 객체를 인자로 받는 것이 아니라 숫자를 하나씩 인자로 사용하기 때문에 반복문이 돌 때마다 매 숫자 자체가 iterator 객체가 된다. 나도 100% 이해가 안되서 말이 어렵게 나오는 것 같다. 그냥 쉽게 생각하면 요소가 하나 들어있는 리스트(?) 를 만든다고 해야하나. 그게 맞는건가 잘모르겠다.

그리고나서 __next__ method를 통해서 stop 보다 낮은 경우에는 인스턴스의 current 속성에 1을 더하고, 그 값을 return 한다. 반복문으로 값이 더해지다가 stop과 같아지는 시점에 에러를 발생시키는 조건문을 만든다.

getitem method

이버에는 인덱스를 통해 접근 가능하도록 __getitem__ method를 통해서 iterator 를 만들어 보겠다.

class Iterator:
    def __init__(self, stop):
        self.stop = stop
        
    def __getitem__(self, index):
        if index < self.stop:
            return index
        else:
            raise IndexError
            
print(Iterator(3)[0], Iterator(3)[1], Interator(3)[2])
for i in Iterator:
    print(i, end=" ")
> 0 1 2
> 0 1 2

__getitem__ method를 통해서 반복 가능한 iterator를 생성할 뿐만 아니라, 인덱스로도 접근 가능하게 되었다.

__init__,__getitem__ 처럼 앞, 뒤로 더블 언더스코어로 감싸고 있는 method를 special method 혹은 magic method 라고 한다. 파이썬에서 자동으로 호출해주는 method 이며, 종류가 매우 많다. 필요에 따라 검색해서 사용하면 될 것 같다.


Generator

Generator 는 iterator 를 생성해주는 함수이다.
Iterator 에서는 __iter__,__next__,__getitem__ method 를 구현해야 하지만 generator 안에서는 yield 키워드만 사용하면 된다.

이미지 출처

def generator():
    yield 0
    yield 1
    yield 2
    
for i in generator():
    print(i)
    
> 0
> 1
> 2

매우 간단하다. StopIteration 에러도 자동으로 발생한다. 개 꿀

근데, yield는 해석하면 양보하다 라는 뜻이다. 뭘 양보하는거지?

def generator():
    yield 1
    yield 2
    return "return 지정 값"
    
try:
    g = generator()
    print(next(g))
    print(next(g))
    print(next(g))
except StopIteration as e:
    print(e)
    
> 1
> 2
> return 지정 값

yield 키워드는 return과는 달리, 함수를 종료하지 않고, 해당 값을 함수 밖으로 전달한 뒤 바깥 코드가 실행되도록 양보한다.
위의 예에서 g라는 generator 객체를 만들었다. 그리고 next함수를 통해 값을 차례로 꺼낸다. 이 때, 코드가 진행되는 순서를 차례로 나열해보겠다.

  1. next(g)를 통해 generator 함수 실행
  2. generator 안에서 1을 함수 밖으로 전달하고, 잠시 중단
  3. 전달받은 1을 print한 뒤, 다음 코드로 넘어감
  4. 두 번째 print에서 다시 한번 next(g)를 통해 generator 실행
  5. 멈췄던 generator 에서 다시 2를 함수 밖으로 전달하고 중단
  6. 전달받은 2를 print한 뒤, 다음 코드로 넘어감
  7. 마찬가지로 마지막 print에서 generator 실행
  8. return 값을 받아 함수를 종료하고, StopIteration 에러 발생
  9. 전달받은 StopIteration 에러의 메시지를 return 값으로 출력

복잡해보이지만, 결론은 yield는 함수를 종료하지 않고 잠시 중단하며 그 값을 함수 밖으로 전달한다는 것이다. 또한 return 은 함수를 종료시키는 동시에 StopIteration 에러를 발생시킨다는 점을 알고 넘어가면 될 것 같다.

Generator 만들기(range)

이번에는 range와 같은 기능을 할 수 있는 generator 를 만들어 보겠다.

def generator(stop):
    n = 0
    while n < stop:
        yield n
        n += 1
        
for i in generator(3):
    print(i, end=" ")
 
> 0 1 2

while문을 통해서 간단하게 작성 가능하다.

Generator 만들기(yield from)

앞서 봐왔던 예제들은 값을 여러 번 밖으로 전달하기 위해 for문이나 while문을 사용하였다. 하지만 yield from 을 사용하면 동일한 기능을 구현할 수 있다.

def generator():
    x = [1,2,3]
    for i in x:
        yield i
        
for i in generator():
    print(i, end=" ")
    
> 1 2 3

위와 같은 결과를 yield from 을 사용해서 만들어 보겠다.

def generator():
    x = [1,2,3]
    yield from x
    
for i in generator():
    print(i, end=" ")
  
> 1 2 3

이렇듯이 yield from 을 통해서 for문이나 while문 역할을 대신할 수 있다.

Generator Expression

Generator 도 리스트, 딕셔너리 표현식과 같이 간단한 표현식으로 나타낼 수 있다.

( 표현식 for 변수 in iterable 객체 if 조건문)

g = (i for i in range(10) if x%2==0)
print(g)

> <generator object <genexpr> at 0x7f1518745f20>

for i in g:
    print(i, end=" ")

> 0 2 4 6 8

지금까지 Exception, Iterator, Generator 에 대해서 알아봤다.
사실 iterator와 generator는 아직 완벽히 숙지하지는 못했다. 개념적으로는 어느정도 들어왔지만 직접 더 만들어보고 어떤 상황에 적합한 지를 고민해 볼 필요가 있을 것 같다.
리스트를 생성하고 append 했던 기존의 방식보다 메모리에 부담이 훨씬 덜 가는 방식이기 때문에 반드시 익혀두어야겠다.

profile
keep calm and carry on

0개의 댓글