Python프로그래밍Python 개발자

완전히 기다린 후에 다시 시작할 수 없게 하는 **Python** 코루틴 객체의 이유는 무엇인가요? 생성기 함수와 달리 각 호출에서 새로운 반복자를 인스턴스화합니까?

Hintsage AI 어시스턴트로 면접 통과

질문에 대한 답변

Python에서 async def를 통해 생성된 코루틴은 CPython 바이트코드 수준에서 CO_ITERABLE_COROUTINE 또는 네이티브 코루틴 플래그에 의해 작동되는 단일 사용 상태 기계로 구현됩니다. 비동기 함수를 호출하면 즉시 프레임 객체와 실행 상태를 포함하는 코루틴 객체를 반환하나, 이를 기다리는 것은 이 상태 기계를 완료로 이끌고, 이때 내부 f_lasti (마지막 명령) 마커가 끝에 도달하게 되어 프레임이 소진된 것으로 표시됩니다. Python 런타임은 재진입을 방지하기 위해 이 완료 플래그를 확인하고, 이후의 기다림이 발생하면 RuntimeError를 발생시키며, 코루틴도 단일의 개별 비동기 작업을 나타내고 선형 제어 흐름으로 설계되었기 때문입니다. 반대로, 생성기 함수는 팩토리입니다. 각 호출은 독립적인 스택 프레임과 명령 포인터를 가진 새로운 PyGenObject를 만들어 여러 개의 독립적인 반복자를 생산하고 각 반복자는 독립적인 실행 컨텍스트를 유지할 수 있습니다.

생활에서의 상황

개발 팀은 실패한 연결 시도를 지수 백오프를 사용하여 재시도해야 하는 강력한 WebSocket 클라이언트를 구축하고 있었습니다. 그들은 처음에 모듈 수준에서 연결 코루틴을 정의하고 재시도 로직에서 이를 재사용하려고 했습니다.

import asyncio async def establish_connection(): return await websockets.connect("wss://api.example.com") # 모듈 수준에서의 인스턴스화 connection_coro = establish_connection() async def retry_connect(max_attempts=3): for attempt in range(max_attempts): try: ws = await connection_coro # 두 번째 반복에서 실패 return ws except Exception: await asyncio.sleep(2 ** attempt)

문제는 두 번째 루프 반복에서 connection_coro를 다시 기다리려 할 때 발생하여, 첫 번째 성공적인 기다림이 이미 코루틴 객체를 소진시키기 때문에 RuntimeError가 발생하였습니다. 팀은 세 가지 아키텍처 솔루션을 고려했습니다.

한 가지 접근 방식은 RuntimeError를 잡은 후 except 블록 내에서 코루틴 객체를 수동으로 재구성하는 것이었습니다. 기술적으로 가능하지만, 이는 취약한 상태 관리를 도입하고 코드를 예외 처리로 소진 감지에 의존하게 되어 의미적으로 애매모호하며 연결 로직 내에서 정당한 런타임 오류를 가릴 수 있습니다.

또 다른 솔루션은 establish_connection을 클래스로 변환하여 __await__를 구현하도록 제안하여 재설정 가능한 대기 가능 객체를 생성하는 것이었습니다. 이는 팩토리 패턴을 제공하지만 불필요한 보일러플레이트와 복잡성을 추가하며, 연결을 설정하려는 간단한 의도를 가리게 하며, Python 런타임이 이미 함수 호출을 통해 제공하는 것을 중복하여 수동 상태 추적을 요구했습니다.

선택된 해결책은 비동기 함수를 팩토리로 취급하여 루프 내의 호출 지점을 이동시키는 것이었습니다. 이를 통해 각 반복에서는 깨끗한 코루틴 객체를 수신하게 되었습니다. ws = await establish_connection()로 리팩토링하여 각 시도는 독립적인 리소스 관리를 가진 새로운 상태 기계를 인스턴스화했습니다. 이는 Python의 설계 철학과 일치하여 비동기 함수는 일회성 계산적 미래의 생성자 역할을 하여 실패한 연결 시도를 다음 재시도와 잘 격리되도록 하는 깔끔하고 예외 없는 재시도 로직을 생성했습니다.

지원자가 자주 놓치는 점

코루틴을 변수에 저장하고 이를 기다리는 것을 잊는 것이 어떻게 리소스 누수를 초래하며, close()가 이를 어떻게 완화합니까?

지원자들은 종종 기다리지 않은 코루틴이 부작용 없이 간단히 가비지 컬렉션 된다고 가정합니다. 그러나 코루틴이 본체에 들어가고 await 표현식에서 중단된 경우(예: 데이터베이스 연결이나 잠금을 보유), 프레임은 이러한 리소스에 대한 참조를 유지하게 됩니다. 코루틴 객체에서 close()를 호출하면 프레임을 통해 GeneratorExit 예외를 강제로 발생시켜, 컨텍스트 관리자(async with)와 try/finally 블록이 즉시 리소스를 해제하게 합니다. 명시적인 close()가 없다면 이러한 리소스는 순환 가비지 수집이 실행될 때까지 유지되며, 이는 연결 풀 고갈 시나리오에는 너무 늦을 수 있습니다.

inspect.iscoroutine()inspect.isawaitable()은 어떻게 다르며, 이 구별이 일반 asyncio 유틸리티를 작성할 때 왜 중요한가요?

inspect.iscoroutine()async def 함수에 의해 생성된 네이티브 코루틴 객체에 대해서만 True를 반환하고, inspect.isawaitable()__await__를 구현한 모든 객체에 대해 True를 반환합니다. 여기에는 코루틴, 태스크, 미래 객체 및 사용자 정의 대기 가능 객체가 포함됩니다. 지원자들은 asyncio 함수인 ensure_future()가 코루틴만이 아닌 모든 대기 가능 객체를 수용한다는 사실을 놓치게 됩니다. 엄격하게 iscoroutine()을 확인하는 라이브러리를 작성하면 asyncio.Queue().get()이나 사용자 정의 미래 객체와 같은 유효한 대기 가능 객체가 거부되어 임의의 비동기 작업을 예약하기 위한 일반 유틸리티 함수에서 다형성이 깨지는 문제가 발생합니다.

async forawait가 비동기 생성기를 소비하는 데 서로 다른 점은 무엇이며, 왜 전자가 생성자 자체가 아닌 해당 생성기를 반환하는데 __aiter__가 필요합니까?

await는 코루틴 또는 미래를 완료할 때까지 소비하고 단일 값을 반환하며, 반면 async for는 비동기 반복기를 반복하고 각 async def 생성기 함수 내의 yield에서 중지합니다. 지원자들은 async for를 코루틴 목록을 기다리는 것과 혼동합니다. 매우 중요한 것은, __aiter__가 비동기 반복기 객체를 직접 반환해야 하며 (대기 가능 객체가 아님), 이는 Python 런타임이 순차적으로 반복 프로토콜을 시작하기 전에 반복기를 얻기 위해 __aiter__를 동기적으로 호출하기 때문입니다. __aiter__에서 코루틴을 반환하게 되면 TypeError가 발생하며, 프로토콜은 비동기 반복 상태 기계를 활성화하기 위해 반복기의 __anext__ 메소드에 즉시 접근할 수 있어야 합니다.