[파이썬 100강] 82강. operator 모듈로 정렬·집계 키 함수를 더 빠르고 읽기 좋게 만들기

[파이썬 100강] 82강. operator 모듈로 정렬·집계 키 함수를 더 빠르고 읽기 좋게 만들기

정렬, 그룹화, 집계 코드를 쓰다 보면 lambda x: x[1], lambda u: u["age"], lambda o: o.price 같은 짧은 람다를 계속 만들게 됩니다. 문제는 이런 코드가 많아질수록 의도는 비슷한데 표현이 제각각이라 가독성이 떨어지고, 디버깅할 때도 기준이 흩어집니다. 이번 강의에서는 operator 모듈의 itemgetter, attrgetter, methodcaller를 중심으로 반복되는 키 함수 패턴을 표준화하는 방법을 실전 예제로 익힙니다.


핵심 개념

  • operator.itemgetter()는 시퀀스/딕셔너리에서 특정 인덱스·키를 꺼내는 함수를 만듭니다.
  • operator.attrgetter()는 객체의 속성을 읽는 함수를 만듭니다.
  • operator.methodcaller()는 객체 메서드를 호출하는 함수를 만듭니다.
  • operator 함수들은 lambda를 완전히 대체하는 도구가 아니라, 반복되는 접근/호출 패턴을 일관되게 표현하는 도구입니다.

핵심은 "짧게 쓰기"보다 "같은 의도를 같은 방식으로 쓰기"입니다. 예를 들어 팀 코드 전체에서 정렬 키를 모두 lambda로 쓰면 가능한 표현이 너무 많습니다. 반면 itemgetter("price"), attrgetter("created_at")처럼 쓰면 코드만 봐도 "아, 이건 단순 접근이구나"를 즉시 이해할 수 있습니다. 특히 데이터 파이프라인, API 응답 정렬, 리포트 생성처럼 비슷한 키 함수가 많이 등장하는 영역에서 효과가 큽니다.

또 한 가지 장점은 다중 키 처리입니다. itemgetter("team", "score"), attrgetter("dept", "level")처럼 여러 값을 한 번에 튜플로 뽑아 정렬 우선순위를 명시할 수 있습니다. 즉, 로직은 그대로인데 코드가 더 선언적으로 바뀝니다.


기본 사용

예제 1) 리스트/딕셔너리 정렬에서 itemgetter 쓰기

>>> from operator import itemgetter
>>>
>>> rows = [
...     {"name": "민수", "score": 88},
...     {"name": "지연", "score": 95},
...     {"name": "현우", "score": 91},
... ]
>>>
>>> sorted(rows, key=itemgetter("score"), reverse=True)
[{'name': '지연', 'score': 95}, {'name': '현우', 'score': 91}, {'name': '민수', 'score': 88}]

해설:

  • itemgetter("score")는 각 원소에서 score 값을 꺼내 정렬 키로 사용합니다.
  • lambda r: r["score"]와 동작은 같지만, "키 접근" 의도가 더 선명하게 보입니다.

예제 2) 객체 정렬에서 attrgetter와 다중 키 사용

>>> from dataclasses import dataclass
>>> from operator import attrgetter
>>>
>>> @dataclass
... class User:
...     name: str
...     team: str
...     level: int
...
>>> users = [
...     User("가영", "backend", 2),
...     User("도윤", "backend", 1),
...     User("서진", "frontend", 1),
...     User("하린", "frontend", 3),
... ]
>>>
>>> sorted(users, key=attrgetter("team", "level"))
[User(name='도윤', team='backend', level=1), User(name='가영', team='backend', level=2), User(name='서진', team='frontend', level=1), User(name='하린', team='frontend', level=3)]

해설:

  • attrgetter("team", "level")(team, level) 튜플을 키로 만듭니다.
  • 팀 우선, 레벨 차순 같은 정렬 정책이 코드 한 줄에 명확히 드러납니다.

예제 3) methodcaller로 문자열 전처리 파이프라인 구성

>>> from operator import methodcaller
>>>
>>> normalize = methodcaller("strip")
>>> lower = methodcaller("lower")
>>>
>>> raw = ["  Python  ", " DATA ", " Ai "]
>>> cleaned = [lower(normalize(x)) for x in raw]
>>> cleaned
['python', 'data', 'ai']

해설:

  • methodcaller("strip")은 각 문자열의 strip()을 호출하는 함수 객체입니다.
  • 전처리 단계를 함수로 다루기 쉬워져서, map/filter 조합이나 파이프라인 코드에 재사용하기 좋습니다.

예제 4) 집계 기준 통일하기

>>> from operator import itemgetter
>>>
>>> sales = [
...     ("A", 1200),
...     ("B", 980),
...     ("C", 1530),
... ]
>>>
>>> top = max(sales, key=itemgetter(1))
>>> low = min(sales, key=itemgetter(1))
>>> top, low
(('C', 1530), ('B', 980))

해설:

  • 정렬뿐 아니라 max, min, heapq.nlargest 같은 API에서도 동일한 키 함수를 재사용할 수 있습니다.
  • "무엇을 기준으로 비교하는가"를 일관되게 유지할 수 있습니다.

자주 하는 실수

실수 1) itemgetter를 dict 키 추출 함수로 오해

>>> from operator import itemgetter
>>>
>>> d = {"name": "건우", "role": "admin"}
>>> g = itemgetter("name")
>>> g
operator.itemgetter('name')
>>> # 잘못된 기대: g가 즉시 값을 반환한다고 생각함
>>> g()
Traceback (most recent call last):
... TypeError: itemgetter expected 1 argument, got 0

원인:

  • itemgetter("name")는 "값"이 아니라 "함수"를 만듭니다. 실제 데이터(d)를 인자로 넣어 호출해야 합니다.

해결:

>>> g(d)
'건우'

핵심:

  • itemgetter/attrgetter/methodcaller는 모두 "호출 가능한 함수 객체 생성기"입니다.

실수 2) 없는 키/속성 접근을 바로 정렬에 사용

>>> from operator import itemgetter
>>>
>>> rows = [{"name": "a", "score": 10}, {"name": "b"}]
>>> sorted(rows, key=itemgetter("score"))
Traceback (most recent call last):
... KeyError: 'score'

원인:

  • 일부 데이터에 키가 누락되어 있는데, 검증 없이 바로 itemgetter("score")를 적용했습니다.

해결:

>>> sorted(rows, key=lambda r: r.get("score", -1))
[{'name': 'b'}, {'name': 'a', 'score': 10}]

핵심:

  • 누락 가능성이 있으면 lambda + get(default)가 더 안전할 때가 있습니다. operator는 "단순하고 확정된 접근"에 특히 강합니다.

실수 3) methodcaller 인자 위치를 잘못 전달

  • 증상: TypeError가 발생하거나 기대와 다른 결과가 나옵니다.
  • 원인: 예를 들어 replaceold, new 순서인데, 이를 헷갈려 뒤집어 전달.
  • 해결: 메서드 시그니처를 먼저 확인하고, 의미가 애매하면 직접 함수로 감싸 명시합니다.
>>> from operator import methodcaller
>>>
>>> swap = methodcaller("replace", "-", "/")
>>> swap("2026-02-17")
'2026/02/17'

실무 패턴

  • 입력 검증 규칙

    • 정렬/집계 전에 필수 키 존재 여부를 먼저 보장합니다.
    • 스키마가 불안정한 외부 데이터(JSON, CSV)는 정제 단계에서 기본값을 넣고, 이후 단계에서 itemgetter를 사용합니다.
  • 로그/예외 처리 규칙

    • KeyError, AttributeError는 "데이터 스키마 불일치" 신호로 보고 입력 샘플을 함께 로그에 남깁니다.
    • 운영에서는 정렬 실패 시 전체 중단보다 "문제 레코드 분리 + 경고 로그" 전략을 적용하면 안정적입니다.
  • 재사용 함수/구조화 팁

    • KEY_USER_SCORE = itemgetter("score"), KEY_ORDER_TIME = itemgetter("created_at")처럼 상수화하면 기준 변경이 쉬워집니다.
    • 파이프라인 모듈마다 키 함수를 중앙 선언해 팀 내 일관성을 유지합니다.
  • 성능/메모리 체크 포인트

    • 대량 정렬에서 itemgetter/attrgetter는 일반적으로 짧은 lambda보다 약간 유리한 경우가 많지만, 체감은 데이터 크기와 병목 위치에 따라 달라집니다.
    • 핵심은 미세 최적화보다 "키 함수 재사용"으로 실수 비용을 줄이는 것입니다.

오늘의 결론

한 줄 요약: operator 모듈은 정렬·집계·전처리에서 반복되는 키 함수/메서드 호출을 표준화해 코드 품질을 높이는 도구다.

기억할 것:

  • 스키마가 안정적이면 itemgetter/attrgetter가 가독성과 일관성을 크게 높입니다.
  • 누락 데이터 가능성이 있으면 안전한 기본값 전략(get, 사전 정제)을 먼저 적용합니다.
  • methodcaller는 문자열/객체 전처리를 함수형 스타일로 조립할 때 특히 유용합니다.

연습문제

  1. orders = [{"id": 1, "price": 5000}, {"id": 2, "price": 1200}, {"id": 3, "price": 8000}]price 내림차순으로 정렬해 보세요. (itemgetter 사용)
  2. Product(name, category, rank) dataclass를 만들고, category 오름차순 + rank 오름차순으로 정렬해 보세요. (attrgetter 다중 키)
  3. 문자열 리스트 [") Hello ", " PYTHON ", " data "]strip -> lower -> replace(" ", "") 순서로 정규화하는 코드를 methodcaller를 활용해 작성해 보세요.

이전 강의 정답

  1. send_metric(name, value, unit, namespace)에서 namespace="python100" 고정
>>> from functools import partial
>>>
>>> def send_metric(name, value, unit, namespace):
...     return f"{namespace}:{name}={value}{unit}"
...
>>> send_course_metric = partial(send_metric, namespace="python100")
>>> send_course_metric("lesson_progress", 82, "%")
'python100:lesson_progress=82%'
  1. build_url(base, path, version, query)에서 base/version 고정해 v1_users, v1_orders 만들기
>>> from functools import partial
>>>
>>> def build_url(base, path, version, query):
...     q = f"?{query}" if query else ""
...     return f"{base}/{version}/{path}{q}"
...
>>> v1_builder = partial(build_url, "https://api.example.com", version="v1")
>>> v1_users = partial(v1_builder, "users")
>>> v1_orders = partial(v1_builder, "orders")
>>> v1_users(query="page=1")
'https://api.example.com/v1/users?page=1'
>>> v1_orders(query="status=paid")
'https://api.example.com/v1/orders?status=paid'
  1. run_job(job_type, retry, payload)에서 retry=3retry=0 partial 비교
>>> from functools import partial
>>>
>>> def run_job(job_type, retry, payload):
...     return f"job={job_type}, retry={retry}, payload={payload}"
...
>>> run_retry3 = partial(run_job, retry=3)
>>> run_no_retry = partial(run_job, retry=0)
>>> run_retry3("sync_payment", payload={"order_id": 101})
"job=sync_payment, retry=3, payload={'order_id': 101}"
>>> run_no_retry("send_webhook", payload={"event": "ping"})
"job=send_webhook, retry=0, payload={'event': 'ping'}"

설명:

  • retry=3은 외부 API 호출처럼 일시적 실패 가능성이 있는 작업에 적합합니다.
  • retry=0은 멱등성 보장이 어렵거나, 실패 즉시 사람 확인이 필요한 작업에 적합합니다.

실습 환경/재현 정보

  • 실행 환경: conda env python100 (Python 3.11.14)
  • 가정한 OS: macOS/Linux 공통
  • 사용 모듈: operator (itemgetter, attrgetter, methodcaller)
  • 재현 체크:
    • 정렬 전 데이터 스키마(필수 키/속성) 확인
    • 누락 가능 데이터는 사전 정제 또는 기본값 전략 적용
    • methodcaller 인자 순서가 원본 메서드 시그니처와 일치하는지 점검