Python 미드레벨 코딩 면접
새 면접Q. Python에서 heapq 모듈을 사용하여 최소 힙(Min Heap)과 최대 힙(Max Heap)을 각각 구현하는 방법을 설명하고, heappush()와 heappop() 연산의 시간복잡도를 설명해주세요. 또한 실무에서 우선순위 큐가 필요한 시나리오와 일반 리스트 정렬 대비 힙을 사용했을 때의 성능상 이점을 설명해주세요.
heapq는 기본적으로 최소 힙을 제공하며, 최대 힙을 구현하려면 값을 음수로 변환하여 삽입하고 꺼낼 때 다시 음수로 변환합니다. heappush()와 heappop()은 모두 O(log n)의 시간복잡도를 가지며, 힙 구조를 유지하기 위해 상향/하향 조정을 수행합니다. 실무에서는 실시간 데이터 스트림에서 상위 K개 요소를 추적하거나, 작업 스케줄링 시스템에서 우선순위가 가장 높은 작업을 빠르게 선택해야 할 때 사용합니다. 전체 정렬은 O(n log n)이지만 힙을 사용하면 K개만 유지하면 되므로 공간복잡도 O(K)로 메모리 효율적이며, 지속적으로 최솟값/최댓값만 필요한 경우 매번 정렬하는 것보다 훨씬 효율적입니다.
- • heapq는 최소 힙 기본 제공, 최대 힙은 음수 변환으로 구현
- • heappush/heappop 모두 O(log n) 시간복잡도
- • 실시간 Top-K 문제나 우선순위 스케줄링에 적합
- • 전체 정렬 대비 메모리 효율적이고 부분 정렬 시 성능 우수
heapq.nlargest()와 heapq.nsmallest() 함수는 내부적으로 어떤 알고리즘을 사용하며, n이 작을 때와 클 때 각각 어떤 방식으로 최적화되는지 설명해주세요.
Q. Python에서 concurrent.futures 모듈의 ThreadPoolExecutor와 ProcessPoolExecutor의 차이점을 설명하고, submit()과 map()의 차이점을 설명해주세요. 또한 실무에서 웹 스크래핑 작업과 이미지 리사이징 작업을 병렬 처리할 때 각각 어떤 Executor를 선택해야 하는지 그 이유와 함께 설명해주세요.
ThreadPoolExecutor는 스레드 풀을 사용하여 I/O-bound 작업에 적합하고, ProcessPoolExecutor는 프로세스 풀을 사용하여 CPU-bound 작업에 적합하며 GIL을 우회합니다. submit()은 단일 작업을 비동기로 제출하고 Future 객체를 즉시 반환하여 개별 작업의 결과를 추적할 수 있지만, map()은 여러 인자에 대해 함수를 매핑하고 결과를 순서대로 반환하는 이터레이터를 제공합니다. 웹 스크래핑은 네트워크 I/O 대기 시간이 주요 병목이므로 ThreadPoolExecutor가 적합하며 메모리 오버헤드가 적고 컨텍스트 스위칭이 빠릅니다. 이미지 리사이징은 CPU 연산이 집중적이므로 ProcessPoolExecutor를 사용해야 GIL의 제약 없이 멀티코어를 활용할 수 있지만, 프로세스 간 데이터 전송(pickling) 비용을 고려해야 합니다.
- • ThreadPoolExecutor는 I/O-bound, ProcessPoolExecutor는 CPU-bound 작업에 적합
- • submit()은 개별 Future 반환, map()은 순서 보장된 이터레이터 반환
- • 웹 스크래핑은 I/O 대기가 많아 Thread 사용
- • 이미지 처리는 CPU 집약적이라 Process 사용으로 GIL 우회
ProcessPoolExecutor를 사용할 때 객체를 직렬화(pickle)할 수 없어서 발생하는 문제를 어떻게 해결할 수 있으며, 공유 메모리를 사용해야 하는 경우 multiprocessing.Manager나 shared_memory를 어떻게 활용하는지 설명해주세요.
Q. Python에서 리스트 컴프리헨션, map/filter 함수, for 루프의 성능을 비교하고, 각각 어떤 상황에서 사용하는 것이 적절한지 설명해주세요. 또한 실무에서 코드 가독성과 성능 사이의 트레이드오프를 고려할 때 어떤 기준으로 선택해야 하는지 설명해주세요.
리스트 컴프리헨션은 C 레벨에서 최적화되어 일반 for 루프보다 약 2배 빠르며, map/filter는 함수 호출 오버헤드가 있어 람다 사용 시 리스트 컴프리헨션보다 느릴 수 있지만 내장 함수와 함께 사용하면 빠릅니다. 리스트 컴프리헨션은 간단한 변환과 필터링에 적합하고 가독성이 좋으며, map/filter는 함수형 프로그래밍 스타일이 필요하거나 이미 정의된 함수를 재사용할 때 적합합니다. for 루프는 복잡한 로직이나 중간 변수가 필요한 경우, 또는 조기 종료가 필요할 때 사용합니다. 실무에서는 성능 차이가 미미한 경우 팀의 코딩 스타일과 가독성을 우선하고, 병목 구간에서만 프로파일링 후 최적화하며, 복잡한 리스트 컴프리헨션보다는 명확한 for 루프가 유지보수에 유리합니다.
- • 리스트 컴프리헨션이 일반적으로 가장 빠르고 Pythonic함
- • map/filter는 내장 함수와 사용 시 효율적이나 람다 사용 시 오버헤드 존재
- • 복잡한 로직이나 조기 종료가 필요하면 for 루프 사용
- • 성능보다 가독성 우선, 병목 구간만 프로파일링 후 최적화
리스트 컴프리헨션이 중첩되어 3중, 4중으로 복잡해지는 경우 어떻게 리팩토링해야 하며, 제너레이터 표현식으로 전환하는 것이 항상 메모리 효율적인지 반례가 있다면 설명해주세요.
Q. Python에서 예외 처리 시 except Exception과 except BaseException의 차이를 설명하고, KeyboardInterrupt나 SystemExit를 잡으면 안 되는 이유를 설명해주세요. 또한 실무에서 try-except-else-finally 블록을 각각 어떤 용도로 사용해야 하는지 구체적인 예시와 함께 설명해주세요.
Exception은 일반적인 프로그램 에러를 나타내며, BaseException은 시스템 레벨의 예외(KeyboardInterrupt, SystemExit, GeneratorExit 등)를 포함합니다. KeyboardInterrupt나 SystemExit를 잡으면 프로그램 종료 시그널을 무시하게 되어 사용자가 프로그램을 강제 종료하지 못하거나 정상적인 종료 프로세스가 방해받을 수 있습니다. try 블록에는 예외가 발생할 수 있는 코드를, except에는 특정 예외 처리를, else에는 예외가 발생하지 않았을 때만 실행할 코드를, finally에는 예외 발생 여부와 관계없이 항상 실행해야 하는 정리 코드(리소스 해제 등)를 작성합니다. 실무에서 데이터베이스 트랜잭션의 경우 try에서 쿼리 실행, except에서 롤백, else에서 커밋, finally에서 연결 종료를 처리하는 패턴이 일반적입니다.
- • Exception은 일반 에러, BaseException은 시스템 레벨 예외 포함
- • KeyboardInterrupt/SystemExit 캐치 시 프로그램 제어 불가 문제
- • else는 예외 미발생 시, finally는 항상 실행되는 정리 코드용
- • DB 트랜잭션: try(실행)-except(롤백)-else(커밋)-finally(연결종료) 패턴
Python 3.11에서 도입된 Exception Groups와 except* 문법이 기존 예외 처리와 어떻게 다르며, 어떤 실무 시나리오에서 유용한지 설명해주세요.
Q. Python의 functools 모듈에서 lru_cache, cache, cached_property의 차이점을 설명하고, 각각의 내부 동작 원리와 적합한 사용 사례를 설명해주세요. 또한 lru_cache를 사용할 때 메모리 누수를 방지하기 위해 주의해야 할 점과 maxsize 파라미터를 설정하는 기준을 설명해주세요.
lru_cache는 함수 호출 결과를 LRU(Least Recently Used) 방식으로 캐싱하며 maxsize로 크기를 제한할 수 있고, cache는 Python 3.9부터 도입된 무제한 캐시(maxsize=None인 lru_cache)입니다. cached_property는 클래스의 속성에 사용되며 인스턴스별로 한 번만 계산되고 결과를 인스턴스 딕셔너리에 저장합니다. lru_cache는 재귀 함수(피보나치 등)나 반복적으로 호출되는 순수 함수에 적합하고, cache는 전역 설정이나 초기화 함수에, cached_property는 비용이 큰 계산 속성에 사용됩니다. 메모리 누수를 방지하려면 가변 객체를 인자로 받지 않도록 하고(해시 불가), 무한정 증가할 수 있는 입력에는 적절한 maxsize를 설정하며, 장시간 실행되는 서비스에서는 cache_clear()로 주기적으로 캐시를 비워야 합니다. maxsize는 예상되는 고유 입력 조합의 수와 메모리 제약을 고려하여 설정하며, 일반적으로 128이 기본값입니다.
- • lru_cache는 LRU 캐싱(크기 제한), cache는 무제한, cached_property는 인스턴스 속성용
- • lru_cache는 순수 함수, cached_property는 비용 큰 계산 속성에 적합
- • 가변 객체 인자 사용 금지, 적절한 maxsize 설정으로 메모리 관리
- • 장시간 실행 시 cache_clear()로 주기적 정리 필요
lru_cache 데코레이터를 클래스 메서드에 사용할 때 인스턴스가 캐시 키에 포함되어 발생하는 문제와, 이를 해결하기 위한 방법(예: 인스턴스별 캐시 vs 클래스 레벨 캐시)을 설명해주세요.
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!