Created
April 27, 2025
Created by
D
DaEun KimTags
Python
Property
- 레퍼런스 :
Python Enhancement Proposals (PEPs) PEP 612 – Parameter Specification Variables | peps.python.org
파이썬 3.10 미만의 버전에서는 decorated 함수 파라미터의 타입 추적이 되지 않는다.
이는 mypy 에서도 잡아내지 못하기 때문에 런타임 오류를 일으키기 쉽다.
1 import logging
1 import logging
2 from typing import Callable, TypeVar
3
4 logger = logging.getLogger(__name__)
5 logger.setLevel(logging.INFO)
6 logger.addHandler(logging.StreamHandler())
7
8 ANYTHING = TypeVar("ANYTHING") # 리턴 타입이 아무거나 다 될 수 있음을 나타낼 용도
9
10
11 def log_to_database():
12 logger.info("Logging to database")
13
14
15 def add_logging(f: Callable[..., ANYTHING]) -> Callable[..., ANYTHING]:
16 def inner(*args: object, **kwargs: object) -> ANYTHING:
17 log_to_database()
18 return f(*args, **kwargs)
19 return inner
20
21 @add_logging
22 def takes_int_str(x: int, y: str) -> int:
23 return x + 7
24
25
26 takes_int_str(1, "A")
27 takes_int_str("B", 2) # 런타임 오류 발생
$ mypy runtime_error_1.py
Success: no issues found in 1 source file
$ param_spec python runtime_error_1.py
Logging to database
Logging to database
Traceback (most recent call last):
File "/Users/daeun/python-study/param_spec/runtime_error_1.py", line 27, in <module>
takes_int_str("B", 2)
~~~~~~~~~~~~~^^^^^^^^
File "/Users/daeun/python-study/param_spec/runtime_error_1.py", line 18, in inner
return f(*args, **kwargs)
File "/Users/daeun/python-study/param_spec/runtime_error_1.py", line 23, in takes_int_str
return x + 7
~~^~~
TypeError: can only concatenate str (not "int") to str
파이썬 버전 3.10 에 추가된 ParamSpec
으로 데코레이터의 파라미터 타입을 지정하면 decorated 함수 파라미터의 타입을 보존할 수 있다.
1 import logging
2 from typing import Callable, ParamSpec, TypeVar
3
4 P = ParamSpec("P")
5 ANYTHING = TypeVar("ANYTHING")
6
7 logger = logging.getLogger(__name__)
8 logger.setLevel(logging.INFO)
9 logger.addHandler(logging.StreamHandler())
10
11
12 def log_to_database():
13 logger.info("Logging to database")
14
15
16 def add_logging(f: Callable[P, ANYTHING]) -> Callable[P, ANYTHING]:
17 def inner(*args: P.args, **kwargs: P.kwargs) -> ANYTHING:
18 log_to_database()
19 return f(*args, **kwargs)
20 return inner
21
22 @add_logging
23 def takes_int_str(x: int, y: str) -> int:
24 return x + 7
25
26 takes_int_str(1, "A")
27 takes_int_str("B", 2)
$ mypy param_spec_1.py
param_spec_1.py:27: error: Argument 1 to "takes_int_str" has incompatible type "str"; expected "int" [arg-type]
param_spec_1.py:27: error: Argument 2 to "takes_int_str" has incompatible type "int"; expected "str" [arg-type]
Found 2 errors in 1 file (checked 1 source file)
만약 필수로 넘겨야 하는 파라미터의 타입을 명시하고 싶다면 Concatenate()
를 활용하면 된다.
아래 add_logging()
데코레이터에는 위치 인자를 통해 필수로 전달받아야 하는 2개 파라미터의 타입이 명시되어 있다.
1 import logging
2 from typing import Callable, TypeVar, Concatenate
3
4 from typing_extensions import ParamSpec
5
6 logger = logging.getLogger(__name__)
7 logger.setLevel(logging.INFO)
8 logger.addHandler(logging.StreamHandler())
9
10
11 P = ParamSpec("P")
12 ANYTHING = TypeVar("ANYTHING")
13
14
15 def log_to_database():
16 logger.info("Logging to database")
17
18
19 def add_logging(f: Callable[Concatenate[int, int, P], ANYTHING]) -> Callable[Concatenate[int, int, P], ANYTHING]:
20 def inner(x: int, y: int, *args: P.args, **kwargs: P.kwargs) -> ANYTHING:
21 log_to_database()
22 return f(x, y, *args, **kwargs)
23 return inner
24
25 @add_logging
26 def plus(x: int, y: int, z: str) -> int:
27 return x + y
28
29
30 plus(1, 1, "Z1")
31 plus(1, "1", "Z1")
32 plus(2, y=2, z="Z2")
33 plus(x=2, y=2, z="Z3")
34 plus(2, 2, z=3)
35 plus(2, z=3)
$ mypy concatenate.py
concatenate.py:31: error: Argument 2 to "plus" has incompatible type "str"; expected "int" [arg-type]
concatenate.py:32: error: Unexpected keyword argument "y" for "plus" [call-arg]
concatenate.py:33: error: Unexpected keyword argument "x" for "plus" [call-arg]
concatenate.py:33: error: Unexpected keyword argument "y" for "plus" [call-arg]
concatenate.py:34: error: Argument "z" to "plus" has incompatible type "int"; expected "str" [arg-type]
concatenate.py:35: error: Too few arguments for "plus" [call-arg]
concatenate.py:35: error: Argument "z" to "plus" has incompatible type "int"; expected "str" [arg-type]
Found 7 errors in 1 file (checked 1 source file)
아래 예시코드는 functool.partial()
과 동작이 유사한데, plus()
를 호출하는 외부에서 매번 Request
객체를 만들지 않고 데코레이터 내부에서 객체를 만들어 전달한다.
여기에 Concatenate()
를 통해 plus()
함수의 req
파라미터에 대한 타입 보존이 가능하다.
1 import logging
1 import logging
1 import logging
1 import logging
2 from typing import Callable, TypeVar, Concatenate
3
4 from typing_extensions import ParamSpec
5
6 logger = logging.getLogger(__name__)
7 logger.setLevel(logging.INFO)
8 logger.addHandler(logging.StreamHandler())
9
10
11 P = ParamSpec("P")
12 ANYTHING = TypeVar("ANYTHING")
13
14 class Request:
15 pass
16
17 class AnotherRequest:
18 pass
19
20 def log_to_database():
21 logger.info("Logging to database")
22
23
24 def add_logging(f: Callable[Concatenate[Request, P], ANYTHING]) -> Callable[P, ANYTHING]:
25 def inner(*args: P.args, **kwargs: P.kwargs) -> ANYTHING:
26 log_to_database()
27 return f(AnotherRequest(), *args, **kwargs)
28 return inner
29
30 @add_logging
31 def plus(req: Request, x: int, y: int, z: str) -> int:
32 print(id(req))
33 return x + y
34
35
36 plus(1, 1, "Z1")
$ mypy concatenate2.py
concatenate2.py:27: error: Argument 1 has incompatible type "AnotherRequest"; expected "Request" [arg-type]
Found 1 error in 1 file (checked 1 source file)