NewTypeãTypeVarãããã³ãžã§ããªãã¯å¶çŽã䜿çšãããé«åºŠãªPythonã®åæå®ã®è©³çްãªè§£èª¬ãããå ç¢ã§ãèªã¿ããããä¿å®ããããã¢ããªã±ãŒã·ã§ã³ãæ§ç¯ããæ¹æ³ãåŠã³ãŸãã
Mastering Python's Typing Extensions: A Guide to NewType, TypeVar, and Generic Constraints
çŸä»£ã®ãœãããŠã§ã¢éçºã®äžçã§ã¯ãæ©èœçãªã ãã§ãªããæç¢ºã§ãä¿å®ãããããå
ç¢ãªã³ãŒããæžãããšãæãéèŠã§ããåŸæ¥ãåçåä»ãèšèªã§ãã£ãPythonã¯ãPEP 484ã§å°å
¥ããã匷åãªåã·ã¹ãã ãéããŠããã®å²åŠãæ¡çšããŸãããint
ãstr
ãlist
ã®ãããªåºæ¬çãªåãã³ãã¯ä»ãäžè¬çã§ãããPythonã®åæå®ã®çã®åã¯ãã®é«åºŠãªæ©èœã«ãããŸãããããã®ããŒã«ã䜿çšãããšãéçºè
ã¯è€éãªé¢ä¿ãšå¶çŽã衚çŸã§ããããå®å
šã§èªå·±ææžåãããã³ãŒãã«ã€ãªãããŸãã
ãã®èšäºã§ã¯ãtyping
ã¢ãžã¥ãŒã«ã®æã圱é¿åã®ãã3ã€ã®æ©èœãNewType
ãTypeVar
ãããã³ãããã«é©çšã§ããå¶çŽã«ã€ããŠæ·±ãæãäžããŸãããããã®æŠå¿µãç¿åŸããããšã§ãPythonã³ãŒããåãªãæ©èœçãªãã®ãããããèšèšãããã®ãžãšé«ããæ¬çªç°å¢ã«å°éããåã«åŸ®åŠãªãã°ãæ€åºã§ããŸãã
Why Advanced Typing Matters
å
·äœçãªå
容ã説æããåã«ãåºæ¬çãªåãè¶
ããããšããªãã²ãŒã ãã§ã³ãžã£ãŒãšãªãã®ãã確ç«ããŸããããå€§èŠæš¡ãªã¢ããªã±ãŒã·ã§ã³ã§ã¯ãåçŽãªããªããã£ãåã§ã¯ã衚çŸããããŒã¿ã®å®å
šãªæå³çæå³ãæããããªãããšããããããŸããint
ã¯ããŠãŒã¶ãŒIDã補åã«ãŠã³ãããŸãã¯ã¡ãŒãã«åäœã®æž¬å®å€ã§ããïŒã³ã³ããã¹ãããªããã°ããããã¯åãªãæ°å€ã§ãããã³ã³ãã€ã©ãŸãã¯ã€ã³ã¿ãŒããªã¿ã¯ãå¥ã®å Žæã§äœ¿çšãããããšãæåŸ
ãããŠããå Žæã§èª€ã£ãŠäœ¿çšããããšãé²ãããšã¯ã§ããŸããã
é«åºŠãªåæå®ã¯ããã®ããžãã¹ããžãã¯ãšãã¡ã€ã³ç¥èãã³ãŒãã®æ§é ã«çŽæ¥åãèŸŒãæ¹æ³ãæäŸããŸããããã«ãããæ¬¡ã®ããã«ãªããŸãã
- Enhanced Code Clarity: Types act as a form of documentation, making function signatures instantly understandable.
- Improved IDE Support: Tools like VS Code, PyCharm, and others can provide more accurate autocompletion, refactoring support, and real-time error detection.
- Early Bug Detection: Static type checkers like Mypy, Pyright, or Pyre can analyze your code and identify a whole class of potential runtime errors during development.
- Greater Maintainability: As a codebase grows, strong typing makes it easier for new developers to understand the system's design and make changes with confidence.
ããã§ã¯ãæåã®ããŒã«ã§ããNewType
ã調ã¹ãŠããã®ãã¯ãŒãã¢ã³ããã¯ããŸãããã
NewType: Creating Distinct Types for Semantic Safety
The Problem: Primitive Obsession
ãœãããŠã§ã¢éçºã«ãããäžè¬çãªã¢ã³ããã¿ãŒã³ã¯ããããªããã£ããžã®ãã ãããã§ãããã¡ã€ã³åºæã®æŠå¿µã衚ãããã«ãçµã¿èŸŒã¿ã®ããªããã£ãåãé床ã«äœ¿çšããããšã§ãããŠãŒã¶ãŒãšæ³šææ å ±ãåŠçããã·ã¹ãã ãèããŠã¿ãŸãããã
def process_order(user_id: int, order_id: int) -> None:
print(f"Processing order {order_id} for user {user_id}...")
# A simple, but potentially disastrous, mistake
user_identification = 101
order_identification = 4512
process_order(order_identification, user_identification) # Whoops!
# Output: Processing order 101 for user 4512...
äžèšã®äŸã§ã¯ã誀ã£ãŠuser_id
ãšorder_id
ãå
¥ãæ¿ããŠããŸããŸãããPythonã¯ã©ã¡ããæŽæ°ãªã®ã§æå¥ãèšããŸãããéçåãã§ãã«ãŒãåãçç±ã§ããããã£ããããŸããããã®çš®ã®ãã°ã¯ãããŒã¿ã®ç Žæã誀ã£ãããžãã¹ãªãã¬ãŒã·ã§ã³ã«ã€ãªããå¯èœæ§ããããæ²¹æãªããŸããã
The Solution: Introducing `NewType`
NewType
ã¯ãæ¢åã®åããåå¥ã®åç®åãäœæã§ããããã«ããããšã§ããã®åé¡ã解決ããŸãããããã®æ°ããåã¯ãéçåãã§ãã«ãŒã«ãã£ãŠäžæãšããŠæ±ãããŸãããã©ã³ã¿ã€ã ãªãŒããŒãããã¯ãŒãã§ããã©ã³ã¿ã€ã æã«ã¯ãåºã«ãªãåºæ¬åãšãŸã£ããåãããã«åäœããŸãã
NewType
ã䜿çšããŠäŸããªãã¡ã¯ã¿ãªã³ã°ããŠã¿ãŸãããã
from typing import NewType
# Define distinct types for User IDs and Order IDs
UserId = NewType('UserId', int)
OrderId = NewType('OrderId', int)
def process_order(user_id: UserId, order_id: OrderId) -> None:
print(f"Processing order {order_id} for user {user_id}...")
user_identification = UserId(101)
order_identification = OrderId(4512)
# Correct usage - works perfectly
process_order(user_identification, order_identification)
# Incorrect usage - now caught by a static type checker!
# Mypy will raise an error like:
# error: Argument 1 to "process_order" has incompatible type "OrderId"; expected "UserId"
# error: Argument 2 to "process_order" has incompatible type "UserId"; expected "OrderId"
process_order(order_identification, user_identification)
NewType
ã䜿çšãããšãUserId
ãšOrderId
ã¯ãã³ã¢ã§ã¯ã©ã¡ããæŽæ°ã§ãã£ãŠãã亀æå¯èœã§ã¯ãªãããšãåãã§ãã«ãŒã«äŒããŸããããã®ç°¡åãªå€æŽã«ããã匷åãªå®å
šå±€ã远å ãããŸãã
`NewType` vs. `TypeAlias`
NewType
ãåçŽãªåãšã€ãªã¢ã¹ãšåºå¥ããããšãéèŠã§ããåãšã€ãªã¢ã¹ã¯ãæ¢åã®åã«æ°ããååãä»ããã ãã§ãããåå¥ã®åãäœæããŸããã
from typing import TypeAlias
# This is just an alias. A type checker sees UserIdAlias as exactly the same as int.
UserIdAlias: TypeAlias = int
def process_user(user_id: UserIdAlias) -> None:
...
# No error here, because UserIdAlias is just an int
process_user(123)
process_user(OrderId(999)) # OrderId is also an int at runtime
åã亀æå¯èœãªå Žåã¯ïŒäŸïŒVector = list[float]
ïŒãèªã¿ãããã®ããã«TypeAlias
ã䜿çšããŸããåãæŠå¿µçã«ç°ãªããæ··åšãã¹ãã§ãªãå Žåã¯ãå®å
šã®ããã«NewType
ã䜿çšããŸãã
TypeVar: The Key to Powerful Generic Functions and Classes
å€ãã®å Žåãåéã®é¢ä¿ãç¶æããªãããããŸããŸãªåã§åäœããããã«èšèšããã颿°ãŸãã¯ã¯ã©ã¹ãäœæããŸããããšãã°ããªã¹ãã®æåã®èŠçŽ ãè¿ã颿°ã¯ãæååã®ãªã¹ããäžããããå Žåã¯æååãè¿ããæŽæ°ã®ãªã¹ããäžããããå Žåã¯æŽæ°ãè¿ãå¿ èŠããããŸãã
The Problem with `Any`
ãã€ãŒããªã¢ãããŒãã§ã¯ãtyping.Any
ã䜿çšããå ŽåããããŸããããã«ããããã®å€æ°ã®åãã§ãã¯ãäºå®äžç¡å¹ã«ãªããŸãã
from typing import Any, List
def get_first_element_any(items: List[Any]) -> Any:
if items:
return items[0]
return None
numbers = [1, 2, 3]
first_num = get_first_element_any(numbers)
# What is the type of 'first_num'? The type checker only knows 'Any'.
# This means we lose autocompletion and type safety.
# (first_num.imag) # No static error, but a runtime AttributeError!
Any
ã䜿çšãããšãéçåä»ãã®å©ç¹ãç ç²ã«ããå¿
èŠãçããŸããåãã§ãã«ãŒã¯ã颿°ããè¿ãããå€ã«é¢ãããã¹ãŠã®æ
å ±ã倱ããŸãã
The Solution: Introducing `TypeVar`
TypeVar
ã¯ãåã®ãã¬ãŒã¹ãã«ããŒãšããŠæ©èœããç¹å¥ãªå€æ°ã§ããããã«ããã颿°ã®åŒæ°ã®åãšãã®æ»ãå€ã®éã®é¢ä¿ã宣èšã§ããŸããããã¯ãPythonã®ãžã§ããªãã¯ã¹ã®åºç€ã§ãã
TypeVar
ã䜿çšããŠé¢æ°ãæžãçŽããŠã¿ãŸãããã
from typing import TypeVar, List, Optional
# Create a TypeVar. The string 'T' is a convention.
T = TypeVar('T')
def get_first_element(items: List[T]) -> Optional[T]:
if items:
return items[0]
return None
# --- Usage Examples ---
# Example 1: List of integers
numbers = [10, 20, 30]
first_num = get_first_element(numbers)
# Mypy correctly infers that 'first_num' is of type 'Optional[int]'
# Example 2: List of strings
names = ["Alice", "Bob", "Charlie"]
first_name = get_first_element(names)
# Mypy correctly infers that 'first_name' is of type 'Optional[str]'
# Now, the type checker can help us!
if first_num is not None:
print(first_num + 5) # OK, it's an int!
if first_name is not None:
print(first_name.upper()) # OK, it's a str!
å
¥åïŒList[T]
ïŒãšåºåïŒOptional[T]
ïŒã®äž¡æ¹ã§T
ã䜿çšããããšã§ããªã³ã¯ãäœæããŸãããåãã§ãã«ãŒã¯ãå
¥åãªã¹ãã«å¯ŸããŠT
ãã€ã³ã¹ã¿ã³ã¹åãããåã«é¢ä¿ãªããåãåã颿°ã«ãã£ãŠè¿ãããããšãçè§£ããŠããŸããããããžã§ããªãã¯ããã°ã©ãã³ã°ã®æ¬è³ªã§ãã
Generic Classes
TypeVar
ã¯ããžã§ããªãã¯ã¯ã©ã¹ãäœæããããã«ãäžå¯æ¬ ã§ãããããè¡ãã«ã¯ãã¯ã©ã¹ãtyping.Generic
ããç¶æ¿ããå¿
èŠããããŸãã
from typing import TypeVar, Generic, List
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: List[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def is_empty(self) -> bool:
return not self._items
# Create a stack specifically for integers
int_stack = Stack[int]()
int_stack.push(10)
int_stack.push(20)
value = int_stack.pop() # 'value' is correctly inferred as 'int'
# int_stack.push("hello") # Mypy error: Expected 'int', got 'str'
# Create a stack specifically for strings
str_stack = Stack[str]()
str_stack.push("hello")
# str_stack.push(123) # Mypy error: Expected 'str', got 'int'
Taking Generics Further: Constraints on `TypeVar`
å¶çŽã®ãªãTypeVar
ã¯ä»»æã®åã衚ãããšãã§ãã匷åã§ãããèš±å¯ç¯å²ãåºãããå ŽåããããŸãããžã§ããªãã¯é¢æ°ããå ç®ãæ¯èŒããŸãã¯å
¥åã«å¯Ÿããç¹å®ã®ã¡ãœããã®åŒã³åºããªã©ã®æäœãå®è¡ããå¿
èŠãããå Žåã¯ã©ãã§ãããããåãã§ãã«ãŒã¯ãä»»æã®åT
ããããã®æäœããµããŒãããããšãä¿èšŒã§ããªããããå¶çŽã®ãªãTypeVar
ã¯æ©èœããŸããã
ããã§å¶çŽãç»å ŽããŸãããããã䜿çšãããšãTypeVar
ã衚ãããšãã§ããåãå¶éã§ããŸãã
Constraint Type 1: `bound`
bound
ã¯ãTypeVar
ã®äžéãæå®ããŸããããã¯ãTypeVar
ãããŠã³ãåèªäœããŸãã¯ãã®ãµãã¿ã€ãã®ããããã«ãªãå¯èœæ§ãããããšãæå³ããŸããããã¯ãåãç¹å®ã®åºæ¬ã¯ã©ã¹ã®ã¡ãœãããšå±æ§ããµããŒãããŠããããšã確èªããå¿
èŠãããå Žåã«åœ¹ç«ã¡ãŸãã
2ã€ã®æ¯èŒå¯èœãªã¢ã€ãã ã®ãã¡å€§ããæ¹ãèŠã€ãã颿°ãèããŠã¿ãŸãããã>
æŒç®åã¯ããã¹ãŠã®åã«å¯ŸããŠå®çŸ©ãããŠããããã§ã¯ãããŸããã
from typing import TypeVar
# This version causes a type error!
T = TypeVar('T')
def find_larger(a: T, b: T) -> T:
# Mypy error: Unsupported operand types for > ("T" and "T")
return a if a > b else b
bound
ã䜿çšããŠãããä¿®æ£ã§ããŸããint
ãfloat
ãªã©ã®æ°å€åã¯æ¯èŒããµããŒãããŠãããããfloat
ãããŠã³ããšããŠäœ¿çšã§ããŸãïŒint
ã¯åä»ãã®äžçã§ã¯float
ã®ãµãã¿ã€ãã§ããããïŒã
from typing import TypeVar
# Create a bounded TypeVar
Number = TypeVar('Number', bound=float)
def find_larger(a: Number, b: Number) -> Number:
# This is now type-safe! The checker knows 'Number' supports '>'
return a if a > b else b
find_larger(10, 20) # OK, T is int
find_larger(3.14, 1.618) # OK, T is float
# find_larger("a", "b") # Mypy error: Type 'str' is not a subtype of 'float'
bound=float
ã¯ãNumber
ã«ä»£å
¥ãããåã¯ãæ¯èŒæŒç®åãå«ãfloat
ã®ã¡ãœãããšåäœãæã€ããšãåãã§ãã«ãŒã«ä¿èšŒããŸãã
Constraint Type 2: Value Constraints
ã¯ã©ã¹éå±€ã«TypeVar
ãå¶éããããªãããç¹å®ã«åæãããå¯èœãªåã®ãªã¹ãã«å¶éãããå ŽåããããŸãããã®ããã«ã¯ãè€æ°ã®åãTypeVar
ã³ã³ã¹ãã©ã¯ã¿ãŒã«çŽæ¥æž¡ãããšãã§ããŸãã
str
ãŸãã¯bytes
ã®ãããããåŠçã§ãããããã以å€ã®ãã®ã¯åŠçã§ããªã颿°ãæ³åããŠã¿ãŠãã ãããstr
ãšbytes
ã¯ãç§ãã¡ã®ç®çã®ããã«äŸ¿å©ã§ç¹å®ãªåºæ¬ã¯ã©ã¹ãå
±æããŠããªããããbound
ã¯ããã§ã¯é©åã§ã¯ãããŸããã
from typing import TypeVar
# Create a TypeVar constrained to 'str' and 'bytes'
StrOrBytes = TypeVar('StrOrBytes', str, bytes)
def get_hash(data: StrOrBytes) -> int:
# Both str and bytes have an __hash__ method, so this is safe.
return hash(data)
get_hash("hello world") # OK, StrOrBytes is str
get_hash(b"hello world") # OK, StrOrBytes is bytes
# get_hash(123) # Mypy error: Value of type variable "StrOrBytes" of "get_hash"
# # cannot be "int"
ããã¯bound
ãããæ£ç¢ºã§ããStrOrBytes
ã¯ãå
±éã®ç¥å
ã®ãµãã¿ã€ãã§ã¯ãªãã*æ£ç¢ºã«*str
ãŸãã¯bytes
ã§ãªããã°ãªããªãããšãåãã§ãã«ãŒã«äŒããŸãã
Putting It All Together: A Practical Scenario
ãããã®æŠå¿µãçµã¿åãããŠãåå®å šãªå°ããªããŒã¿åŠçãŠãŒãã£ãªãã£ãæ§ç¯ããŸããããç§ãã¡ã®ç®æšã¯ãã¢ã€ãã ã®ãªã¹ããåãåããããããããç¹å®ã®å±æ§ãæœåºãããã®å±æ§ã®äžæã®å€ã®ã¿ãè¿ã颿°ãäœæããããšã§ãã
import dataclasses
from typing import TypeVar, List, Set, Hashable, NewType
# 1. Use NewType for semantic clarity
ProductId = NewType('ProductId', int)
# 2. Define a data structure
@dataclasses.dataclass
class Product:
id: ProductId
name: str
category: str
# 3. Use a bounded TypeVar. The attribute we extract must be hashable
# to be put into a set for uniqueness.
HashableValue = TypeVar('HashableValue', bound=Hashable)
def get_unique_attributes(items: List[Product], attribute_name: str) -> Set[HashableValue]:
"""Extracts a unique set of attribute values from a list of products."""
unique_values: Set[HashableValue] = set()
for item in items:
value = getattr(item, attribute_name)
# A static checker can't verify 'value' is HashableValue here without
# more complex plugins, but the bound documents our intent and helps consumers.
unique_values.add(value)
return unique_values
# --- Usage ---
products = [
Product(id=ProductId(1), name="Laptop", category="Electronics"),
Product(id=ProductId(2), name="Mouse", category="Electronics"),
Product(id=ProductId(3), name="Desk Chair", category="Furniture"),
]
# Get unique categories. The type checker knows the return is Set[str]
unique_categories: Set[str] = get_unique_attributes(products, 'category')
print(f"Unique Categories: {unique_categories}")
# Get unique product IDs. The return is Set[ProductId]
unique_ids: Set[ProductId] = get_unique_attributes(products, 'id')
print(f"Unique IDs: {unique_ids}")
ãã®äŸã§ã¯:
NewType
ã¯ProductId
ãæäŸããä»ã®æŽæ°ãšèª€ã£ãŠæ··åããã®ãé²ããŸããTypeVar('...', bound=Hashable)
ã¯ãæœåºãã屿§ãããã·ã¥å¯èœã§ãªããã°ãªããªããšããéèŠãªèŠä»¶ãææžåããé©çšããŸããããã¯ãSet
ã«è¿œå ããããã§ãã- 颿°ã®ã·ã°ããã£
-> Set[HashableValue]
ã¯ããžã§ããªãã¯ã§ãããªããã颿°ã®åäœã«ã€ããŠéçºè ãšããŒã«ã«åŒ·åãªãã³ããæäŸããŸãã
Conclusion: Write Code That Works for Humans and Machines
Pythonã®åã·ã¹ãã ã¯ãé«å質ã®ãœãããŠã§ã¢ã远æ±ããäžã§åŒ·åãªå³æ¹ã§ããåºæ¬ãè¶
ããŠãNewType
ãTypeVar
ããžã§ããªãã¯å¶çŽãªã©ã®ããŒã«ãæ¡çšããããšã§ã倧å¹
ã«å®å
šã§ãçè§£ãããããä¿å®ããããã³ãŒããäœæã§ããŸãã
- Use `NewType` to give semantic meaning to primitive types and prevent logical errors from mixing different concepts.
- Use `TypeVar` to create flexible, reusable generic functions and classes that preserve type information.
- Use `bound` and value constraints on `TypeVar` to enforce requirements on your generic types, ensuring they support the operations you need to perform.
ãããã®ãã¿ãŒã³ãæ¡çšããããšã¯ãæåã¯äœåãªäœæ¥ã®ããã«æãããããããŸãããããã°ã®åæžãã³ã©ãã¬ãŒã·ã§ã³ã®æ¹åãéçºè ã®çç£æ§ã®åäžãšããé·æçãªå©çã¯èšãç¥ããŸããã仿¥ãããããžã§ã¯ãã«çµã¿èŸŒã¿ãããå ç¢ã§ãããã§ãã·ã§ãã«ãªPythonã¢ããªã±ãŒã·ã§ã³ã®åºç€ãæ§ç¯ããŠãã ããã