Python Development At 2024

Intro

与之对比是 2020 年左右自己 Python 开发相关技术栈,那时以 Python 3.6 为主,主要做 Flask Web 开发。现在则是以 Python 3.10 作为新的版本来进行学习。

本文会涉及一些我觉得挺重要的一些改动,包括

  • 语法上的更新
  • async / await
  • 框架和工具

Language Changes

PEP 557 Data Classes

以前没怎么太详细读过 PEP,这次认真看了几个,第一次从语言开发者的角度了解语言 feature 前后,感觉也不错。

在讲 PEP 557 之前需要了解下 PEP 526。PEP 526 主要讲的是 variable annotation。之前在 Python 3.6 阶段已经有了 PEP 484,但其更多关注是类型推断。PEP 526 则更进一步扩展到 class and instance variable 用途上。

其中一个重要用途

1
2
3
4
5
6
7
class BasicStarship:
captain: str = 'Picard' # instance variable with default
damage: int # instance variable without default
stats: ClassVar[Dict[str, int]] = {} # class variable

bss = BasicStarship()
print(vars(bss)) # {}, instance without attribute

上述代码中,能通过 type annotation 标记出其是属于 instance 还是 class 的 variable。但需要注意的是,这种 type annotaition 属于标记,并不会影响实际程序逻辑,即上述的代码中,captain 依旧还是 class variable。直到 PEP 557 则可以借助类装饰器来更好的完成类定义。

A class decorator is provided which inspects a class definition for variables with type annotations as defined in PEP 526, “Syntax for Variable Annotations”.

同样是上述例子

1
2
3
4
5
6
7
8
9
10
@dataclass
class BasicStarship:
damage: int # instance variable without default
captain: str = 'Picard' # instance variable with default
stats: ClassVar[Dict[str, int]] = {} # class variable


bss = BasicStarship(damage=1)

print(vars(bss)) # {'damage': 1, 'captain': 'Picard'}

这里的关键是,这个类装饰器可以帮你自动实现如 __init____ne__ 等一些常用的 magic method。这一特性可以更方便实现一些只需要进行序列化和反序列化的类。此外借助 dataclass 的 field 可以对不同的属性进行定制。

1
2
3
4
5
6
7
8
9
10
@dataclass
class D:
x: list = field(default_factory=list) # init the mutable variable
y: int = field(repr=False, default=10)
z: int = 20


assert D().x is not D().x # True

print(vars(D())) # {'x': [], 'y': 10, 'z': 20}

更多用法请参考 dataclasses 的文档。

PEP 636 Structural Pattern Matching

这个是我最感兴趣的特性,在 Python 3.10 引入。模式匹配这个语法层面的支持可能不同语言都大差不差,但看了下一些样例,能很好支持 Python 已有的 packing / unpacking 的语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
match command.split():
case ["quit"]:
print("Goodbye!")
quit_game()
case ["look"]:
current_room.describe()
case ["get", obj]:
character.get(obj, current_room)
case ["go", direction] if direction in current_room.exits: # if guard
current_room = current_room.neighbor(direction)
case ["drop", *objects]:
print(objects)
case ["north"] | ["go", "north"]: # OR pattern
current_room = current_room.neighbor("north")
case _:
print(f"Sorry, I couldn't understand {command!r}")

Dict Keys Ordering

这个功能的更新记录

Dictionaries preserve insertion order. Note that updating a key does not affect the order. Keys added after deletion are inserted at the end.

Changed in version 3.7: Dictionary order is guaranteed to be insertion order. This behavior was an implementation detail of CPython from 3.6.

Changed in version 3.8: Dictionaries are now reversible.

当时出的时候还关注了一下,但实际需要用到已经是 2024 年了,在实际写需要这个功能的时候,第一反应其实是 OrderedDict。现在回想起来,在这种不断进步的时代,所谓经验有时候的确会变成阻碍。现在已经是 3.1X 的版本了,已经不需要考虑这个东西是不是会存在前向兼容的问题了。

Async

async 相关功能引入实际上是在 3.5 版本。但在 2019 年主要用着 3.6,项目实际上还是以 Flask 为主,并没有相应的尝试。但在 2024 年这个时间点,async 的使用则是广泛了许多。async / await 是新引进的两个 keyword,asyncio 则是在新语法基础上,为 Python 编写相应 async 功能的支持库。

Coroutines are a more generalized form of subroutines. Subroutines are entered at one point and exited at another point. Coroutines can be entered, exited, and resumed at many different points. They can be implemented with the async def statement.

Coroutines behave like generators. In older versions of Python, coroutines are defined by generators.

其实有过 async / await 类似 pattern 的其它语言使用经验可以很快接受,PEP 492 也说了采用这种语法是因为其它语言也是这么做的,里面也介绍了很多新 async magic method 应用例子。

这里主要想讲下 ASGI。

ASGI

和以前的 WSGI 相比

The WSGI specification has worked well since it was introduced, and allowed for great flexibility in Python framework and web server choice. However, its design is irrevocably tied to the HTTP-style request/response cycle, and more and more protocols that do not follow this pattern are becoming a standard part of web programming (most notably, WebSocket).

Unlike WSGI, however, applications are asynchronous callables rather than simple callables, and they communicate with the server by receiving and sending asynchronous event messages rather than receiving a single input stream and returning a single iterable.

WSGI 只专注于一个请求的上下文。

ASGI 关注一个连接上下文,根据不同的 scope 和 event 能处理好不同协议的不同流程,不再仅仅局限于 HTTP,也可以支持如 WebSocket 和 HTTP2。

ASGI decomposes protocols into a series of events that an application must receive and react to, and events the application might send in response.

ASGI is structured as a single, asynchronous callable. It takes a scope, which is a dict containing details about the specific connection, send, an asynchronous callable, that lets the application send event messages to the client, and receive, an asynchronous callable which lets the application receive event messages from the client.

一段不借助 web framework 的代码

1
2
3
4
async def application(scope, receive, send):
event = await receive()
...
await send({"type": "websocket.send", ...})

不同协议,events 有多种,每个 event 是一个 dict,都包含一个指明类型的 type 字段。如对于 HTTP

  • http.request
  • http.disconnect

对于 WebSocket

  • websocket.connect
  • websocket.send
  • websocket.receive
  • websocket.disconnect

以下是用 starlette 来编写的样例,需要注意的是,去到 framework 阶段,我们就可以借助 framework 更好的与 scope / receive 打交道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app.py
from starlette.responses import PlainTextResponse
from starlette.requests import Request

async def app(scope, receive, send):
event = await receive()
// ... do something
assert scope["type"] == "http"

// Starlette includes a `Request` class that gives you a nicer interface onto the incoming
// request, rather than accessing the ASGI scope and receive channel directly.
request = Request(scope, receive)
// ... do something with request

response = PlainTextResponse("Hello, world!")
await response(scope, receive, send)

Frameworks and Tools

poetry

2019 年接手的一个项目,最开始使用的是 pipenv 来进行项目管理,当时使用下来最大的问题是,它太慢了。在当时日常开发和部署体验都非常糟糕。当时去看它的文档的时候还注意到,它 18 年最好一次更新之后就一直停在那里了。在写这篇文章的时候再去看了下,我们当时注意到的最新的更新时间是 2018-11-26 然后再下一次更新就是 2020 年的 5 月。在此之前,我们所有的项目都全部替换掉了 pipenv。

当时我们的解决方法是,只使用最原始的 requirement 文件。对几个项目的一些共用的库,如 Flask / schema / requests 都统一版本。在部署的时候,所有的 Python 都用同一个 base docker image,里面已经预先 build 好了所有的共有的 pip packages。这样就能减少在部署的时候需要重新 install 一些 packages。但这个方案只能解决

一些 pipenv 问题的参考文章,和我当时的体验差不多

现在站在 2024 年的角度去看重新看这个问题的话,感觉已经有了更多更好的方案,如 poetry。当时 pipenv 有一个好处其实是能帮我们管理 env,后面去掉之后,开发者们就只能自己来用额外的 env 工具来开发了。

还有一点比较重要的是,poetry 的命令本身也将一些业界约定俗成的用法整合了进来。

1
2
3
4
5
# build wheel package
poetry build

poetry new my-package
poetry new --src my-package

上述代码中,new 可以加一个 --src 参数就可以在 src 目录下新建一个 my-package 的 package。

Pydantic

最开始我们是用 schema 这个 library 去做 data validation。现在 type hint 语法出来之后,Pydantic 可以很好的完成这个工作。

At the time of writing there are 214,100 repositories on GitHub and 8,119 packages on PyPI that depend on Pydantic.

Some notable libraries that depend on Pydantic:

当时的选择其实也挺多的,如 marshmallow 和追求效率的 binary serialization format msgpack。现在这些 library 也还在更新,应该会很长一段时间会维持这两种不同风格的序列化方案。

FastAPI

FastAPI stands on the shoulders of giants:

3.6 版本的时代,感觉上是 Flask 和 Django 两个框架基本上是各取所需,前者更轻便,后者则是集成了很多东西。现在 fastapi 则是更好了。它可以很快的写出简单的样例

1
2
3
4
5
6
7
8
9
10
11
12
13
from typing import Union

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}

但通过 Pydantic 去定义 request / reponse 的 schema,借助其自带的 Swagger / ReDoc 支持,则可以直接生成对应 API 文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
name: str
price: float
is_offer: Union[bool, None] = None

@app.get("/")
def read_root():
return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
return {"item_name": item.name, "item_id": item_id}

实际效果,如同前面介绍的,站在巨人的肩膀上。

Conclusion

语言层面的变更还挺有趣的,感觉在保持 Python 语言有趣的情况下,又增加了许多严谨的特性。希望未来有更多机会用上这些新特性。

References


Python Development At 2024
http://yoursite.com/2024/03/24/python-development-at-2024/
Author
Shing
Posted on
March 24, 2024
Licensed under