지난 글에서는 책 '파이썬 코딩의 기술'을 리뷰하면서 함수에 대해 정리해 보았다. 이번에는 파이썬 컴프리헨션과 제너레이터의 여러 활용에 대해 알아보도록 하자. 파이썬 내장 라이브러리인 itertools에 대해서는 다루지 않을 예정이다.
- 컴프리헨션
- 제너레이터 식 생성
- yield, next
- yield from
- send
- throw
1. 컴프리헨션
컴프리헨션이란
컴프리헨션은 다른 시퀀스나 iterable에서 새로운 데이터 구조(리스트, 딕셔너리 등)를 생성할 수 있는 구문으로, 컴프리헨션 내 왼쪽에서 오른쪽으로 실행된다. 아래 예시 표와 같이 가독성 측면에서 map, filter를 대신할 수 있다.
다만 컴프리헨션 예시에서 볼 수 있다시피, 컴프리헨션 내부에 하위 식이 많으면 가독성이 떨어지기 때문에 주의해야 한다.
컴프리헨션 예시 | [a*3 for a in tmps] |
컴프리헨션 하위식 예시 | [[0 for _ in range(3)] for _ in range(4)] |
map사용 예시 | map(lambda x: x*3, tmps) |
map, filter 사용 예시 | map(lambda x : x*2 , filter(lambda x : x % 2 ==0, tmps)) |
cf.) iterator(이터레이터)는 모두 iterable 한데, list, dic, tuple과 같이 반복 가능한 객체를 의미한다.
컴프리헨션과 왈러스 연산자
컴프리헨션 내 수식이 길어지면 가독성이 떨어져 지양해야 한다고 위에서 언급했다. 하지만 불가피하게 식이 길어질 때는 어떻게 해야 할까? 왈러스 연산자(:=)를 사용하면 컴프리헨션 내에 대입식을 사용할 수 있다. 아래 예시 코드를 보자.
#아래 코드의 결과는 같다.
def add_and_divide(a, b):
return a%b
tmps = [2,3,4,5,6,7,8]
# comprehension only
{a : add_and_divide(a, 2) for a in tmps if add_and_divide(a, 2)} #-> {3: 1, 5: 1, 7: 1}
# comprehension + walrus
{a : cnt for a in tmps if (cnt:= add_and_divide(a, 2))} # -> {3: 1, 5: 1, 7: 1}
두 코드의 차이는 대입식을 활용해 반복을 피했다는 것이다.
cf.) 지난 글에서 왈러스 연산자에 대해 다뤘었다. (https://hi-lu.tistory.com/entry/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EC%BD%94%EB%94%A9%EC%9D%98-%EA%B8%B0%EC%88%A0-%EB%A6%AC%EB%B7%B0-self%EC%99%80-cls-bytes%EC%99%80-str-f-%EB%AC%B8%EC%9E%90%EC%97%B4)
파이썬 코딩의 기술 리뷰 - self와 cls, bytes와 str, f-문자열, 왈러스 연산자
오늘은 책 '파이썬 코딩의 기술'을 읽고 파이썬으로 코딩하는 데에 유용한 내용들을 정리해보고자 한다. 이 책의 챕터는 다음과 같이 구성되어 있다. 파이썬답게 생각하기 리스트와 딕셔너리 함
hi-lu.tistory.com
단점
입력 시퀀스가 길다면 컴프리헨션은 긴 리스트 인스턴스를 만들게 된다. 즉, 입력이 커지면 메모리를 많이 잡게 되고 프로그램이 꺼질 수 있다.
2. 제너레이터
제너레이터 식
리스트 컴프리헨션과 제너레이터를 일반화한 식으로, 해당 식을 실행할 때 출력 시퀀스 전체를 return하지는 않는다. 제너레이터 식을 호출할 때마다 식 내 원소를 뱉어내는 이터레이터를 생성한다.
제너레이터 식 사용 방법
1. ()
사이에 컴프리헨션 생성 구문을 넣으면 제너레이터 식으로 사용할 수 있다. 아래 코드처럼, 제너레이터 식에서 원소를 가져오고 싶다면 next를 사용하면 된다.
tmp = [1,2,3,4,5]
generator_tmp = (t for t in tmp)
print(generator_tmp) ## <generator object <genexpr> at 0x7ff298014ba0>
#generator 값 받아오기
print(next(generator_tmp)) ## 1
2. 함수
다음과 같이 함수와 yield로 제너레이터를 정의할 수 있다.
def generator_func(tmp):
for t in tmp:
yield t
print(generator_func(tmp)) # <generator object generator_func at 0x7ff2b80a5dd0>
next(generator_func(tmp)) # 1
제너레이터 기능
yield from
여러 제너레이터를 합성하고 싶을 때 사용할 수 있는 기능이다. 예를 들어 두 제너레이터 함수를 호출하는 새 제너레이터를 만들고 싶다고 하자. 그러면 아래와 같은 코드를 작성할 수 있을 것이다. 그러나 여기서 합성할 제너레이터가 많아진다면 for, yield문이 많아지고 가독성이 떨어지게 된다.
def generator_fcn1(tmp):
for i in tmp:
yield i
def generator_fcn2(tmp):
for i in range(len(tmp)):
yield i
def generator_fcn3(tmp):
for t in generator_fcn1(tmp):
yield t
for t in generator_fcn2(tmp):
yield t
그래서 나온 두 번째 방법은 yield from이다. 부모 제너레이터(여기서는 _fcn3를 의미함)에 제어를 맡기기 전에 제너레이터 fcn1, fcn2가 모든 값을 내보낸다. 코드는 아래와 같게 변경될 수 있다. fcn3와 fcn4는 같은 기능을 하고, 파이썬 인터프리터가 for문을 내포하므로 성능이 빨라진다.
def generator_fcn4(tmp):
yield from generator_fcn1(tmp)
yield from genereator_fcn2(tmp)
send
제너레이터가 데이터를 출력하는 것과 데이터를 받아들이는 것을 같이 할 수는 없을까? (== 양방향 통신) send 메서드를 통해 아래와 같이 양방향 통신을 할 수는 있지만, 권장하지는 않는다.
def gen1():
received = yield 1
return received ## default None. 제너레이터를 이터레이션하면 yield는 None을 반환
## test 1 : send를 처음 호출할 때는 None만 가능
it = iter(gen1())
out = it.send('hi')
print(out) #-> TypeError: can't send non-None value to a just-started generator
## test2 : 이런식으로 send를 사용 가능!
it = iter(gen1())
out = it.send(None)
print(out) # 1
try :
it.send('hi!')
except StopIteration:
print('stop')
## 'hi!'
## 'stop'
코드 이해하는데도 직관적이지 않고, send로 보내는 값이 많아진다고 가정하면 더 복잡해진다. 그렇다고 yield from과 같이 쓰면 제너레이터 하나가 시작될 때마다 None이 등장하게 된다.
throw
제너레이터 내에서 Execption을 던질 수 있지만, 이 또한 위 send와 같이 권장되는 사항은 아니다. 우선 throw는 다음과 같이 사용할 수 있다.
class ExError(Exception):
pass
### 사용예시 1
def gen2():
yield 1
yield 2
yield 3
it = gen2()
next(it)
it.throw(ExError('error만들기!')) # -> ExError: error만들기!
### 사용예시 2
def gen3():
try:
yield 1
yield 2
yield 3
except ExError:
yield 11
it = gen3()
print(next(it)) # -> 1
it.throw(ExError('Error 만들기!')) # -> 11
이 Excoption Error를 던짐으로써 제너레이터가 양방향 통신을 할 수는 있지만, 이 또한 코드가 복잡해지면 읽기 어려워진다. 차라리 아래와 같이 클래스로 이 트러블 컨테이너 객체를 생성하는 것이 낫다.
class Gen():
def __init__(self, stop = 4):
self.t = 0
self.stop = stop
def reset(self):
self.t = 0
def __iter__(self):
while self.t <= self.stop:
self.t+=1
yield self.t
def check_error(tmp): # 위 MyError처럼 필요한 에러 함수 정의해서 넣기
print('Error 만들기!')
return False
## error가 있다면 yield 인자를 0으로 reset
gen = Gen()
for g in gen:
if check_error(g):
gen.reset()
print(g)
오늘은 컴프리헨션과 제너레이터에 대해 알아보았다. 파이썬으로 코딩을 하면 알게 모르게 컴프리헨션을 많이 써봤을 것이다. 혹시라도 입력 시퀀스의 메모리가 다 안 올라갈 것 같다면 제너레이터를 사용해 이터레이터를 만들어 사용하도록 하자.
'python > 파이썬 코딩의 기술' 카테고리의 다른 글
파이썬 코딩의 기술 리뷰 - 클래스, 인터페이스 (0) | 2022.01.16 |
---|---|
파이썬 코딩의 기술 리뷰- 동시성과 병렬성 1 (2) | 2022.01.10 |
파이썬 코딩의 기술 리뷰 - 함수 (0) | 2021.12.19 |
파이썬 코딩의 기술 - 리스트, 딕셔너리 (0) | 2021.12.19 |
파이썬 코딩의 기술 리뷰 - self와 cls, bytes와 str, f-문자열, 왈러스 연산자 (2) | 2021.12.12 |