[파이썬 100강] 81강. functools.partial로 함수 인자를 고정해 재사용성 높이기
같은 함수를 여러 곳에서 쓰는데, 특정 인자만 매번 고정해서 호출하는 상황이 자주 있습니다. 예를 들어 로그 레벨은 항상 INFO, 통화 포맷은 항상 KRW, API 클라이언트 timeout은 항상 3초처럼요. 이때 매번 래퍼 함수를 새로 만들면 코드가 늘어나고, 호출 규칙이 흩어집니다. 이번 강의에서는 functools.partial로 기존 함수의 일부 인자를 미리 고정해 재사용 가능한 호출 단위를 만드는 방법을 바로 예제로 익힙니다.
핵심 개념
partial(func, *args, **kwargs)는 함수의 일부 인자를 미리 채운 새 호출 객체를 만듭니다.partial로 만든 객체는 함수처럼 호출할 수 있고, 남은 인자만 받아 실행됩니다.- 반복되는 "호출 규칙"을 데이터처럼 재사용할 수 있어 중복 코드와 실수를 줄입니다.
partial의 포인트는 "로직을 새로 짜는 것"이 아니라 "호출 계약을 고정"하는 데 있습니다. 초보 때는 보통 def send_info(msg): return send(msg, level="INFO") 같은 래퍼를 많이 만드는데, 이런 래퍼가 늘어나면 함수 이름만 많아지고 실제 규칙은 분산됩니다. 반면 partial은 어떤 함수를 기반으로 무엇을 고정했는지 한눈에 드러납니다.
또 하나 중요한 점은 partial이 단순 문법 설탕이 아니라는 것입니다. 실무에서는 콜백, 스케줄러, 재시도 큐, 이벤트 핸들러처럼 "나중에 호출될 함수"를 전달해야 하는 경우가 많습니다. 이때 partial로 인자를 묶어두면, 상태를 클로저에 숨기지 않고도 호출 의도를 명확히 전달할 수 있습니다.
기본 사용
예제 1) 가장 기본 패턴: 인자 일부 고정
>>> from functools import partial
>>>
>>> def multiply(a, b):
... return a * b
...
>>> double = partial(multiply, 2)
>>> triple = partial(multiply, 3)
>>> double(10)
20
>>> triple(10)
30
해설:
double = partial(multiply, 2)는multiply(2, x)형태를 고정한 함수처럼 동작합니다.- 기존 함수를 그대로 재사용하면서도 용도가 분명한 호출 단위를 만들 수 있습니다.
- 같은 로직을
lambda x: multiply(2, x)로도 만들 수 있지만, 팀 코드에서는partial이 의도가 더 선명합니다.
예제 2) 키워드 인자 고정으로 실무 함수 표준화
>>> from functools import partial
>>>
>>> def format_price(value: float, currency: str = "KRW", precision: int = 0) -> str:
... unit = "원" if currency == "KRW" else currency
... return f"{value:.{precision}f} {unit}"
...
>>> krw_price = partial(format_price, currency="KRW", precision=0)
>>> usd_price = partial(format_price, currency="USD", precision=2)
>>> krw_price(12900)
'12900 원'
>>> usd_price(12.9)
'12.90 USD'
해설:
- 통화/정밀도처럼 서비스 정책으로 거의 고정되는 값은
partial로 미리 묶어두면 호출자가 실수할 여지를 줄일 수 있습니다. - 호출부에서
format_price(x, "KRW", 0)를 계속 적는 대신, 의도 있는 이름(krw_price)으로 규칙을 공유할 수 있습니다.
예제 3) 콜백 전달 시 인자 미리 바인딩
>>> from functools import partial
>>>
>>> def notify(channel: str, level: str, message: str) -> str:
... return f"[{channel}][{level}] {message}"
...
>>> slack_info = partial(notify, "slack", "INFO")
>>> slack_error = partial(notify, "slack", "ERROR")
>>> slack_info("배치 작업 시작")
'[slack][INFO] 배치 작업 시작'
>>> slack_error("결제 동기화 실패")
'[slack][ERROR] 결제 동기화 실패'
해설:
- 이벤트 핸들러 등록 시
partial객체를 그대로 전달하면 상황별 콜백을 쉽게 구성할 수 있습니다. - 채널/레벨처럼 문맥 값은 고정하고, 실제 메시지처럼 변하는 값만 호출 시점에 받는 구조가 됩니다.
예제 4) partial 객체의 메타정보 확인
>>> from functools import partial
>>>
>>> def request(method, url, timeout=3, retries=1):
... return method, url, timeout, retries
...
>>> get_json = partial(request, "GET", timeout=5, retries=2)
>>> get_json
functools.partial(<function request at ...>, 'GET', timeout=5, retries=2)
>>> get_json.func.__name__
'request'
>>> get_json.args
('GET',)
>>> get_json.keywords
{'timeout': 5, 'retries': 2}
해설:
- 디버깅할 때
func/args/keywords를 확인하면 어떤 규칙이 고정됐는지 바로 파악할 수 있습니다. - 운영 장애에서 "어떤 설정으로 호출됐나"를 추적할 때 꽤 유용합니다.
자주 하는 실수
실수 1) 즉시 실행과 함수 전달을 혼동
>>> from functools import partial
>>>
>>> def greet(prefix, name):
... return f"{prefix} {name}"
...
>>> # 잘못된 예: 함수를 전달해야 하는데 결과를 먼저 실행함
>>> greet_hello = greet("안녕", "건우님")
>>> type(greet_hello)
<class 'str'>
원인:
- 콜백을 전달해야 하는 맥락에서
greet(...)를 먼저 실행해버리면 문자열(결과값)만 남고 함수가 사라집니다.
해결:
>>> greet_hello = partial(greet, "안녕")
>>> greet_hello("건우님")
'안녕 건우님'
핵심:
- "지금 실행할 것인가, 나중에 실행할 호출 객체를 만들 것인가"를 분리해 생각해야 합니다.
실수 2) 가변 객체를 고정 인자로 바인딩해서 상태가 누적됨
>>> from functools import partial
>>>
>>> def add_event(event, bucket):
... bucket.append(event)
... return bucket
...
>>> shared = []
>>> push = partial(add_event, bucket=shared)
>>> push("A")
['A']
>>> push("B")
['A', 'B']
원인:
- 리스트 같은 가변 객체를
partial에 묶으면 같은 객체가 계속 재사용됩니다. - 의도하지 않은 공유 상태가 생겨 버그가 됩니다.
해결:
>>> def add_event_safe(event, bucket=None):
... bucket = [] if bucket is None else bucket
... bucket.append(event)
... return bucket
...
>>> push_safe = partial(add_event_safe)
>>> push_safe("A")
['A']
>>> push_safe("B")
['B']
핵심:
partial은 인자를 "그 시점의 객체 참조"로 묶습니다. 상태 공유가 의도인지 반드시 확인하세요.
실수 3) 시그니처가 바뀐 줄 모르고 인자 순서를 틀림
- 증상:
TypeError: got multiple values for argument ...또는 엉뚱한 값 매핑 - 원인: 위치 인자를 고정한 뒤 호출할 때도 원래 순서 감각으로 넣어버림
- 해결: 가능하면 키워드 인자 고정을 우선하고, 팀 규칙으로 partial 팩토리 함수를 둬서 호출 계약을 명시
>>> from functools import partial
>>>
>>> def connect(host, port, timeout):
... return f"{host}:{port} timeout={timeout}"
...
>>> connect_local = partial(connect, "127.0.0.1", timeout=3)
>>> connect_local(5432)
'127.0.0.1:5432 timeout=3'
실무 패턴
-
입력 검증 규칙
partial로 만든 함수도 결국 원본 시그니처 제약을 따릅니다. 외부 입력(사용자/파일/API)은 먼저 검증하고 바인딩하세요.- 바인딩 대상이 가변 객체(list/dict)라면 공유 상태 의도를 코드 리뷰에서 반드시 확인합니다.
-
로그/예외 처리 규칙
- 장애 시
partial_obj.func.__name__,partial_obj.args,partial_obj.keywords를 로그에 남기면 재현 속도가 빨라집니다. - "콜백 등록 단계"와 "실행 단계" 로그를 분리하면 어디서 잘못됐는지 빠르게 좁힐 수 있습니다.
- 장애 시
-
재사용 함수/구조화 팁
make_api_callers(),build_notifiers()같은 팩토리에서 partial들을 한 번에 생성하면 서비스 규칙(timeout, retry, region)을 중앙에서 통제할 수 있습니다.- 라우터/작업 큐에서는
partial(task, job_id=..., trace_id=...)형태로 문맥을 묶어 전달하면 추적성이 올라갑니다.
-
성능/메모리 체크 포인트
partial자체 오버헤드는 매우 작지만, 대량 생성(수십만 개)하면 객체 수 증가로 메모리 부담이 생깁니다.- 핫패스에서는 partial을 루프 안에서 매번 만들지 말고, 루프 밖에서 한 번 만들어 재사용하는 습관이 좋습니다.
오늘의 결론
한 줄 요약: functools.partial은 래퍼 함수 남발을 줄이고, 반복 호출 규칙을 명시적으로 재사용하게 해주는 실전 도구다.
기억할 것:
- partial은 "코드 복붙"이 아니라 "호출 계약 고정"을 위해 사용합니다.
- 가변 객체 바인딩은 의도된 공유 상태인지 반드시 확인해야 합니다.
- 콜백/스케줄러/이벤트 핸들러에서 특히 강력합니다.
연습문제
send_metric(name, value, unit, namespace)함수를 만들고,namespace="python100"을 고정한send_course_metricpartial을 만들어 보세요.build_url(base, path, version, query)함수에서base와version을 고정해 API별 호출 함수를 2개(v1_users,v1_orders) 만들어 출력 결과를 확인해 보세요.- 작업 큐에 넣을 콜백으로
run_job(job_type, retry, payload)함수가 있을 때,retry=3을 고정한 partial과retry=0partial을 만들어 어떤 상황에서 각각 쓰면 좋은지 설명해 보세요.
이전 강의 정답
to_csv_cell(value)구현 (int,float,None처리)
>>> from functools import singledispatch
>>>
>>> @singledispatch
... def to_csv_cell(value):
... raise TypeError(f"unsupported: {type(value).__name__}")
...
>>> @to_csv_cell.register
... def _(value: int):
... return str(value)
...
>>> @to_csv_cell.register
... def _(value: float):
... return f"{value:.2f}"
...
>>> @to_csv_cell.register
... def _(value: type(None)):
... return ""
...
>>> to_csv_cell(7), to_csv_cell(3.14159), to_csv_cell(None)
('7', '3.14', '')
to_log(data)구현 (dict/list/str지원, 미지원 타입TypeError)
>>> from functools import singledispatch
>>>
>>> @singledispatch
... def to_log(data):
... raise TypeError(f"unsupported: {type(data).__name__}")
...
>>> @to_log.register
... def _(data: dict):
... return f"DICT keys={list(data.keys())}"
...
>>> @to_log.register
... def _(data: list):
... return f"LIST size={len(data)}"
...
>>> @to_log.register
... def _(data: str):
... return f"TEXT len={len(data)}"
...
>>> to_log({"id": 1, "name": "toby"})
"DICT keys=['id', 'name']"
>>> to_log([10, 20, 30])
'LIST size=3'
>>> to_log("hello")
'TEXT len=5'
PriceFormatter+@singledispatchmethod구현
>>> from functools import singledispatchmethod
>>>
>>> class PriceFormatter:
... @singledispatchmethod
... def fmt(self, value):
... return f"N/A:{value}"
...
... @fmt.register
... def _(self, value: int):
... return f"{value:,}원"
...
... @fmt.register
... def _(self, value: float):
... return f"{value:,.2f}원"
...
... @fmt.register
... def _(self, value: str):
... return value
...
>>> pf = PriceFormatter()
>>> pf.fmt(12000)
'12,000원'
>>> pf.fmt(12.5)
'12.50원'
>>> pf.fmt("문의")
'문의'
실습 환경/재현 정보
- 실행 환경:
condaenvpython100(Python 3.11.14) - 가정한 OS: macOS/Linux 공통
- 사용 모듈:
functools.partial - 재현 체크:
- partial 객체가 기대한 인자만 고정했는지 (
args/keywords) 확인 - 콜백 전달 시 함수 객체를 넘겼는지(즉시 실행 아님) 확인
- 가변 객체를 고정 인자로 묶었을 때 상태 공유가 의도인지 점검
- partial 객체가 기대한 인자만 고정했는지 (