Python:typing
This module provides runtime support for type hints. The most fundamental support consists of the types Any, Union, Callable, TypeVar, and Generic. For a full specification, please see PEP 484. For a simplified introduction to type hints, see PEP 483.
Annotated
Add context-specific metadata to a type.
Example: Annotated[int, runtime_check.Unsigned]
indicates to the hypothetical runtime_check module that this type is an unsigned int. Every other consumer of this type can ignore this metadata and treat this type as int.
The first argument to Annotated must be a valid type.
Details:
- It's an error to call
Annotated
with less than two arguments. - Access the metadata via the
__metadata__
attribute::
- Nested Annotated types are flattened::
- Instantiating an annotated type is equivalent to instantiating the underlying type::
- Annotated can be used as a generic type alias::
Optimized:
TypeAlias = Annotated[T, runtime.Optimize()]
assert Optimized[int] == Annotated[int, runtime.Optimize()]
OptimizedList:
TypeAlias = Annotated[list[T], runtime.Optimize()]
assert OptimizedList[int] == Annotated[list[int], runtime.Optimize()]
- Annotated cannot be used with an unpacked TypeVarTuple::
Variadic:
This would be equivalent to::
where T1, T2 etc. are TypeVars, which would be invalid, because only one type should be passed to Annotated.
At Runtime
def annotated_by(
annotated: object,
kind: type[T],
) -> Iterable[tuple[str, T, type]]:
for k, v in get_type_hints(annotated, include_extras=True).items():
all_args = get_args(v)
if not all_args:
continue
actual, *rest = all_args
for arg in rest:
if isinstance(arg, kind):
yield k, arg, actual
이렇게 하면 사용하기 쉬운 편리한 형식으로 우리가 찾고 있는 주석을 다시 얻을 수 있습니다. 다음은 이를 사용하는 방법에 대한 간단한 예입니다.
@dataclass
class AnAnnotation:
name: str
def a_function(
a: str,
b: Annotated[int, AnAnnotation("b")],
c: Annotated[float, AnAnnotation("c")],
) -> None:
...
print(list(annotated_by(a_function, AnAnnotation)))
# [('b', AnAnnotation(name='b'), <class 'int'>),
# ('c', AnAnnotation(name='c'), <class 'float'>)]
Callable
Frameworks expecting callback functions of specific signatures might be type hinted using:
For example:
from collections.abc import Callable
def feeder(get_next_item: Callable[[], str]) -> None:
# Body
def async_query(on_success: Callable[[int], None],
on_error: Callable[[int, Exception], None]) -> None:
# Body
It is possible to declare the return type of a callable without specifying the call signature by substituting a literal ellipsis for the list of arguments in the type hint:
참고로 Callable[[...], ReturnType]
는 허용되지 않는 문법이다.
ParamSpec
#Callable 타입의 콜백 함수들을 인자로 가지는 경우에는 해당 콜백 함수의 인자의 타입에 이를 호출하는 함수가 의존하는 경우가 있습니다.
데코레이터가 대표적인데요. 이럴 때는 ParamSpec을 사용해주실 수 있습니다. (이 역시 Generic 타입의 일종으로 볼 수 있습니다.)
from typing import TypeVar, Callable, ParamSpec
import logging
T = TypeVar('T')
P = ParamSpec('P')
def add_logging(f: Callable[P, T]) -> Callable[P, T]:
'''A type-safe decorator to add logging to a function.'''
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
logging.info(f'{f.__name__} was called')
return f(*args, **kwargs)
return inner
@add_logging
def add_two(x: float, y: float) -> float:
'''Add two numbers together.'''
return x + y
@add_logging
def send_msg(msg: str) -> None:
print(f"I Sent {msg}")
ModuleType
typing
이 아닌, types
에 있다.
아니면 직접 추가하자.
Awaitable Callable
비동기 함수는 다음과 같이 사용하면 된다.
from typing import Any, Union, Callable, Awaitable
Watcher = Callable[..., Union[Awaitable[Any], Any]]
# ...
from inspect import iscoroutinefunction
if iscoroutinefunction(watcher):
return await watcher(*args, **kwargs) # type: ignore[misc]
else:
return watcher(*args, **kwargs)
Generics
List[X]
와 같은 제네릭 클래스를 사용하는 방법:
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
# Create an empty list with items of type T
self.items: List[T] = []
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> T:
return self.items.pop()
def empty(self) -> bool:
return not self.items
다음과 같이 사용할 수 있다:
# Construct an empty Stack[int] instance
stack = Stack[int]()
stack.push(2)
stack.pop()
stack.push('x') # Type error
유형 추론은 사용자 정의 제네릭 유형에서도 작동합니다:
def process(stack: Stack[int]) -> None: ...
process(Stack()) # Argument has inferred type Stack[int]
제네릭 유형의 인스턴스 생성도 유형 검사됩니다.
class Box(Generic[T]):
def __init__(self, content: T) -> None:
self.content = content
Box(1) # OK, inferred type is Box[int]
Box[int](1) # Also OK
s = 'some string'
Box[int](s) # Type error
init_subclass
class_getitem
Parameterizes a generic class.
At least, parameterizing a generic class is the *main* thing this method
does. For example, for some generic class `Foo`, this is called when we
do `Foo[int]` - there, with `cls=Foo` and `params=int`.
However, note that this method is also called when defining generic
classes in the first place with `class Foo(Generic[T]): ...`.
제네릭 클래스에서 MyClass[int]
와 같은 타입 인덱싱을 지원하기 위해 사용됩니다.
TypeVar
특정 타입 중 하나만 허용 (정확히 일치해야 한다):
bound=...
를 사용하면 해당 타입 또는 상속된 타입 허용:
공변성 (Covariant)
타입 분산(variance)에 대한 내용이다.
Python 타입 지정에서는 제네릭 클래스의 타입 매개변수가 하위 타입 관계에서 어떻게 동작할지 지정할 수 있습니다. 타입 분산의 세 가지 유형은 다음과 같습니다:
- 공변 (+T): 주어진 타입의 하위 타입도 허용합니다.
covariant=True
플래그를 사용하면 된다. - 반변적 (-T): 주어진 타입의 상위 타입도 허용합니다.
contravariant=True
플래그를 사용하면 된다. - 불변: 하위 타입이나 상위 타입을 허용하지 않으며 정확히 동일한 타입만 허용합니다.
Example:
from typing import TypeVar, Generic
# T is covariant
T = TypeVar('T', covariant=True)
# Base class
class Animal:
pass
# Subclass
class Dog(Animal):
pass
# A generic class with a covariant type parameter
class Box(Generic[T]):
def __init__(self, content: T) -> None:
self.content = content
# Because T is covariant, Box[Dog] can be assigned to Box[Animal]
dog_box: Box[Dog] = Box(Dog())
animal_box: Box[Animal] = dog_box
print(isinstance(animal_box.content, Animal)) # 출력: True
위 예제에서 T
를 covariant=True
로 선언했기 때문에, Box[Dog]
는 Box[Animal]
에 할당될 수 있습니다.
공변 타입 변수 덕분에 자식 클래스(Dog)가 부모 클래스(Animal)로 올바르게 취급됩니다.
Type aliases
A type alias is defined by assigning the type to the alias. In this example, Vector
and List[float]
will be treated as interchangeable synonyms:
from typing import List
Vector = List[float]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
# typechecks; a list of floats qualifies as a Vector.
new_vector = scale(2.0, [1.0, -4.2, 5.4])
Type aliases are useful for simplifying complex type signatures. For example:
from typing import Dict, Tuple, Sequence
ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]
def broadcast_message(message: str, servers: Sequence[Server]) -> None:
...
# The static type checker will treat the previous type signature as
# being exactly equivalent to this one.
def broadcast_message(
message: str,
servers: Sequence[Tuple[Tuple[str, int], Dict[str, str]]]) -> None:
...
Note that None
as a type hint is a special case and is replaced by type(None)
.
NewType
Rust에서는 일반적인 데이터 타입 (e.g. 정수)에 새로운 동작을 추가하지 않고, 도메인에 걸맞는 용도로 새롭게 이름을 지정하는 데이터 타입을 정의하는게 일반적입니다. 이 패턴을 “newtype”이라고 하며 Python에서도 사용할 수 있습니다.
아래의 상황을 한번 봅시다:
class Database:
def get_car_id(self, brand: str) -> int:
def get_driver_id(self, name: str) -> int:
def get_ride_info(self, car_id: int, driver_id: int) -> RideInfo:
db = Database()
car_id = db.get_car_id("Mazda")
driver_id = db.get_driver_id("Stig")
info = db.get_ride_info(driver_id, car_id)
get_ride_info
의 인수가 잘못들어갔습니다. </code>db.get_rider_info(car_id, driver_id)</code>가 되어야합니다. 하지만, 차량 ID와 운전자 ID는 모두 정수에 불과하므로 의미상 함수 호출이 잘못되었더라도 타입 자체는 올바르므로 타입 오류는 없습니다.
이 문제는 "NewType"을 사용하여 서로 다른 종류의 ID에 대해 별도의 타입을 정의함으로써 해결할 수 있습니다:
from typing import NewType
# Define a new type called "CarId", which is internally an `int`
CarId = NewType("CarId", int)
# Ditto for "DriverId"
DriverId = NewType("DriverId", int)
class Database:
def get_car_id(self, brand: str) -> CarId:
def get_driver_id(self, name: str) -> DriverId:
def get_ride_info(self, car_id: CarId, driver_id: DriverId) -> RideInfo:
db = Database()
car_id = db.get_car_id("Mazda")
driver_id = db.get_driver_id("Stig")
# Type error here -> DriverId used instead of CarId and vice-versa
info = db.get_ride_info(driver_id, car_id) ## 경고 포착!
ForwardRef
문자열 정방향 참조의 내부 입력 표현에 사용되는 클래스입니다. 예를 들어 List["SomeClass"]
는 암시 적으로 List[ForwardRef("SomeClass")]
. 이 클래스는 사용자가 인스턴스화해서는 안되지만 내부 검사 도구에서 사용할 수 있습니다.
Protocol
PEP 544 항목 참조.
TypeGuard
Type Narrowing (타입 좁히기) 을 위해 필요.
TypeGuard는 Python의 정적 타입 검사기(mypy)에서 사용되는 유용한 도구로, 조건에 따라 특정 타입을 확인하고, 그 타입이 참인 경우 해당 타입으로 변수를 추론할 수 있도록 돕습니다. Python 3.10에서 도입된 PEP 647에 정의된 개념입니다.
일반적으로 타입 추론은 함수의 반환 값이나 변수가 고정된 타입을 가지는 경우 유용하지만, 조건문 등을 통해 타입을 좁히는 경우 그 타입을 정확히 추론하기 어려울 수 있습니다. TypeGuard를 사용하면 이러한 상황에서 타입 힌트를 추가로 제공하여 더 나은 타입 추론을 가능하게 만듭니다.
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
'''Determines whether all objects in the list are strings'''
return all(isinstance(x, str) for x in val)
def func1(val: list[object]):
if is_str_list(val):
# Type of ``val`` is narrowed to ``list[str]``.
print(" ".join(val))
else:
# Type of ``val`` remains as ``list[object]``.
print("Not a list of strings!")
- TypeGuard 정의: 함수의 반환 타입으로 TypeGuard를 지정하면, 해당 함수가 True를 반환할 경우 인수의 타입을 지정된 타입으로 좁힐 수 있습니다.
- 조건에 따른 타입 좁히기: 위 예시에서 is_str_list 함수는 리스트의 모든 요소가 문자열인 경우 True를 반환하며, 그 결과로 타입 검사기는 리스트를 list[str]로 추론합니다.
Unpack
INFORMATION |
Added in version 3.11 |
from typing import TypedDict, Unpack
class Movie(TypedDict):
name: str
year: int
# This function expects two keyword arguments - `name` of type `str`
# and `year` of type `int`.
def foo(**kwargs: Unpack[Movie]):
kwargs["name"] = ... # 여기에서 "name" 을 칠 때 AutoComplete 나타난다. @dataclass 클래스는 안되더라
See PEP 692 for more details on using Unpack
for **kwargs
typing.
overload
인자에 따라 반환값이 달라지거나 등, IDE의 타입 추론에 도움이 된다. 사용사례는 다음과 같다:
from os import environ
from typing import Dict, Optional, TypeVar, Union, overload
from your_api.types.string.to_boolean import string_to_boolean
DefaultT = TypeVar("DefaultT", str, bool, int, float)
@overload
def get_typed_environ_value(key: str) -> Optional[str]: ...
@overload
def get_typed_environ_value(key: str, default: str) -> str: ...
@overload
def get_typed_environ_value(key: str, default: bool) -> bool: ...
@overload
def get_typed_environ_value(key: str, default: int) -> int: ...
@overload
def get_typed_environ_value(key: str, default: float) -> float: ...
def get_typed_environ_value(
key: str,
default: Optional[DefaultT] = None,
) -> Optional[Union[str, bool, int, float]]:
if default is None:
return environ.get(key)
value = environ.get(key, str(default))
if isinstance(default, str):
return value
elif isinstance(default, bool):
return string_to_boolean(value)
elif isinstance(default, int):
return int(value)
elif isinstance(default, float):
return float(value)
else:
raise TypeError(f"Unsupported default type: {type(default).__name__}")
get_args
타입 안에 들어간 인자들을 받아올 수 있다. (개인적으로) 리터럴 인자 목록을 가져올 때 사용한다.
TimedRotatingWhenLiteral = Literal[
"S", "M", "H", "D", "W0", "W1", "W2", "W3", "W4", "W5", "W6", "midnight"
] # W0=Monday
TIMED_ROTATING_WHEN: Final[Sequence[str]] = get_args(TimedRotatingWhenLiteral)
def my_func(when: str):
assert when in TIMED_ROTATING_WHEN
Union 의 클래스 목록을 확인할 수도 있다.
from enum import IntEnum
from typing import Union, get_args
class Test1(IntEnum):
A = 1
B = 2
class Test2(IntEnum):
C = 3
D = 4
AnyTest = Union[Test1, Test2)
print(get_args(AnyTest)) # -> (<enum 'Test1'>, <enum 'Test2'>)
ABC vs Prototype
요약하면, 이미 상속 구조가 잡혀있어서 컨트롤 하기 힘들다면 Prototype, 처음부터 설계 가능하다면 ABC
상속 클래스 타입 확인
class User: ...
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...
# Accepts User, BasicUser, ProUser, TeamUser, ...
def make_new_user(user_class: Type[User]) -> User:
# ...
return user_class()
함수의 파라미터 타입 확인
- Stackoverflow - Python: Get type of
typing.List
- Stackoverflow - How to get class variables and type hints?
import typing
def f(x: typing.List[MyType]):
...
print(f.__annotations__["x"].__args__[0]) # prints <class '__main__.MyType'>
print(f.__annotations__["x"].__base__) # prints typing.List[float]
print(f.__annotations__["x"].__orig_bases__[0]) # prints <class 'list'>
get_type_hints를 사용하는 방법이 더 좋다.
from typing import get_type_hints
class Person:
name: str
age: int
get_type_hints(Person)
# returns {'name': <class 'str'>, 'age': <class 'int'>}
최신 갱신 |
inspect 모듈의 signature 함수를 사용하는걸 추천한다. |
Troubleshooting
Invariant type variable
cvp/patterns/proxies/callable_proxy.py:10:1: error: Invariant type variable
"ValueT" used in protocol where covariant one is expected [misc]
class ReadableCallableProtocol(Protocol[ValueT]):
직역하면
공변(covariant)이 필요한 상황에서 불변(invariant)으로 사용되었기 때문에 발생합니다. 자세한 내용은 #공변성 (Covariant) 항목 참조.
간단히, TypeVar(..., covariant=True)
를 사용하면 된다.
error: Type variable "..." is unbound [valid-type]
mypy에서 다음과 같이 출력됨:
cvp/wsdl/schema.py: note: In class "XsdSchema":
cvp/wsdl/schema.py:21:19: error: Type variable "cvp.wsdl.schema._UrlKey" is unbound [valid-type]
_loaded: Dict[_UrlKey, _EtreeElement]
^
cvp/wsdl/schema.py:21:19: note: (Hint: Use "Generic[_UrlKey]" or "Protocol[_UrlKey]" base class to bind "_UrlKey" inside a class)
cvp/wsdl/schema.py:21:19: note: (Hint: Use "_UrlKey" in function signature to bind "_UrlKey" inside a function)
이 때 에러가 발생된 _UrlKey
를 보면 다음과 같다:
이걸 다음과 같이 수정: