web-dev-qa-db-fra.com

Python équivalent de l'interface TypeScript

Récemment, j'ai beaucoup travaillé avec TypeScript, cela permet d'exprimer des choses comme:

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' }
    ]
}

Assez concis et donnant tout le luxe comme vérification de type et complétion de code lors du codage avec des personnes.

Comment cela se fait-il en Python?

J'ai regardé Mypy et ABC mais je n'ai pas encore réussi à trouver la façon Pythonique de faire quelque chose de similaire à ce qui précède (mes tentatives ont abouti à trop de passe-partout à mon goût).

21
Otto

Pour l'achèvement du code et l'indication de type dans les IDE, ajoutez simplement une saisie statique pour les classes Person et Address et vous êtes déjà prêt. En supposant que vous utilisez la dernière python3.6, voici un équivalent approximatif des classes TypeScript de votre exemple:

# 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

Je suppose que le passe-partout que vous avez mentionné émerge lors de l'ajout des constructeurs de classe. Ceci est en effet inévitable. Je souhaiterais que les constructeurs par défaut soient générés lors de l'exécution lorsqu'ils ne sont pas déclarés explicitement, comme ceci:

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

mais malheureusement, vous devez les déclarer manuellement.


Modifier

Comme Michael0x2a souligné dans le commentaire , le besoin de constructeurs par défaut est rendu évitable dans python3.7 qui a introduit un @dataclass décorateur, donc on peut en effet déclarer:

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


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

et obtenir l'impl par défaut de plusieurs méthodes, réduisant la quantité de code passe-partout. Consultez PEP 557 pour plus de détails.


Je suppose que vous pouvez voir des fichiers stub qui peuvent être générés à partir de votre code, comme une sorte de fichiers d'interface:

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

Le fichier stub généré contient les signatures typées de toutes les classes et fonctions non privées du module sans implémentation:

# 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

Ces fichiers de raccord sont également reconnus par les IDE et si votre module d'origine n'est pas typé statiquement, ils utiliseront le fichier de raccord pour les conseils de type et la complétion de code.

14
hoefling

Python 3.6 a ajouté une nouvelle implémentation de namedtuple qui fonctionne avec les indications de type, ce qui supprime une partie du passe-partout requis par les autres réponses.

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'),
    ],
)

Edit: NamedTuples sont immuables, alors sachez que vous ne pouvez pas utiliser cette solution si vous souhaitez modifier les champs de vos objets. Changer le contenu de lists et dicts est toujours correct.

5
Brian Schlenker

Une solution simple que j'ai trouvée (qui ne nécessite pas Python 3.7) est d'utiliser 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')
    ])
  • Cela fonctionne en Python 3.3 et supérieur
  • Les champs sont modifiables (contrairement à la solution NamedTuple)
  • L'achèvement du code semble fonctionner parfaitement dans PyCharm mais pas à 100% dans VSCode (soulevé un problème pour cela)
  • La vérification de type dans mypy fonctionne, mais PyCharm ne se plaint pas si je fais par exemple person.name = 1

Si quelqu'un peut expliquer pourquoi Python 3.7's dataclass décorateur serait mieux que j'aimerais entendre.

4
Otto

Avec Python 3.5, vous pouvez utiliser des annotations pour spécifier le type de paramètres et les types de retour. La plupart des IDE récents, comme PyCharm, peuvent interpréter ces annotations et vous donner une bonne complétion de code. Vous pouvez également utiliser un commentaire pour spécifier la signature d'une fonction ou le type d'une variable.

Voici un exemple:

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')
    ])

Notez que Python n'est pas un langage fortement typé. Les annotations ne sont donc qu'un guide pour les développeurs. Si vous voulez vraiment vérifier votre code, vous avez besoin d'un outil externe (actuellement, le meilleur est mypy ). Il peut être utilisé comme n'importe quel autre vérificateur de code pendant le contrôle de la qualité du code.

3
Laurent LAPORTE

Peut-être que cela fonctionnera bien avec 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},
]

En savoir plus sur TypedDict dans les mypy docs ou dans les code source

Je suis sûr que vous pouvez imbriquer ces choses , et en définir certaines sur Optional si vous le souhaitez.

J'ai eu cette idée de https://stackoverflow.com/a/21014863/5017391

2
Boris Yakubchik

Une interface TypeScript décrit un objet JavaScript. Un tel objet est analogue à un Python avec des clés de chaîne bien connues, qui est décrit par un mypy TypedDict.

Exemple d'interface TypeScript

Par exemple, l'interface TypeScript:

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

décrira des objets JavaScript comme:

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

exemple typypict mypy

Le mypy équivalent TypedDict:

from typing_extensions import TypedDict

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

décrira Python dictionnaires comme:

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

# or equivalently:

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

Ces dictionnaires peuvent être sérialisés vers/depuis JSON de manière triviale et seront conformes au type d'interface TypeScript analogue.

Remarque: Si vous utilisez Python 2 ou des versions antérieures de Python 3, vous devrez peut-être utiliser l'ancienne syntaxe basée sur les fonctions pour TypedDict:

from mypy_extensions import TypedDict

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

Alternatives

Il existe d'autres façons dans Python pour représenter les structures avec des propriétés nommées.

Tuples nommés sont bon marché et ont des clés en lecture seule. Cependant, ils ne peuvent pas être sérialisés vers/depuis JSON automatiquement.

from typing import NamedTuple

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

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

Classes de données, disponibles dans Python 3.7, ont des clés de lecture-écriture. Ils ont également ne peut pas être sérialisé vers/depuis JSON automatiquement.

from dataclasses import dataclass

@dataclass
class Address:
    street: str
    housenumber: int

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

Espaces de noms simples, disponible dans Python 3.3, sont similaires aux classes de données mais ne le sont pas très bien connu.

from types import SimpleNamespace

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

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

attrs est une bibliothèque tierce partie similaire aux classes de données mais avec beaucoup plus de fonctionnalités. attrs est reconnu par le vérificateur de caractères mypy .

import attrs

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

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