티스토리 뷰
Iterator
iterator란 가지고 있는 값을 순차적으로 반복해서 하나씩 반환할 수 있는 개체입니다.
python에서는 거의 모든 개체를 iterator로 사용할 수 있게 지원합니다.
iterator와 비슷한 개념으로 iterable이 있습니다.
둘은 비슷하지만 다른 개념입니다.
iterable란 가지고 있는 값을 한번에 하나씩 반환할 수 있는 개체입니다.
즉 한번에 하나씩 반환할 수도 있지만, 한번에 가지고 있는 값을 모두 반환할 수도 있습니다.
iterable의 예시로는 container(list, string, tuple 등..), open files, open socket등이 있습니다.
또한 class에서 __iter__ 나 __getitem__ method를 구현한 경우 이를 iterable이라고 할 수 있습니다.
iterable하다고 해서 모두 iterator는 아닙니다.
즉, iterable중에서 한번에 하나씩 값을 반환하는 것만 iterator라고 할 수 있습니다.
하지만 python에서는 내장함수인 “iter”를 통해 list, dictionary같은 한번에 모든 값을 반환하는 개체도 iterator로 변환할 수 있습니다.
iter를 통해 iterator로 변환된 개체는 next함수를 통해 한번에 하나씩 값을 반환합니다.
반복문을 통해 한번에 하나씩 값을 반환하기 위해서는 iterable의 개체를 iterator로 변환해야합니다.
즉, list같은 iterable한 개체를 iter를 통해 iterator로 변환해주지 않으면 next함수를 통해 한번에 하나씩 값을 반환하지 못합니다.
하지만 우리는 iterable인 list를 for문에서 iterator로 변환하지 않습니다.
왜냐하면 for문에서는 자동적으로 iterable 개체를 iterator로 변환해주기 때문입니다.
iterator역시 iterable처럼 여러 개의 값을 가지고 있습니다.
하지만 iterable과 달리 iterator는 한번에 하나씩 값을 반환하기 위해서는 어디까지 반환한지 저장하고 있어야 next함수를 이용해 값을 순차적으로 가져올 수 있습니다.
순차적으로 값을 가져오다가 더 이상 반환할 값이 없다면 StopIteration이라는 예외를 발생시킵니다.
for문 같은 반복문에는 내부적으로 해당 예외처리가 되어있기 때문에 값을 모두 찾아도 오류없이 iterator를 처리하게 됩니다.
Generator
generator의 사전적인 정의는 루프의 반복 동작을 제어하는 특수한 함수입니다.
generator에 대해 깊게 살펴보기 전에 generator와 generator iterator에 대해 먼저 살펴봅시다.
이름이 헷갈리니 간단히 정리해봅시다.
generator와 generator iterator가 있고, 흔히 generator라고 표현하는 것은 generator함수 입니다.
또한, generator 함수는 generator iterator를 반환하는 함수입니다.
그렇다면 generator iterator란 무엇일까요?
먼저 generator iterator는 iterator의 속성을 갖습니다.
하지만 generator iterator는 iterator와는 명백히 다릅니다.
이를 iterator와 generator의 “next” method를 살펴봄으로써 알 수 있습니다.
iterator의 next method는 container에 있는 다음 항목을 반환하는 반면 generator의 next method는 generator 함수를 실행하거나 마지막을 실행된 yield구문에서 다시 시작한다고 정의 되어있습니다.
정의를 보니 궁금한 점이 또 생기게 됩니다.
그렇다면 yield구문이란 무엇일까요?
yield 구문은 yield 표현식이라고도 불리며 이는 괄호가 생략되었는지 괄호로 식이 감싸져 있는 지의 차이입니다.
yield는 generator를 정의하는 역할을 하므로 yield 구문이 있어야 generator라고 할 수 있습니다.
generator에서 yield를 만나면 현재의 상태가 멈추게 됩니다.
즉, generator의 내부의 모든 상태가 모두 멈춘채로 유지되며, generator를 호출하는 주체인 next 함수가 호출되면 이전의 상태를 이어서 실행됩니다.
yield를 이용한 예시를 한번 살펴봅시다.
def gen():
yield 1
yield 2
yield 3
def main():
print("=== print gen function ===")
print (gen())
print("=== print gen function in loop ===")
for g in gen():
print(g)
if __name__ == "__main__":
main()
result :
=== print gen function ===
<generator object gen at 0x000001C7345AB0B0>
=== print gen function in loop ===
1
2
3
gen()함수는 generator object이고 generator는 generator iterator를 반환합니다.
또한, for는 내부적으로 next를 호출하게 되어있으므로 gen()함수를 반복문에 이용할 수 있는 것입니다.
yield는 generator에서 값을 반환하거나 값을 받는 기능을 합니다.
이때 반환하는 값은 generator iterator입니다. 따라서 generator를 for문에서 사용할 수 있습니다.
값을 받는 기능은 send 함수를 통해 수행됩니다.
def gen():
value = 1
while True:
value = yield value
def main():
print ("===print gen function===")
g = gen()
print(g)
print (next(g))
print (g.send(2))
print (g.send(10))
print (g.send(5))
print (next(g))
if __name__ == "__main__":
main()
result :
===print gen function===
<generator object gen at 0x0000021DFC64B0B0>
1
2
10
5
None
gen()을 저장하고 있는 g를 출력해보면 해당 값이 generator object인 것을 알 수 있습니다.
또 해당 예제에서는 yield를 값을 입력하는 것과 반환하는 2가지로 사용하고 있습니다.
yield를 만나면 현재 가지고 있는 값을 반환하고 멈춥니다.
그리고 send 함수로 보낸 값이 할당되고 그 값이 출력됩니다.
마지막으로 send를 하지 않고 next함수를 호출하면 None이 출력됩니다.
yield 구문을 사용하면 generator의 상태를 멈추기도 하고, 이어서 진행하기도 합니다.
이때 generator에서는 내부의 상태를 저장합니다.
def gen(items):
count = 0
for item in items:
if count == 5:
return -1
count += 1
yield item
if __name__ == "__main__":
print("=== print gen ===")
for i in gen(range(10)):
print(i)
result :
=== print gen ===
0
1
2
3
4
내부의 상태를 저장하므로 반복문을 통해 지역변수의 값을 변화시켜도 다시 yield 구문을 통해 값을 반환한 후에도 해당 값이 저장됩니다.
또한 return문을 만나게 되면 return값은 반환하지 않고 오류도 발생하지 않습니다.
왜냐하면 return으로 값을 받으면 StopIteration 예외가 발생하게 되는데 내부적으로 예외처리가 되어있기 때문입니다.
모든 iterator는 generator로 대체할 수 있습니다.
또한 성능면에서 generator가 우수하므로 대체하는 것이 좋습니다.
앞선 포스팅에서 python2의 range보다 xrange의 메모리 사용률이 적다는 것을 언급한 적이 있습니다.
그 이유는 xrange가 generator를 사용하기 때문입니다.
range는 결과를 모두 연산한 결과를 list로 반환하는 반면 xrange는 xrange 객체를 반환합니다.
generator iterator는 값을 반환할 때 연산을 수행합니다.
즉, 연산을 수행하기 직전의 상태로 멈춰있기 때문에 사용하는 메모리의 크기가 작아질 수 있습니다.
메모리 효율성뿐만 아니라 geneator를 사용하면 iterator에 비해 실행시간도 크게 줄일 수 있습니다.
따라서 python3에서 iterator를 사용하던 대부분의 코드는 generator로 변경되었습니다.
Comprehension & Expression
Comprehension과 expression은 코드를 간결하게 작성하기 위한 문법입니다.
이는 lambda식 보다 직관적으로 사용이 가능한 문법입니다.
Comprehension의 경우 iterable한 개체를 대상으로 동작하고 expression은 generator에 대한 문법입니다.
num_list = [1, 2, 3]
country = ["korea", "japan", "china"]
capital = ["seoul", "tokyo", "beijing"]
def print_list_with_comprehension():
power_list = [x*x for x in num_list]
print(power_list)
def print_dict_with_comprehension():
comprehension_dict = {k:v for k, v in zip(country, capital)}
print(comprehension_dict)
def main():
print("print list")
print(num_list)
print_list_with_comprehension()
print("print dictionary")
print(country)
print(capital)
print_dict_with_comprehension()
if __name__ == "__main__":
main()
comprehension을 이용해 코드를 작성한다면 코드의 line수도 줄어들 수 있고 가독성도 좋아집니다.
list의 경우 대괄호를 이용해 iterable한 객체를 입력으로 사용하고 dictionary의 경우 중괄호와 zip함수를 이용해 comprehension을 사용했습니다.
comprehension을 사용해 각각의 수를 제곱한 list와 2개의 list를 하나의 dictionary로 합치는 작업을 간결하게 수행할 수 있었습니다.
num_list = [1, 2, 3, 4, 5]
def generate_list():
result = (x*x for x in num_list)
print(result)
return result
def generate_list_by_range():
result = (i for i in range(5))
print(result)
return result
def print_generator(items):
for item in items:
print(item)
def main():
print("print list")
print_generator(generate_list())
print_generator(generate_list_by_range())
if __name__ == "__main__":
main()
comprehension과 다르게 expression은 generator를 이용하고 ( ) 괄호를 사용합니다.
print list
<generator object generate_list.<locals>.<genexpr> at 0x00000207FFC8B0B0>
1
4
9
16
25
<generator object generate_list_by_range.<locals>.<genexpr> at 0x00000207FFC8B0B0>
0
1
2
3
4
정의한 2개의 함수인 generate_list와 generate_list_by_range에서 만든 result는 모두 generator object이고
print_generator를 통해 정상적으로 출력되는 것을 볼 수 있습니다.
Lazy Evaluation
Lazy Evaluation이란 프로그래밍 기법 중 하나로 프로그램의 실행을 지연시키는 기법입니다.
필요할 때까지 실행을 지연시킨 후, 필요할 때 실행시킴으로써 불필요한 실행을 줄일 수 있습니다.
generator를 이용해 lazy evaluation을 수행할 수 있습니다.
import time
def wait_return(num):
print ("sleep")
time.sleep(0.5)
return num
def print_items(items):
for i in items:
print(i)
def main():
print("===print list comprehension===")
iterator_list = [wait_return(i) for i in range(5)]
print_items(iterator_list)
print("===print generator expression===")
iterator_list = (wait_return(i) for i in range(5))
print_items(iterator_list)
if __name__ == "__main__":
main()
result :
===print list comprehension===
sleep
sleep
sleep
sleep
sleep
0
1
2
3
4
===print generator expression===
sleep
0
sleep
1
sleep
2
sleep
3
sleep
4
결과를 살펴보면 list comprehension과 generator expression이 다른 출력결과를 보이는 것을 알 수 있습니다.
wait_return() 함수는 "sleep"을 출력한 뒤 0.5초 프로그램을 멈춘 뒤, 인자로 받은 값을 반환합니다.
list comprehension의 경우 5개의 sleep이 먼저 모두 출력된 뒤, 값이 출력됐습니다.
반면에 generator expresssion은 sleep과 값이 번갈아 가면서 출력되었습니다.
우리는 여기서 Lazy Evaluation을 볼 수 있습니다.
generator expression의 경우 wait_return() 함수가 list를 만드는 시점에 실행되는 아니라 print_items()에서 값을 사용할 때 실행되는 것을 알 수 있습니다.
즉, 값이 로직에 의해서 미리 값이 계산되는 것이 아닌 실제 사용될 때 값을 계산합니다.
값을 미리 계산해 저장해 놓는 것이 아니기 때문에 많은양의 데이터를 메모리 영향 없이 처리할 수 있습니다.
Reference
프로그래밍 언어의 개념과 흐름에 대한 고찰, 파이썬답게 코딩하기
'Python' 카테고리의 다른 글
[python] 6. Thread (1) (0) | 2021.07.16 |
---|---|
5. 동시성과 병렬성 (0) | 2021.07.12 |
[python] 3. Decorator (0) | 2021.07.05 |
[python]2. Basic Pythonic Grammar (0) | 2021.06.30 |
[python]1. Pythonic (0) | 2021.06.28 |
- Total
- Today
- Yesterday
- 파이썬 문법
- 비대칭키암호
- 곱셈암호
- thread
- Cryptography
- wechall
- 파이썬 비동기
- systemhacking
- 동시발생지수
- 전치암호
- python
- 시저암호
- 파이썬
- Qiskit
- Encrypted Traffic Analysis
- 파이썬문법
- 덧셈암호
- 양자컴퓨팅
- 양자컴퓨터
- 복호화
- 암호화
- 공개키암호
- ETA 프로젝트
- 암호론
- Quantum entanglement
- pythonic
- 대칭키암호
- 파이썬암호
- Quantum Computing
- ACM Computing survey
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |