파이썬 강의

파이썬 중급자 필수 키워드: yield, async/await, nonlocal 완전 정복

마블e 2025. 3. 15. 21:36

파이썬 중급자 필수 키워드: yield, async/await, nonlocal 완전 정복

파이썬 프로그래밍 언어는 그 유연성과 강력함 덕분에 많은 개발자들에게 사랑받고 있습니다. 특히 중급자로 발전하기 위해 꼭 알아야 할 개념들이 존재합니다. 이번 글에서는 yield, async/await, nonlocal 키워드에 대해 깊이 있는 이해를 제공하고, 이를 활용하는 예제와 함께 설명하겠습니다.

1. yield의 이해

yield는 파이썬의 제너레이터를 만드는 키워드로, 함수가 호출될 때마다 상태를 저장하고, 다음 호출 시 그 상태에서 계속 실행할 수 있게 해줍니다. 이를 통해 메모리를 절약하고, 대량의 데이터를 처리할 때 유용하게 사용할 수 있습니다. 제너레이터를 사용하는 이유는 반복적인 계산에서 효율성을 극대화하기 위함입니다.

예를 들어, 피보나치 수열을 생성하는 제너레이터 함수를 다음과 같이 정의할 수 있습니다:

def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b

위의 코드에서 fibonacci 함수는 yield를 통해 수열의 각 요소를 차례로 반환합니다. 이 함수는 호출될 때마다 현재 상태를 기억하고 다음 호출 시 그 지점에서부터 실행을 재개합니다. 따라서, 메모리를 적게 사용하면서도 무한한 수열을 생성할 수 있습니다.

제너레이터의 사용은 많은 데이터 또는 긴 연산을 처리할 때 유용하는데, 특히 웹 크롤링이나 대규모 데이터 처리 시 많은 이점을 제공합니다. 비동기 처리와 결합하면 더욱 강력한 성능을 발휘하게 됩니다.

2. async/await의 개념

비동기 프로그래밍은 복잡한 작업을 논리적으로 순차적으로 실행하는 대신, 작업이 완료될 때까지 기다리지 않고 다음 작업을 수행할 수 있게 합니다. 파이썬에서는 이러한 비동기 프로그래밍을 asyncawait 키워드를 통해 구현할 수 있습니다.

async로 정의된 함수는 항상 코루틴을 반환하며, 내부에서 await 키워드를 사용하여 비동기 작업이 완료될 때까지 기다릴 수 있습니다. 이렇게 하면 블로킹 없이 다른 작업을 수행할 수 있습니다. 예를 들어, 웹 요청을 비동기로 처리하고 싶다면 다음과 같은 코드로 수행할 수 있습니다:

import asyncio
import aiohttp

async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()

async def main():
html = await fetch('http://example.com')
print(html)

asyncio.run(main())

위의 예제에서 fetch 함수는 네트워크 요청을 비동기적으로 수행하고, main 함수는 await를 통해 응답을 기다립니다. 이러한 방식은 I/O 작업에 매우 유리하여 높은 성능을 제공합니다.

3. nonlocal 키워드의 역할

nonlocal 키워드는 중첩 함수에서 외부 함수의 변수를 참조할 수 있도록 해줍니다. 이는 파이썬의 스코프 규칙에 따라, 내부 함수가 자신보다 바깥쪽의 변수(즉, 부모 함수의 변수)를 직접 수정할 수 없기 때문에 필요합니다. nonlocal를 사용하면 이러한 제약을 해결하고, 변수의 값을 변경할 수 있습니다.

예를 들어, 카운터를 구현하는 중첩 함수를 아래와 같이 정의할 수 있습니다:

def counter():
count = 0

def increment():
nonlocal count
count += 1
return count

return increment

이제 increment 함수를 호출할 때마다 count 값이 증가합니다. nonlocal을 사용하지 않으면, count는 내부 함수에서 독립적인 변수가 되어버려 원하는 결과를 얻지 못하게 됩니다.

4. yield와 async의 차이점

yieldasync는 모두 상태를 유지하는 방식으로 보일 수 있지만, 그 용도와 동작 방식은 다릅니다. yield는 제너레이터를 통해 값을 생성하고, 해당 값을 사용할 때마다 일시 중단과 재개가 이루어집니다. 반면, async는 비동기 처리로, API 호출이나 파일 입출력과 같이 시간 소모가 큰 작업을 효율적으로 처리하기 위해 사용됩니다.

비교하자면, yield는 데이터를 순차적으로 생성하고, async는 I/O 작업을 비동기로 처리하는 데 중점을 둡니다. 이 두 개념은 결합하여 사용될 때 더욱 훌륭한 성능을 발휘할 수 있습니다.

5. yield와 nonlocal의 연계

yieldnonlocal은 함께 사용될 수 있습니다. 카운터 예제를 제너레이터와 결합한다면, 다음과 같은 형태로 구현할 수 있습니다:

def counter():
count = 0

def increment():
nonlocal count
count += 1
yield count

return increment

이 경우, increment() 함수는 호출될 때마다 count를 증가시키고, 상태를 유지합니다. 제너레이터가 아니더라도, nonlocal을 사용하여 외부 상태를 수정할 수 있는 구조를 제공합니다.

6. async/await을 사용하는 비동기 처리 예제

비동기 처리를 활용하여 데이터베이스나 API 호출을 효율적으로 관리할 수 있습니다. 예를 들어, 여러 개의 API 호출을 동시에 수행하는 경우 다음과 같이 작성할 수 있습니다:

import asyncio
import aiohttp

async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(url) for url in urls]
return await asyncio.gather(*tasks)

urls = ['http://example.com', 'http://example.org', 'http://example.net']
results = asyncio.run(fetch_all(urls))

위의 코드에서 fetch_all 함수는 여러 URL에 대해 동시에 fetch를 수행합니다. asyncio.gather는 비동기 작업을 병렬로 실행하고, 모든 결과가 반환될 때까지 기다립니다. 이는 웹 스크래핑이나 대량의 데이터를 비동기로 처리할 때 유용합니다.

7. yield와 에러 처리

제너레이터를 사용할 때 에러 처리도 중요합니다. yield가 포함된 함수에서 예외가 발생하면, 예외가 발생한 이후에 제너레이터의 상태와 함께 종료됩니다. 이를 방지하기 위해, try / except 블록을 사용하는 것이 좋습니다. 예를 들어:

def safe_generator():
try:
yield 1 / 0  # 이 라인에서 예외 발생
except ZeroDivisionError:
yield 'Error occurred: division by zero'

위의 코드는 예외 발생 시 에러 메시지를 반환합니다. 이러한 방식은 프로그램의 안정성을 높이는 데 기여합니다.

8. async 코드에서의 에러 처리

비동기 작업에서도 에러 처리는 필수적입니다. await를 사용하는 경우, try/except 블록 안에서 비동기 작업을 수행하여 에러를 처리할 수 있습니다:

async def safe_fetch(url):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
except Exception as e:
return f'Error occurred: {str(e)}'

이와 같이 비동기 코드에서도 예외 처리를 통해 예상치 못한 오류로부터 프로그램을 보호할 수 있습니다.

9. nonlocal과 클로저

nonlocal은 클로저와 깊은 연결이 있습니다. 클로저는 함수가 정의된 외부 스코프의 변수를 기억하는 특성을 가지고 있습니다. nonlocal을 사용하면 이러한 클로저가 외부 함수의 변수를 변경할 수 있는 기능을 제공합니다.

예를 들어, 다음은 클로저를 활용한 카운터 예제입니다:

def closure_counter():
count = 0

def increment():
nonlocal count
count += 1
return count

return increment

위의 예제에서 increment 함수는 count를 변경하여 클로저의 특성을 활용합니다. 이러한 패턴은 상태를 유지해야 하는 여러 상황에서 유용하게 쓰입니다.

10. 비동기 코드를 통한 성능 향상

비동기 프로그래밍을 사용하면 어플리케이션의 성능을 크게 향상시킬 수 있습니다. 특히 I/O 작업이 많은 서비스를 구축할 경우, 비동기 코딩 패턴을 적용하는 것이 필수적입니다. CPU 연산이 아닌 I/O 중심의 작업에서 비동기 함수는 전통적인 동기 함수보다 더 높은 성능을 발휘합니다.

결과적으로, 파이썬의 async / awaityield, nonlocal 키워드를 잘 활용하면 더 효과적이고 유지보수하기 쉬운 코드를 작성할 수 있게 됩니다. 이러한 중급자 필수 키워드를 이해하고 활용하는 것은 파이썬 프로그래머로서의 다음 단계로 나아가는 큰 재무장이라고 할 수 있습니다.

11. yield와 async 조합의 사용 사례

yieldasync를 조합하면 동시에 대량의 데이터를 처리하면서도 비동기적으로 작업할 수 있는 특성을 가집니다. 예를 들어, 로그 파일을 읽어들이는 동시에 HTTP 요청을 보낸다면, 다음과 같은 방식으로 구현할 수 있습니다:

import asyncio
import aiohttp

async def read_log(file_path):
with open(file_path, 'r') as file:
for line in file:
yield line.strip()

async def fetch_logs(log_file):
async for log in read_log(log_file):
# 비동기 HTTP 요청 보내기
response_text = await fetch(log)
print(response_text)

asyncio.run(fetch_logs('server_logs.txt'))

위의 코드는 로그 파일을 한 줄씩 읽으면서 각 로그를 기반으로 비동기적으로 HTTP 요청을 보냅니다. 이 조합은 대규모 로그 분석에 특히 유용할 수 있습니다.

12. nonlocal 사용의 데이터 무결성

변수의 불변성을 유지하는 것과 관련해 nonlocal 키워드를 사용하는 것은 데이터 무결성을 유지하는 데 중요한 역할을 합니다. 다음과 같이 파라미터를 받아 상태를 변경하는 함수를 작성할 수 있습니다:

def state_keeper():
state = {}

def update(key, value):
nonlocal state
state[key] = value
return state

return update

updater = state_keeper()
print(updater("key1", "value1"))
print(updater("key2", "value2"))

이 코드에서는 update 함수가 nonlocal을 통해 외부 변수 state에 접근하고 이를 갱신합니다. 외부 상태를 안전하게 변경할 수 있어 데이터의 신뢰성을 높이는 데 기여합니다.

13. yield의 성능 이점

제너레이터는 메모리를 절약하는 데 유리합니다. 일반적인 함수와 다르게, 제너레이터는 전체 데이터를 메모리에 로드하는 것이 아니라, 필요할 때마다 하나씩 값을 생성합니다. 예를 들어, 큰 파일을 한 줄씩 처리해야 할 경우 제너레이터를 사용하는 것이 훨씬 더 효율적입니다:

def read_large_file(file_path):
with open(file_path, 'r') as file:
for line in file:
yield line.strip()

for line in read_large_file('large_file.txt'):
process(line)  # 각 라인을 바로 처리

위의 예제는 대용량 파일을 한 번에 메모리에 올리지 않고도 필요한 데이터만 읽어오는 방식을 보여줍니다. 이는 메모리 부담을 줄이고 성능을 최적화하는 데 큰 역할을 합니다.

14. async/await의 병렬 처리

비동기 프로그래밍의 강력한 기능 중 하나는 병렬 처리를 통한 성능 향상입니다. 다음 예제에서는 동시에 여러 작업을 처리하기 위해 asyncio.gather를 사용합니다:

async def main():
tasks = []
urls = ['http://example.com', 'http://example.org']

for url in urls:
tasks.append(fetch(url))

results = await asyncio.gather(*tasks)
for result in results:
print(result)

asyncio.run(main())

위 코드에서는 두 개의 URL에 대해 비동기적으로 HTTP 요청을 보내고 결과를 기다립니다. 이는 CPU 자원을 최적화하여 전반적인 성능을 강화하는 데 도움을 줍니다.

15. yield를 통한 상태 관리

제너레이터를 활용하여 상태를 효과적으로 관리하는 방법은 여러 분야에서 매우 유용합니다. 상태를 유지하는 카운터 배열과 같은 구조를 구현할 수 있습니다:

def counter(start=0):
while True:
yield start
start += 1

gen = counter()
for _ in range(5):
print(next(gen))  # 0, 1, 2, 3, 4

이 코드는 상태를 유지하며 카운터를 증가시키는 기능을 제공합니다. yield를 통해 함수는 이전 상태를 기억하고, 다음 호출에서 그 상태에서부터 다시 실행됩니다.

16. async/await의 최적화 포인트

비동기 코드에서 중요한 것은 최적화입니다. I/O 작업에서는 종종 이메일 전송, 파일 처리, DB 조회 등을 사용합니다. 이러한 작업이 비동기적으로 실행되면, 프로그램 전체의 반응성이 증가합니다. 예를 들어:

async def send_notifications():
await send_email()
await update_database()

async def main():
await asyncio.gather(send_notifications())

asyncio.run(main())

여기서는 이메일 전송과 데이터베이스 업데이트를 비동기로 처리하여 응답 시간을 줄이고 효율성을 높였습니다.

17. yield와 상태 변화의 통합

다양한 프로세스나 계산을 수행하면서 상태를 변경해야 할 때 yield를 사용할 수 있습니다. 예를 들어, 상태가 변경될 때마다 값을 반환하는 제너레이터를 작성할 수 있습니다:

def state_transition():
state = 0
while state < 5:
yield state
state += 1

for s in state_transition():
print(s)  # 0, 1, 2, 3, 4

이 코드는 상태가 변경될 때마다 각 상태를 출력함으로써, 상태 전이가 이루어지는 과정을 시각화를 시도하고 있습니다.

18. nonlocal의 복잡한 스코프

nonlocal을 사용하면 중첩 함수 안에서 바깥쪽 함수의 변수를 효과적으로 제어할 수 있습니다. 다음과 같은 관계에 있는 예제를 보면 이해하기 쉬울 것입니다:

def outer():
x = "initial"

def inner():
nonlocal x
x = "modified"
return x

inner()
return x

print(outer())  # modified

이 예제에서 inner 함수는 nonlocal을 통해 outer 함수의 로컬 변수를 수정합니다. 외부 상태를 조작할 수 있어 유연한 프로그래밍이 가능합니다.

19. yield와 async의 실무 활용 예제

현업에서 yieldasync를 조합하여 사용할 수 있는 방법 중 하나는 실시간 데이터 스트리밍입니다. 대규모 데이터의 변경 사항을 실시간으로 수신하고 업데이트하는 데 유용합니다. 다음은 그 예입니다:

import asyncio
import random

async def data_stream():
while True:
await asyncio.sleep(1)  # 데이터 소스에서 정기적으로 데이터 받음
yield random.randint(1, 100)

async def process_stream():
async for data in data_stream():
print(f"Received data: {data}")

asyncio.run(process_stream())

이 코드는 매초마다 새로운 데이터를 생성하고 이를 비동기적으로 처리하는 예시를 보여줍니다. 실시간 데이터 모니터링, 로그인 등의 시스템에 매우 유용하게 쓰일 수 있습니다.

20. 비동기 처리에서의 상태 관리

비동기 프로그래밍에서도 상태 관리가 필요합니다. 다양한 비동기 작업 간의 상태 정보를 유지하는 것이 중요합니다. 예를 들어, 다음과 같은 코드에서 상태를 유지할 수 있습니다:

async def async_counter(start=0):
count = start

while count < 5:
yield count
count += 1
await asyncio.sleep(1)  # 비동기 작업을 적절히 병행

async def main():
async for number in async_counter():
print(number)

asyncio.run(main())

위 예시는 비동기적으로 카운트를 진행하면서 매 초마다 현재 카운트를 출력합니다. 이와 같은 구조는 비동기 프로그래밍에서 상태 정보를 추적하면서도 자원을 효율적으로 사용할 수 있게 합니다.

21. 상황에 따라 유연한 함수 설계

nonlocal을 사용하여 여러 개의 변수 값을 유동적으로 관리할 수 있습니다. 상황에 따라 변할 수 있는 파라미터를 설정하여 다양한 상황에서 사용이 가능하도록 설계할 수 있습니다:

def flexible_function(param=0):
var = param

def inner():
nonlocal var
var += 1
return var

return inner

func = flexible_function(10)
print(func())  # 11
print(func())  # 12

이 코드는 인스턴스화할 때 기본 값을 받아서, 그 값을 기반으로 내부 상태를 유동적으로 변경할 수 있는 구조입니다.

22. 비동기 실행의 병렬화

비동기 실행의 큰 장점 중 하나는 손쉽게 병렬화를 구현할 수 있다는 점입니다. 다음 예제는 여러 비동기 작업을 동시에 실행하는 방법을 보여줍니다:

async def perform_task(num):
await asyncio.sleep(num)  # 작업 시뮬레이션
return f'Task {num} completed!'

async def main():
tasks = [perform_task(i) for i in range(1, 6)]
results = await asyncio.gather(*tasks)
for result in results:
print(result)

asyncio.run(main())

여기서는 1부터 5까지의 숫자를 비동기적으로 처리하고 동시에 결과를 출력합니다. I/O 중심의 작업에서 특히 유용한 구조입니다.

23. yield란 무엇인가

yield는 파이썬 제너레이터의 핵심 기능으로, 이 키워드를 사용하면 함수를 호출할 때마다 중단 가능하며, 대량의 데이터에서 하나씩 반환할 수 있는 효율적인 방법을 제공합니다.

def countdown(n):
while n > 0:
yield n
n -= 1

이 함수는 카운트다운을 제너레이터 형식으로 진행합니다. yield를 사용하면 메모리를 절약하며, 필요 시 상태를 유지하여 여러 데이터 요소를 효율적으로 반환할 수 있습니다.

24. async/await 사용 시의 유의사항

비동기 프로그래밍을 사용할 때 유의할 점은, 작업이 비동기적으로 진행됨에도 불구하고 순차적인 프로세스를 유지해야 하는 상황입니다. 이럴 경우 await 키워드를 적재적소에 사용하여 원하는 순서로 작업을 수행할 수 있습니다. 다음은 그 예시입니다:

async def step_one():
await asyncio.sleep(1)
print("Step one complete.")

async def step_two():
await asyncio.sleep(1)
print("Step two complete.")

async def main():
await step_one()
await step_two()

asyncio.run(main())

이 코드에서는 step_one이 먼저 완료되고 그 다음에 step_two가 호출됩니다. 각 단계에서 await를 통해 비동기 처리임에도 불구하고 원하는 순서대로 진행하게 됩니다.

25. conclusion

이제 yield, async/await, nonlocal 키워드를 통해 간단한 상태 관리를 넘어 비동기 작업까지 효율적으로 처리할 수 있는 방법에 대해 알아보았습니다. 각 키워드는 서로 보완관계에 있으며, 중급자로서 이러한 개념을 이해하고 활용하는 것은 앞으로의 소프트웨어 개발 여정에 큰 도움이 될 것입니다. 이 기법들을 실무에 적용함으로써 코드의 효율성과 유지보수성을 높일 수 있는 기회를 가지길 바랍니다.

향후 개발자들이 중급자로 성장하기 위해서는 yield, async/await, nonlocal 키워드를 철저히 이해하고 이를 적절히 활용하는 것이 중요합니다. 이 세 가지 키워드는 각각 메모리 관리, 이뤄지지 않는 작업의 비동기 처리, 외부 함수의 변수를 수정하는 방법을 제공합니다. 이러한 개념들을 깊게 배워 실제 프로젝트에 적용함으로써, 성능 면에서도 큰 이점을 얻을 수 있습니다. 각각의 키워드를 적절하게 활용할 줄 아는 개발자는 복잡한 프로그램을 더 효율적으로 구성하고, 유지 관리할 수 있는 능력을 가질 것입니다. 따라서 이 글에서 설명한 내용을 바탕으로 직접 실습하며 실력을 다지기를 권장합니다.

키워드: Python, yield, async/await, nonlocal, 제너레이터, 비동기 프로그래밍, 클로저, 데이터 처리, 상태 관리, 성능 최적화