except* & ExceptionGroup
except*와 ExceptionGroup이라는 새로운 문법이 추가되었다.
try:
raise ExceptionGroup("Group1", [TypeError("a"), ValueError("b")])
except* TypeError as e:
print(f"Error: {e!r}")
except* ValueError as e:
print(f"Error: {e!r}")
"""
Error: ExceptionGroup('Group1', [TypeError('a')])
Error: ExceptionGroup('Group1', [ValueError('b')])
"""
ExceptionGroup을 통해 여러 에러를 동시에 처리할 수 있도록 하는 것이 가능해졌다. expect*는 동시에 발생하는 여러 예외들을 처리할 수 있도록 하기 위해 추가된 기능이다. 이러한 기능은 비동기 처리에서 동시에 발생하는 예외를 처리하는 데 사용할 수 있다.
예시:
import asyncio
async def f(n):
return 5 / n
async def main():
tasks = [f(n) for n in (0, "a", "b")]
try:
ret = await asyncio.gather(*tasks, return_exceptions=True)
if ret:
error = [e for e in ret if isinstance(e, Exception)]
raise ExceptionGroup("Error while f()", error)
except* ZeroDivisionError as e:
print(f"!ZeroDivisionError!: {e!r}")
except* TypeError as e:
print(f"!TypeError!: {e!r}")
asyncio.run(main())
"""
!ZeroDivisionError!: ExceptionGroup('Error while f()', [ZeroDivisionError('division by zero')])
!TypeError!: ExceptionGroup('Error while f()', [TypeError("unsupported operand type(s) for /: 'int' and 'str'"), TypeError("unsupported operand type(s) for /: 'int' and 'str'")])
"""
위 예시는 비동기 처리를 하는 과정에서 ZeroDivisionError와 TypeError가 발생하도록 만들어진 코드이다. 그리고 이때 발생한 에러를 ExceptionGroup으로 묶어 처리한 결과이다.
TOML 라이브러리 추가
이제 tomllib 라이브러리가 Python 기본 라이브러리에 추가되었다.
import tomllib
with open("ex.toml", "rb") as toml:
# tomllib.load(toml) -> dict[str, Any]
data = tomllib.load(toml)
비동기 - TaskGroup
Python은 여러 버전에 걸쳐 비동기 처리 방식을 개선시켜 왔다. 이번에 제시한 방식은 마치 IO 작업에서 with open 구문을 사용하듯, async with TaskGroup을 사용한다. 이를 통해 await를 작성하지 않아 발생하는 실수를 줄일 수 있다.
async def f(n):
...
async with asyncio.TaskGroup() as tg:
ret1 = tg.create_task(f(1))
ret2 = tg.create_task(f(2))
... # 비동기로 처리할 task 등록
# ret1과 ret2는 <built-in method result of _asyncio.Task object>
ret1 = ret1.result() # result 메서드를 통해 결과값 반환
ret2 = ret2.result()
create_task를 통해 수행할 함수를 등록하고, 결과값은 result 메서드를 통해 가져올 수 있다.
예시:
import asyncio
async def f(n):
await asyncio.sleep(n)
print(f"-- f({n}) Done! --")
return n
async def main():
async with asyncio.TaskGroup() as tg:
r5 = tg.create_task(f(5))
r3 = tg.create_task(f(3))
r1 = tg.create_task(f(1))
return r1, r3, r5
rets = asyncio.run(main())
rets = [ret.result() for ret in rets]
print("\nresult:", rets)
"""
-- f(1) Done! --
-- f(3) Done! --
-- f(5) Done! --
result: [1, 3, 5]
"""
5, 3, 1 순서로 호출했지만 1, 3, 5 순서로 실행된 것을 보면 비동기로 작업이 처리된 것을 확인할 수 있다. 그리고 반환 값도 예상대로 반환되었다.
속도 향상
CPython 3.10 버전에 비해 평균적으로 25% 정도 빨라졌다. 상황에 따라 10%-60% 속도 향상을 기대할 수 있다.
시작 속도도10-15% 빨라졌다. 코드 객체가 인터프리터에 의해 정적으로 할당되기 때문에 이전 방식보다 적은 단계를 거치게 된다.
frame 객체 호출 방식을 수정해 런타임도 3-7% 빨라졌다.
LiteralString
LiteralString은 SQL injection과 같은 injection을 방지하기 위해 도입되었다고 한다.
# !!주의!!
import subprocess
def echo(name):
subprocess.run(f"echo 'Hello {name}'", shell=True)
name = "' && rm -rf / #"
echo(name) # echo '' && rm -rf / #'
"""
''
(rm -rf / 실행)
"""
만약 위와 같은 시나리오가 있다면 경고 없이 디렉터리를 모두 날려버릴 수 있다. SQL 쿼리를 작성할 때도 똑같은 상황이 적용된다. 따라서 str이 아닌 LiteralString을 사용해 타입을 체크할 때 에러가 발생하도록 하였다. 실제 injection을 막아주는 것은 아니다.
from typing import LiteralString
def echo(name: LiteralString):
- Traceback 에러 메시지 구체화
- Atomic grouping ((?>...)) 과 possessive quantifiers (*+, ++, ?+, {m,n}+) 지원
- typing.Self 추가
그 외 다른 업데이트는 공식 문서에서 확인할 수 있다.