List Comprehension, Iterator, Generator

jayden·2020년 6월 7일
0

python

목록 보기
3/3

1. List Comprehension

(1) 개요

리스트 컴프리헨션이란 새로운 리스트를 만들 때 사용할 수 있는 간단한 표현식으로 리스트와 마찬가지로 대괄호 [, ] 를 사용하여 작성한다.

그리고 우리가 만들려고 하는 원소를 표현하는 표현식으로 시작하여 for 루프가 뒤에 따라오는 형식을 가진다. For 문 뒤에 if문을 추가하여 조건문을 포함한 형식도 올 수 있다.

(2) 표현식 규칙

[ 표현식 for 원소 in 반복 가능한 객체 ]

[ 표현식 for 원소 in 반복 가능한 객체 if문 ]

(3) 적용 예시

  • 사용전
odd_numbers = [ ]
for element in range(1,11):
    if (element % 2) == 1:
        odd_numbers.append(element)
  • 사용후
list_comprehension = [ element for element in range(1,11) if (element % 2) == 1 ]
print(list_comprehension)

(4) 사용 예시

  • 다음과 같은 도시목록의 리스트가 주어졌을때, 도시이름이 S로 시작하지 않는 도시만 리스트로 만들 때 리스트 컴프리헨션을 사용하여 함수를 작성해 보기
cities = ["Tokyo","Shanghai","Jakarta","Seoul","Guangzhou","Beijing","Karachi","Shenzhen","Delhi"]
list_comprehension = [i for i in cities if list(i)[0] != 'S']
print(list_comprehension)
  • 다음과 같은 도시, 인구수가 튜플의 리스트로 주어졌을때, 키가 도시 값이 인구수인 딕셔너리를 딕셔너리 컴프리헨션을 사용한 함수를 작성해 보세요.
population_of_city = [('Tokyo', 36923000), ('Shanghai', 34000000), ('Jakarta', 30000000), ('Seoul', 25514000), ('Guangzhou', 25000000), ('Beijing', 24900000), ('Karachi', 24300000 ), ( 'Shenzhen', 23300000), ('Delhi', 21753486) ]
popDict2 = dict((i) for i in population_of_city)
print(popDict2)

2. Iterable, Iterator

Iterable

(1) Iterable 의미

iterable의 의미는 member를 하나씩 차례로 반환 가능한 object를 말한다.

(2) 대표적인 Iterable

  • sequence 타입인 list, str, tuple 등
  • non-sequence 타입인 dict나 file도 iterable 하다.
  • iter()나 getitem() 메소드로 정의된 class는 모두 iterable 하다.

Iterator

(1) 개요

Iterator 는 next() 메소드로 데이터를 순차적으로 호출 가능한 object 이다. 만약 next() 로 다음 데이터를 불러 올수 없을 경우 (가장 마지막 데이터인 경우) StopIteration
exception을 발생시킨다.

그렇다면 iterable한 object는 iterator일까?

반드시 그렇지 않다.

 x = [1,2,3]
 next(x)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 _TypeError: list object is not an iterator_

list는 iterable이지만, 위와 같이 next() 메소드로 호출해도 동작하지 않고 iterator가 아니라는 에러 메시지를 볼 수 있다.
iterable을 iterable로 변환하고 싶다면, iter()라는 built-in function을 사용하면 된다.

>>> x = [1,2,3]

>>> type(x)

<type 'list'>

>>> y = iter(x)

>>> type(y)

<type 'listiterator'>



출처: https://bluese05.tistory.com/55 [ㅍㅍㅋㄷ]

위와 같이 iter()함수를 사용하여 list를 listiterarot로 변환이 가능하다.

출처: https://bluese05.tistory.com/55 [ㅍㅍㅋㄷ]

(2) 사용 예시

딕셔너리도 반복가능한 객체라서 앞서본 리스트와 같이 iter함수와 next함수를 사용할 수 있고 파이썬 기본함수인 iter, next 또한 사용할 수 있습니다. 다음의 간단한 키를 출력하는 딕셔너리에 대한 for 문을 while문으로 구현해 보세요.

D = {'a':1, 'b':2, 'c':3}
for key in D.keys():
    print(key)

iterator을 활용한 while 문 예시

D = {'a':1, 'b':2, 'c':3}
I = iter(D.keys())

while True:
    try:
        X = next(I)
    except StopIteration:
        break
    print(X, end=' ')  

3. Generator

(1) 개요

Generator는 Iterator의 특수한 한 형태이다. generator 는 간단하게 설명하면 iterator 를 생성해 주는 function 이다.

Generator 는 일반적인 함수와 비슷하게 보이지만, 가장 큰 차이 점은 yield 라는 구문일 것이다.
Generator 함수(Generator function)는 일반적인 함수처럼 작성되지만 데이터를 반환하기 위해서 return 문장이 아니라 함수 안에 yield 를 사용하여 데이타를 하나씩 리턴하는 함수이다.일반 함수와의 차이는 yield 외에는 없다.

Generator 함수가 처음 호출되면, 그 함수 실행 중 처음으로 만나는 yield 에서 값을 리턴한다. Generator 함수가 다시 호출되면, 직전에 실행되었던 yield 문 다음부터 다음 yield 문을 만날 때까지 문장들을 실행하게 된다. 이러한 Generator 함수를 변수에 할당하면 그 변수는 generator 클래스 객체가 된다.

(2) 일반함수와의 차이점

매번 next() 메서드가 호출될 때마다 제너레이터는 중단된 지점부터 다시 시작한다.(모든 데이터 값과 마지막 실행된 명령문을 기억합니다.) 즉, return 을 사용하는 함수라면, 반환될 때마다 내부 지역변수들은 사라지지만 yield를 사용할 경우 내부 값들이 보존된다.

더 자세히 알아보자.

일반적인 함수의 경우를 생각해보자.

일반적인 함수는 사용이 종료되면 결과값을 호출부로 반환 후 함수 자체를 종료시킨 후 메모리 상에서 클리어 된다.

하지만, yield 를 사용할 경우는 다르다.

generator 함수가 실행 중 yield 를 만날 경우, 해당 함수는 그 상태로 정지 되며, 반환 값을 next() 를 호출한 쪽으로 전달 하게 된다. 이후 해당 함수는 일반적인 경우 처럼 종료되는 것이 아니라 그 상태로 유지되게 된다. 즉, 함수에서 사용된 local 변수나 instruction pointer 등과 같은 함수 내부에서 사용된 데이터들이 메모리에 그대로 유지되는 것이다.

def generator(n):
    i = 0
    while i < n:
        yield i
        i += 1

for x in generator(5):
print x


0
1
2
3
4 

출처: https://bluese05.tistory.com/56?category=559959 [ㅍㅍㅋㄷ]

(3) Generator를 사용하는 이유

먼저는, memory를 효율적으로 사용할 수 있다.

아래 예제를 통해 list comprehension 과 generator expression 으로 각각 생성했을 때 메모리 사용 상태를 보자.

>>> import sys

>>> sys.getsizeof( [i for i in xrange(100) if i % 2] )    # list
536

>>> sys.getsizeof( [i for i in xrange(1000) if i % 2] )
4280

>>> sys.getsizeof( (i for i in xrange(100) if i % 2) )    # generator
80

>>> sys.getsizeof( (i for i in xrange(1000) if i % 2) )
80

출처: https://bluese05.tistory.com/56?category=559959 [ㅍㅍㅋㄷ]

list 의 경우 사이즈가 커질 수록 그만큼 메모리 사용량이 늘어나게 된다. 하지만, generator 의 경우는 사이즈가 커진다 해도 차지하는 메모리 사이즈는 동일하다. 이는 list 와 generator의 동작 방식의 차이에 기인한다.

list 는 list 안에 속한 모든 데이터를 메모리에 적재하기 때문에 list의 크기 만큼 차지하는 메모리 사이즈가 늘어나게 된다. 하지만 generator 의 경우 데이터 값을 한꺼번에 메모리에 적재 하는 것이 아니라 next() 메소드를 통해 차례로 값에 접근할 때마다 메모리에 적재하는 방식이다.

따라서 list 의 규모가 큰 값을 다룰 수록 generator의 효율성은 더욱 높아지게 된다.

둘째로, Lazy evaluation 즉 계산 결과 값이 필요할 때까지 계산을 늦추는 효과를 볼 수 있다.

import time

def print_iter(iter):
    for element in iter:
        print(element)

def lazy_return(num):
    print("sleep 1s")
    time.sleep(1)
    return num

print("comprehension_list=")
comprehension_list = [ lazy_return(i) for i in L ]
print_iter(comprehension_list)

print("generator_exp=")
generator_exp = ( lazy_return(i) for i in L )
print_iter(generator_exp)

comprehension_list 코드를 실행하여 보면 아래와 같은 결과를 얻을 수 있다.

comprehension_list=
sleep 1s
sleep 1s
sleep 1s
1
2
3

모든 함수의 실행문이 한꺼번에 실행되고 마지막에 값들 역시 한꺼번에 리턴됨을 알 수 있다. 즉, 그만큼 메모리나 시스템에 부하를 줄 수 있다.

반면에 generator_exp 코드를 실행하면 아래와 같은 결과를 얻을 수 있다.

generator_exp=
sleep 1s      
1
sleep 1s
2
sleep 1s
3

모든 함수의 실행과 리턴이 한 번에 되는 것이 아니라 for문이 수행될 때 마다 하나씩 로드하게 되는 것을 알 수 있다. 이렇게 되면 데이타가 많거나 수행시간이 긴 연산을 필요한 순간까지 늦출 수 있다는 특징을 이용할 수 있다.

profile
DevOps 너로 정했다!

0개의 댓글