Python 相当于 Typescript 接口

Python equivalent of Typescript interface

最近我一直在使用 Typescript,它可以表达如下内容:

interface Address {
    street: string;
    housenumber: number;
    housenumberPostfix?: string;
}

interface Person {
    name: string;
    adresses: Address[]
}

const person: Person = {
    name: 'Joe',
    adresses: [
        { street: 'Sesame', housenumber: 1 },
        { street: 'Baker', housenumber: 221, housenumberPostfix: 'b' }
    ]
}

非常简洁,并且在使用 Persons 编码时提供了类型检查和代码完成等所有功能。

在 Python 中是如何完成的?

我一直在研究 Mypy 和 ABC,但还没有成功找到 pythonic 方法来做与上述类似的事情(我的尝试导致了太多符合我口味的样板文件)。

在Python 3.5中,可以使用注解来指定参数的类型和return类型。最近的大多数 IDE,例如 PyCharm 都可以解释这些注释并为您提供良好的代码补全。您还可以使用注释来指定函数的签名或变量的类型。

这是一个例子:

from typing import List, Optional


class Address(object):
    def __init__(self, street: str, housenumber: int, housenumber_postfix: Optional[str]=None):
        self.street = street
        self.housenumber = housenumber
        self.housenumber_postfix = housenumber_postfix


class Person(object):
    def __init__(self, name: str, addresses: List[Address]):
        self.name = name
        self.addresses = addresses


person = Person(
    name='Joe',
    addresses=[
        Address(street='Sesame', housenumber=1),
        Address(street='Baker', housenumber=221, housenumber_postfix='b')
    ])

请注意 Python 不是强类型语言。所以,注解只是给开发者的一个指南。如果你真的想检查你的代码,你需要一个外部工具(目前最好的是mypy)。在代码质量控制期间,它可以像任何其他代码检查器一样使用。

对于 IDE 中的代码完成和类型提示,只需为 PersonAddress class 添加静态类型就可以了。假设您使用最新的 python3.6,这里是您示例中的打字稿 classes 的粗略等价物:

# spam.py
from typing import Optional, Sequence


class Address:
    street: str
    housenumber: int
    housenumber_postfix: Optional[str]

    def __init__(self, street: str, housenumber: int, 
                 housenumber_postfix: Optional[str] = None) -> None:
        self.street = street
        self.housenumber = housenumber
        self.housenumber_postfix = housenumber_postfix


class Person:
    name: str
    adresses: Sequence[Address]

    def __init__(self, name: str, adresses: Sequence[str]) -> None:
        self.name = name
        self.adresses = adresses


person = Person('Joe', [
    Address('Sesame', 1), 
    Address('Baker', 221, housenumber_postfix='b')
])  # type: Person

我想您提到的样板是在添加 class 构造函数时出现的。这确实是不可避免的。我希望在未明确声明时在运行时生成默认构造函数,如下所示:

class Address:
    street: str
    housenumber: int
    housenumber_postfix: Optional[str]


class Person:
    name: str
    adresses: Sequence[Address]


if __name__ == '__main__':
    alice = Person('Alice', [Address('spam', 1, housenumber_postfix='eggs')])
    bob = Person('Bob', ())  # a tuple is also a sequence

但遗憾的是,您必须手动声明它们。


编辑

Michael0x2a pointed out in the 一样,在 python3.7 中可以避免对默认构造函数的需求,它引入了 @dataclass 装饰器,因此确实可以声明:

@dataclass
class Address:
    street: str
    housenumber: int
    housenumber_postfix: Optional[str]


@dataclass
class Person:
    name: str
    adresses: Sequence[Address]

并获取多个方法的默认实现,减少样板代码量。查看 PEP 557 了解更多详情。


我想您可能会看到可以从您的代码生成的存根文件,作为某种接口文件:

$ stubgen spam  # stubgen tool is part of mypy package
Created out/spam.pyi

生成的存根文件包含所有非私有 classes 和未实现的模块函数的类型化签名:

# Stubs for spam (Python 3.6)
#
# NOTE: This dynamically typed stub was automatically generated by stubgen.

from typing import Optional, Sequence

class Address:
    street: str
    housenumber: int
    housenumber_postfix: Optional[str]
    def __init__(self, street: str, housenumber: int, housenumber_postfix: Optional[str]=...) -> None: ...

class Person:
    name: str
    adresses: Sequence[Address]
    def __init__(self, name: str, adresses: Sequence[str]) -> None: ...

person: Person

IDE 也可以识别这些存根文件,如果您的原始模块不是静态类型的,它们将使用存根文件进行类型提示和代码完成。

Python 3.6 添加了与类型提示一起使用的 namedtuple 的新实现,它删除了其他答案所需的一些样板文件。

from typing import NamedTuple, Optional, List


class Address(NamedTuple):
    street: str
    housenumber: int
    housenumberPostfix: Optional[str] = None


class Person(NamedTuple):
    name: str
    adresses: List[Address]


person = Person(
    name='Joe',
    adresses=[
        Address(street='Sesame', housenumber=1),
        Address(street='Baker', housenumber=221, housenumberPostfix='b'),
    ],
)

编辑:NamedTuples 是不可变的,因此请注意,如果您想修改对象的字段,则不能使用此解决方案。改变listsdicts的内容还是可以的。

我找到的一个简单的解决方案(不需要 Python 3.7)是使用 SimpleNamespace:

from types import SimpleNamespace as NS
from typing import Optional, List

class Address(NS):
    street: str
    housenumber: int
    housenumber_postfix: Optional[str]=None


class Person(NS):
    name: str
    addresses: List[Address]


person = Person(
    name='Joe',
    addresses=[
        Address(street='Sesame', housenumber=1),
        Address(street='Baker', housenumber=221, housenumber_postfix='b')
    ])
  • 这适用于 Python 3.3 及更高版本
  • 字段是可变的(与 NamedTuple 解决方案不同)
  • 代码完成似乎在 PyCharm 中完美运行,但在 VSCode 中并非 100%(为此提出 issue
  • mypy 中的类型检查有效,但是 PyCharm 不会抱怨,例如 person.name = 1

如果有人能指出为什么 Python 3.7 的 dataclass 装饰器会更好,我很乐意听到。

也许这对 mypy

很有效
from typing import List
from mypy_extensions import TypedDict

EntityAndMeta = TypedDict("EntityAndMeta", {"name": str, "count": int})

my_list: List[EntityAndMeta] = [
  {"name": "Amy", "count": 17},
  {"name": "Bob", "count": 42},
]

mypy docs or from the source code

中阅读有关 TypedDict 的更多信息

我很确定你可以 nest these things, and set some of them to Optional 如果你愿意的话。

我的想法来自

TypeScript 接口描述了一个 JavaScript 对象。这样的对象类似于具有众所周知的字符串键的 Python 字典,它由 mypy TypedDict.

描述

TypeScript 界面示例

例如 TypeScript 接口:

interface Address {
    street: string;
    housenumber: number;
}

将描述 JavaScript 个对象,例如:

var someAddress = {
    street: 'SW Gemini Dr.',
    housenumber: 9450,
};

mypy TypedDict 示例

相当于mypyTypedDict:

from typing_extensions import TypedDict

class Address(TypedDict):
    street: str
    housenumber: int

将描述 Python 字典,例如:

some_address = {
    'street': 'SW Gemini Dr.',
    'housenumber': 9450,
}

# or equivalently:

some_address = dict(
    street='SW Gemini Dr.',
    housenumber=9450,
)

这些字典可以简单地序列化 to/from JSON 并且符合类似的 TypeScript 接口类型。

注意:如果您使用的是 Python 2 或 Python 3 的旧版本,您可能需要为 TypedDict 使用旧的基于函数的语法:

from mypy_extensions import TypedDict

Address = TypedDict('Address', {
    'street': str,
    'housenumber': int,
})

备选方案

在 Python 中还有其他方法来表示具有命名属性的结构。

Named tuples 很便宜并且有只读键。但是它们不能自动序列化 to/from JSON。

from typing import NamedTuple

class Address(NamedTuple):
    street: str
    housenumber: int

my_address = Address(
    street='SW Gemini Dr.',
    housenumber=9450,
)

Data classes,可用于Python 3.7,具有读写键。它们也不能自动序列化 to/from JSON。

from dataclasses import dataclass

@dataclass
class Address:
    street: str
    housenumber: int

my_address = Address(
    street='SW Gemini Dr.',
    housenumber=9450,
)

Simple namespaces,在 Python 3.3 中可用,与数据 类 相似,但不是很知名。

from types import SimpleNamespace

class Address(SimpleNamespace):
    street: str
    housenumber: int

my_address = Address(
    street='SW Gemini Dr.',
    housenumber=9450,
)

attrs is a long-standing third-party library that is similar to data classes but with many more features. attrs is recognized by the mypy typechecker.

import attrs

@attr.s(auto_attribs=True)
class Address:
    street: str
    housenumber: int

my_address = Address(
    street='SW Gemini Dr.',
    housenumber=9450,
)

尝试https://github.com/cs-cordero/py-ts-interfaces

看起来还不错。引用:

In web applications where Python is used in the backend and TypeScript is used in the frontend, it is often the case that the client will make calls to the backend to request some data with some specific pre-defined "shape". On the client-side, an interface for this data is usually defined and if the Python backend authors use typechecking, like with mypy, the project authors may be typing the JSON response values as well.

This results in a duplication of code. If the shape changes in the backend, the related interface must also be reflect its changes in the frontend. At best, this is annoying to maintain. At worst, over time the interfaces may diverge and cause bugs.

This library aims to have a single source of truth that describes the shape of the payload between the backend and the frontend.

你们觉得 pydantic 怎么样? python3.10

from pydantic import BaseModel


class Address(BaseModel):
    street: str
    housenumber: int
    housenumberPostfix: str | None = None


class Person(BaseModel):
    name: str
    adresses: list[Address]


person: Person = Person(
    name="Joe",
    adresses=[
        Address(street="Sesame", housenumber=1),
        Address(street="Baker", housenumber=221, housenumberPostfix="b"),
    ],
)

当我们输入错误的类型时。

Editor complain wrong type

有建议

Editor show suggestion