Pythonic 코드와 표준 라이브러리 — Stream/Collection을 Python답게 · 퀴즈

7 문항 · Bloom: Understand:1, Apply:3, Analyze:3

Q1 Apply mcq_single

Java의 `users.stream().filter(u -> u.isActive()).map(User::getEmail).collect(toList())` 코드를 가장 Pythonic하게 한 줄로 옮긴 것은?

정답: A
Java Stream의 `filter` → `if`, `map` → `expr`, `collect(toList())` → 바깥 `[]`로 1:1 매핑되는 list comprehension이 정답입니다. Python 커뮤니티가 권장하는 가장 직관적인 표현입니다.
오답 해설:
  • B. `()`로 감싸면 generator expression이라 결과는 list가 아닌 lazy iterator입니다. `collect(toList())`와는 타입이 다릅니다.
  • C. 동작은 같지만 Python에서는 `map`/`filter` + `lambda` 조합이 가독성이 떨어져 비권장 — Java Stream을 그대로 옮긴 듯한 형태로 Pythonic하지 않습니다.
  • D. `{}`는 set comprehension이라 중복이 제거됩니다. `toList()`와 의미가 달라집니다.
Q2 Analyze mcq_single

10GB짜리 access.log에서 5xx 응답만 세는 코드를 작성합니다. 메모리 안전과 모델 일관성 관점에서 가장 적절한 것은?

정답: A
generator expression은 한 줄씩 lazy하게 읽어 상수 메모리로 동작합니다. `sum()`이 터미널 연산 역할을 하면서 비로소 평가가 시작되는 점이 Java `Files.lines(path).filter(...).count()`와 동일한 lazy pipeline 모델입니다.
오답 해설:
  • B. list comprehension은 eager — 매칭된 모든 줄을 메모리에 올린 뒤 `len()`을 계산하므로 OOM 위험이 있습니다. Java Stream의 `collect(toList())` 후 `size()`를 부르는 것과 같은 안티패턴입니다.
  • C. `readlines()`가 파일 전체를 한 번에 list로 읽어 들이므로 generator로 감싸도 이미 메모리는 폭발한 뒤입니다.
  • D. `read()`로 파일 전체를 문자열로 메모리에 올린 뒤 카운트하므로 가장 위험합니다. 추가로 ` 5` 문자열이 본문에 우연히 포함된 경우도 카운트되어 결과도 부정확합니다.
Q3 Analyze mcq_multi

다음 중 `@dataclass`, `typing.NamedTuple`, `pydantic.BaseModel`의 용도 매핑으로 **올바른 것 2개**를 고르시오.

정답: A, B
결정 기준은 **외부 입력 = Pydantic, 내부 계산 = dataclass, 메모리 critical micro 값 = NamedTuple**입니다. A, B는 핵심 매핑을 정확히 반영합니다.
오답 해설:
  • C. NamedTuple은 tuple 기반 micro 값 객체일 뿐 검증/직렬화 기능이 없습니다. 외부 입력에는 Pydantic이 정답입니다.
  • D. `@dataclass`는 '구조만' 표현하는 plain value object로 검증을 하지 않습니다. `User(email='not-an-email')`도 그대로 통과합니다.
  • E. Pydantic은 인스턴스화마다 검증을 돌리므로 `@dataclass`보다 약 5~10배 느립니다. 도메인 내부까지 끌고 가면 매 변환·복제마다 검증 비용을 다시 치릅니다.
Q4 Understand true_false

Python의 `with open(...) as f:` 블록은 Java try-with-resources와 동일하게 `close`를 보장하며, 추가로 `__exit__`이 `True`를 반환하면 블록 내부에서 발생한 예외를 삼킬 수 있다.

정답: A
context manager 프로토콜의 핵심입니다. `__exit__`은 항상 호출되어 close를 보장하고, 반환값이 `True`면 발생한 예외를 흡수합니다 — Java try-with-resources의 `AutoCloseable`에는 없는 기능이며, 잘 쓰면 강력하지만 모르고 쓰면 디버깅 지옥의 입구가 됩니다.
오답 해설:
  • B. JVM 베테랑이 `with`를 단순히 try-with-resources의 사본으로 가정하면 놓치기 쉬운 차이점입니다. Java에는 없는 예외 흡수 시맨틱이 실제로 존재합니다.
Q5 Apply mcq_single

임시 디렉토리를 만들고 블록 종료 시 자동으로 정리하는 context manager를 가장 간결하게 작성한 것은?

정답: B
`@contextmanager` 데코레이터로 함수 하나에 `yield`를 한 번 쓰는 패턴이 가장 간결합니다. `try/finally`로 감싸야 블록 내부에서 예외가 나든 정상 종료하든 `rmtree`가 보장됩니다 — Java try-with-resources의 `finally` 의미를 함수 본문에 그대로 옮긴 형태입니다.
오답 해설:
  • A. 동작은 맞지만 클래스로 `__enter__`/`__exit__`을 직접 구현하는 방식은 'Python답게 가장 간결한' 답이 아닙니다. Pythonic한 정답은 `@contextmanager` 데코레이터입니다.
  • C. `@contextmanager` 데코레이터가 빠져 있어 그냥 generator일 뿐, `with` 문에 쓸 수 없습니다.
  • D. 데코레이터는 있지만 `try/finally`가 없습니다. `yield d` 직후 블록 내부에서 예외가 발생하면 `shutil.rmtree(d)`가 호출되지 않아 임시 디렉토리가 누수됩니다.
Q6 Analyze mcq_single

100개의 URL을 동시에 GET하기 위해 `httpx.AsyncClient`로 fan-out 코드를 짭니다. 다음 중 **Kotlin coroutine 멘탈 모델로는 자연스럽지만 Python asyncio에서는 잘못된** 동작을 일으키는 것은?

정답: C
Python asyncio는 **single-thread event loop** 위에서 돕니다. `time.sleep(1)`은 OS 호출이라 thread 자체를 잠재우므로 모든 coroutine이 함께 1초간 멈춥니다. JVM coroutine은 thread pool 위에서 돌기 때문에 한 thread가 자도 다른 thread는 진행 가능하지만, Python에서는 모델 위반입니다. 정답은 `await asyncio.sleep(1)`로 양보 지점을 만드는 것입니다.
오답 해설:
  • A. `asyncio.gather`는 Kotlin `awaitAll()`과 등가이며 fan-out의 정석 패턴입니다.
  • B. `asyncio.run`은 main + `runBlocking` 역할의 entry point — 정상 사용입니다.
  • D. `create_task`는 Kotlin `launch`와 1:1 매핑되는 정상 패턴입니다.
Q7 Apply short_answer

FastAPI 핸들러 안에서 무거운 numpy 행렬곱(CPU bound, 약 2초 소요)을 호출해야 합니다. 단순히 `async def`로 감싸면 무엇이 잘못되는지, 올바른 해결책은 무엇이며 Kotlin의 어떤 도구와 매핑되는지를 3~4문장으로 설명하시오.

채점 기준:
  • single-thread event loop 위에서 CPU bound가 돌면 그 2초 동안 다른 모든 coroutine이 굶는다는 점 명시 (1pt)
  • `async def`만으로는 비동기가 되지 않으며 `await`가 있어도 CPU 점유는 양보되지 않는다는 점 명시 (1pt)
  • 해결책으로 `asyncio.to_thread(fn, ...)` 또는 `loop.run_in_executor(ProcessPoolExecutor(), ...)` 중 하나 이상 제시 (1pt)
  • Kotlin `Dispatchers.IO`(thread pool offload) 또는 `Dispatchers.Default`(CPU pool)와의 역할 매핑 언급 (1pt)