Как я должен представлять тип строки, содержащей путь к файлу, в аннотациях типа? - PullRequest
0 голосов
/ 10 июня 2019

Я пишу библиотеку, которая предоставляет класс, который должен быть разделен на подклассы конечными пользователями. Конструктор класса заботится о типах аргументов методов, добавляемых в подкласс. Я хочу, чтобы конечные пользователи могли указывать типы аргументов с помощью аннотаций типов.

Одна из вещей, о которой заботится мой класс, - является ли аргумент строкой, содержащей путь к файлу. По логике это должен быть подтип str, который можно обозначить так:

import mylib
from typing import *

FilePath = NewType('FilePath', AnyStr)

class MySubclass(mylib.MyClass):
    def my_method(self, path: FilePath):
        return open(path)

Но строка документа typing.NewType дает следующие примеры:

from typing import *

UserId = NewType('UserId', int)

def name_by_id(user_id: UserId) -> str:
    ...

name_by_id(42)          # Fails type check
name_by_id(UserId(42))  # OK

Таким образом, чтобы статическая проверка типов не вызывала сбоев кода, использующего мою библиотеку, пользователям необходимо было сделать следующее:

from mylib import *

... # MySubclass defined as above

o = MySubclass()
o.my_method(FilePath('foo/bar.baz'))

Но я хочу, чтобы они могли просто сделать

o.my_method('foo/bar.baz')

без статической ошибки проверки типов. Это связано с тем, что меня беспокоит семантика определяемых мной типов, а не опасность того, что кто-нибудь когда-либо реально использует мой код и , пытаясь запустить на нем статическую проверку типов.

Одним из решений является определение FilePath как

FilePath = Union[AnyStr, NewType('FilePath', AnyStr)]

но это сбивает с толку взгляд, и его __repr__ - откровенная ложь:

>>> FilePath
Union[Anystr, FilePath]

Есть ли лучший способ?

1 Ответ

1 голос
/ 10 июня 2019

Ваши две цели несовместимы: вы не можете одновременно указать, что ваш метод принимает только определенные объекты, похожие на пути (или даже определенный подтип str), в то же время позволяя вызывающей стороне напрямую передавать какую-то произвольную строку.

Вам нужно выбрать один из двух.

Если вы решите пойти с первым (укажите, что метод принимает только определенные объекты, похожие на пути), более приемлемой альтернативой использованию NewTypes может быть вместо этогопереключитесь на то, чтобы ваш метод принимал только pathlib.Path объекты :

from pathlib import Path

class MyClass:
    def my_method(self, x: Path) -> None: ...

MyClass().my_method(Path("foo/bar.baz"))

Ваши вызывающие по-прежнему должны будут преобразовывать свои строки в эти объекты Path, но по крайней мере теперь они получат некоторыеот этого выиграет реальная среда выполнения.

Если вы решите пойти с последней целью (разрешить пользователям передавать строки напрямую), вы можете также избавиться от всех ваших типов NewTypes и переключиться на использование str (илиUnion[Text, bytes] или AnyStr) напрямую.Это было бы более честной сигнатурой типа:

class MyClass:
    def my_method(self, x: str) -> None: ...

MyClass().my_method("foo/bar.baz")

Возможно, вы могли бы сделать это немного более читабельным, используя псевдонимы типов, например так:

MaybeAPath = str

class MyClass:
    def my_method(self, x: MaybeAPath) -> None: ...

MyClass().my_method("foo/bar.baz")

... но этотолько улучшение читабельности.Чтобы быть полностью безопасным для типов, ваш код и код вашего подкласса все равно должны включать некоторую обработку ошибок, если они получают какую-то случайную строку, которую невозможно проанализировать как путь.

Я лично склоняюсь к использованию pathlib.Путь к объектам везде, для чего это стоит.

...