Если вам трудно читать на английском языке можно сразу перейти на русский перевод
сразу приносим свои извинения за машинный перевод.Introduction
With the release of version 3.5, Python has introduced type hints: code annotations that, through additional tooling, can check if you’re using your code correctly.
Long-time Python users might cringe at the thought of new code needing type hinting to work properly, but we need not worry: Guido himself wrote in PEP 484, “no type checking happens at runtime.”
The feature has been proposed mainly to open up Python code for easier static analysis and refactoring.
For data science–and for the data scientist– type hinting is invaluable for a couple of reasons:
- It makes it much easier to understand the code, just by looking at the signature, i.e. the first line(s) of the function definition;
- It creates a documentation layer that can be checked with a type checker, i.e. if you change the implementation, but forget to change the types, the type checker will (hopefully) yell at you.
Of course, as is always the case with documentation and testing, it’s an investment: it costs you more time at the beginning, but saves you (and your co-worker) a lot in the long run.
Note: Type hinting has also been ported to Python 2.7 (a.k.a Legacy Python). The functionality, however, requires comments to work. Furthermore, no one should be using Legacy Python in 2019: it’s less beautiful and only has a couple more months of updates before it stops receiving support of any kind.
Getting started with types
The code for this article may be found at Kite’s Github repository.
The hello world of type hinting is
# hello_world.py def hello_world(name: str = 'Joe') -> str: return f'Hello {name}'
We have added two type hint elements here. The first one is
: str
after name and the second one is-> str
towards the end of the signature.The syntax works as you would expect: we’re marking name to be of type
str
and we’re specifying that thehello_world
function should output astr
. If we use our function, it does what it says:> hello_world(name='Mark') 'Hello Mark'
Since Python remains a dynamically unchecked language, we can still shoot ourselves in the foot:
> hello_world(name=2) 'Hello 2'
What’s happening? Well, as I wrote in the introduction, no type checking happens at runtime.
So as long as the code doesn’t raise an exception, things will continue to work fine.
What should you do with these type definitions then? Well, you need a type checker, or an IDE that reads and checks the types in your code (PyCharm, for example).
Type checking your program
There are at least four major type checker implementations: Mypy, Pyright, pyre, and pytype:
- Mypy is actively developed by, among others, Guido van Rossum, Python’s creator.
- Pyright has been developed by Microsoft and integrates very well with their excellent Visual Studio Code;
- Pyre has been developed by Facebook with the goal to be fast (even though mypy recently got much faster);
- Pytype has been developed by Google and, besides checking the types as the others do, it can run type checks (and add annotations) on unannotated code.
Since we want to focus on how to use typing from a Python perspective, we’ll use Mypy in this tutorial. We can install it using
pip
(or your package manager of choice):$ pip install mypy $ mypy hello_world.py
Right now our life is easy: there isn’t much that can go wrong in our
hello_world
function. We’ll see later how this might not be the case anymore.More advanced types
In principle, all Python classes are valid types, meaning you can use
str
,int
,float
, etc. Using dictionary, tuples, and similar is also possible, but you need to import them from the typing module.# tree.py from typing import Tuple, Iterable, Dict, List, DefaultDict from collections import defaultdict def create_tree(tuples: Iterable[Tuple[int, int]]) -> DefaultDict[int, List[int]]: """ Return a tree given tuples of (child, father) The tree structure is as follows: tree = {node_1: [node_2, node_3], node_2: [node_4, node_5, node_6], node_6: [node_7, node_8]} """ tree = defaultdict(list) for child, father in tuples: if father: tree[father].append(child) return tree print(create_tree([(2.0,1.0), (3.0,1.0), (4.0,3.0), (1.0,6.0)])) # will print # defaultdict( 'list'="">, {1.0: [2.0, 3.0], 3.0: [4.0], 6.0: [1.0]}
While the code is simple, it introduces a couple of extra elements:
- First of all, the
Iterable
type for thetuples
variable. This type indicates that the object should conform to thecollections.abc.Iterable
specification (i.e. implement__iter__
). This is needed because we iterate overtuples
in thefor
loop;- We specify the types inside our container objects: the
Iterable
containsTuple
, theTuples
are composed of pairs ofint
, and so on.Ok, let’s try to type check it!
$ mypy tree.py tree.py:14: error: Need type annotation for 'tree'
Uh-oh, what’s happening? Basically Mypy is complaining about this line:
tree = defaultdict(list)
While we know that the return type should be
DefaultDict[int, List[int]]
, Mypy cannot infer that tree is indeed of that type. We need to help it out by specifying tree’s type. Doing so can be done similarly to how we do it in the signature:tree: DefaultDict[int, List[int]] = defaultdict(list)
If we now re-run Mypy again, all is well:
$ mypy tree.py $
Type aliases
Sometimes our code reuses the same composite types over and over again. In the above example,
Tuple[int, int]
might be such a case. To make our intent clearer (and shorten our code), we can use type aliases. Type aliases are very easy to use: we just assign a type to a variable, and use that variable as the new type:Relation = Tuple[int, int] def create_tree(tuples: Iterable[Relation]) -> DefaultDict[int, List[int]]: """ Return a tree given tuples of (child, father) The tree structure is as follow: tree = {node_1: [node_2, node_3], node_2: [node_4, node_5, node_6], node_6: [node_7, node_8]} """ # convert to dict tree: DefaultDict[int, List[int]] = defaultdict(list) for child, father in tuples: if father: tree[father].append(child) return tree
Generics
Experienced programmers of statically typed languages might have noticed that defining a
Relation
as a tuple of integers is a bit restricting. Can’tcreate_tree
work with a float, or a string, or the ad-hoc class that we just created?In principle, there’s nothing that prevents us from using it like that:
# tree.py from typing import Tuple, Iterable, Dict, List, DefaultDict from collections import defaultdict Relation = Tuple[int, int] def create_tree(tuples: Iterable[Relation]) -> DefaultDict[int, List[int]]: ... print(create_tree([(2.0,1.0), (3.0,1.0), (4.0,3.0), (1.0,6.0)])) # will print # defaultdict( 'list'="">, {1.0: [2.0, 3.0], 3.0: [4.0], 6.0: [1.0]})
However if we ask Mypy’s opinion of the code, we’ll get an error:
$ mypy tree.py tree.py:24: error: List item 0 has incompatible type 'Tuple[float, float]'; expected 'Tuple[int, int]' ...
There is a way in Python to fix this. It’s called
TypeVar
, and it works by creating a generic type that doesn’t require assumptions: it just fixes it throughout our module. Usage is pretty simple:from typing import TypeVar T = TypeVar('T') Relation = Tuple[T, T] def create_tree(tuples: Iterable[Relation]) -> DefaultDict[T, List[T]]: ... tree: DefaultDict[T, List[T]] = defaultdict(list) ... print(create_tree([(2.0,1.0), (3.0,1.0), (4.0,3.0), (1.0,6.0)]))
Now, Mypy will no longer complain, and programmers will be happy as the type hints for
create_tree
correctly reflect thatcreate_tree
works for more than just integers.Note that it’s important that the
‘T’
insideTypeVar
is equal to the variable nameT
.Generic classes: Should I have used a
TypeVar
?What I said about
create_tree
at the beginning of this section is not 100% accurate. SinceT
will be used as a key to a dictionary, it needs to be hashable.This is important as the key lookup in Python works by computing the hash of the key. If the key is not hashable, the lookup will break.
Such properties are encountered enough that Python offers a few types which can indicate that an object should have certain properties (e.g. it should be hashable if we want it to be a key to a dictionary).
Some examples:
typing.Iterable
will indicate that we expect the object to be an iterable;typing.Iterator
will indicate that we expect the object to be an iterator;typing.Reversible
will indicate that we expect the object to be reversible;typing.Hashable
will indicate that we expect the object to implement__hash__
;typing.Sized
will indicate that we expect the object to implement__len__
;typing.Sequence
will indicate that we expect the object to beSized
,Iterable
,Reversible
, and implementcount
,index
.These are important, because sometimes we expect to use those methods on our object, but don’t care which particular class they belong to as long as they have the methods needed. For example, if we’d like to create our own version of
chain
to chain sequences together, we could do the following:from typing import Iterable, TypeVar T = TypeVar('T') def return_values() -> Iterable[float]: yield 4.0 yield 5.0 yield 6.0 def chain(*args: Iterable[T]) -> Iterable[T]: for arg in args: yield from arg print(list(chain([1, 2, 3], return_values(), 'string'))) [1, 2, 3, 4.0, 5.0, 6.0, 's', 't', 'r', 'i', 'n', 'g']
The
return_values
function is a bit contrived but it illustrates the point: the functionchain
doesn’t care about who we are as long as we’re iterable!Any, Union, and Optional
Python provides another couple of features that are handy when writing code with type hints:
- Any does what you think it does, marking the object to not have any specific type
- Union can be used as
Union[A, B]
to indicate that the object can have type A or B- Optional is used as
Optional[A]
to indicate that the object is either of type A or None. Contrary to real functional languages, we can’t expect safety when sending Optionals around, so beware. It effectively works as aUnion[A, None]
. Functional programming lovers will recognize their beloved Option (if you come from Scala) or Maybe (if you come from Haskell).Callables
Python supports passing functions as arguments to other functions, but how should we annotate them?
The solution is to use
Callable[[arg1, arg2], return_type]
. If there are many arguments, we can cut them short by using an ellipsisCallable[..., return_type]
.As an example, let’s assume we want to write our own map/reduce function (different from Hadoop’s MapReduce!). We could do it with type annotations like this:
# mr.py from functools import reduce from typing import Callable, Iterable, TypeVar, Union, Optional T = TypeVar('T') S = TypeVar('S') Number = Union[int, float] def map_reduce( it: Iterable[T], mapper: Callable[[T], S], reducer: Callable[[S, S], S], filterer: Optional[Callable[[S], bool]] ) -> S: mapped = map(mapper, it) filtered = filter(filterer, mapped) reduced = reduce(reducer, filtered) return reduced def mapper(x: Number) -> Number: return x ** 2 def filterer(x: Number) -> bool: return x % 2 == 0 def reducer(x: Number, y: Number) -> Number: return x + y results = map_reduce( range(10), mapper=mapper, reducer=reducer, filterer=filterer ) print(results)
Just by looking at the signature of
map_reduce
we can understand how data flows through the function: the mapper gets aT
and outputs anS
, the filterer, if notNone
, filters theS
s, and the reducers combines theS
s in the ultimateS
.Combined with proper naming, type hints can clarify what the function does without looking at the implementation.
External modules
Annotating our code is nice, but what about all the other modules we might be using? Data scientists import often from, say, NumPy or pandas. Can we annotate functions accepting NumPy arrays as input?
Well, there’s only one way to find out:
# rescale.py import numpy as np def rescale_from_to(array1d: np.ndarray, from_: float=0.0, to: float=5.0) -> np.ndarray: min_ = np.min(array1d) max_ = np.max(array1d) rescaled = (array1d - min_) * (to - from_) / (max_ - min_) + from_ return rescaled my_array: np.array = np.array([1, 2, 3, 4]) rescaled_array = rescale_from_to(my_array)
We can now type check it:
❯ mypy rescale.py rescale.py:1: error: No library stub file for module 'numpy' rescale.py:1: note: (Stub files are from https://github.com/python/typeshed)
It’s failing on line 1 already! What’s happening here is that numpy does not have type annotations, so it’s impossible for Mypy to know how to do the checking (note from the error message that the whole standard library has type annotations through the typeshed project.)
There are a couple of ways to fix this:
- Use
mypy --ignore-missing-import rescale.py
on the command line. This has the drawback that it will ignore mistakes as well (misspelling the package name, for example)- Append
# type: ignore
after the module name
import numpy as np # type: ignore
- We can create a
.mypy.ini
file in our home folder (or amypy.ini
in the folder where our project is) with the following content# mypy.ini [mypy] [mypy-numpy] ignore_missing_imports = True
I am personally a big fan of the third option, because once a module adds type supports, we can remove it from a single file and be done with it. On the other hand, if we use
mypy.ini
in the folder where the project is, we can put that into version control and have every coworker share the same configuration.Conclusion
We learned how to create functions and modules with type hints, and the various possibilities of complex types, generics, and
TypeVar
. Furthermore, we looked at how a type checker such as Mypy can help us catch early mistakes in our code.Type hints are — and probably will remain — an optional feature in Python. We don’t have to cover our whole code with type hinting to start, and this is one of the main selling points of using types in Python.
Instead, we can start by annotating functions and variables here and there, and gradually start enjoying code that has all the advantages of type hinting.
As you use type hinting more and more, you will experience how they can help create code that’s easier for others to interpret, catch bugs early on, and maintain a cleaner API.
If you want to know more about type hints, the Mypy documentation has an excellent type system reference.
The code for this article may be found at Kite’s Github repository.
Введение
С выпуском версии 3.5 Python представил подсказки типа: аннотации кода, которые с помощью дополнительных инструментов могут проверить, правильно ли вы используете свой код.
Давние пользователи Python могли бы съежиться при мысли о новом коде, нуждающемся в типе, намекающем на правильную работу, но нам не нужно беспокоиться: сам Гвидо написал в PEP 484: “никакая проверка типа не происходит во время выполнения.”
Эта функция была предложена в основном для открытия кода Python для облегчения статического анализа и рефакторинга.
Для науки о данных–и для ученого данных– намек типа бесценен по нескольким причинам:
- Это значительно облегчает понимание кода, просто глядя на подпись, т. е. первую строку (ы) определения функции;
- Он создает уровень документации, который можно проверить с помощью средства проверки типов, т. е. если вы измените реализацию, но забудете изменить типы, средство проверки типов будет (надеюсь) кричать на вас.
Конечно, как это всегда бывает с документацией и тестированием, это инвестиции: это стоит вам больше времени в начале, но экономит вас (и вашего сотрудника) много в долгосрочной перспективе.
Примечание: подсказка типа также была перенесена в Python 2.7 (a.k.a Legacy Python). Функциональность, однако, требует комментариев для работы. Кроме того, никто не должен использовать Legacy Python в 2019 году: он менее красив и имеет всего несколько месяцев обновлений, прежде чем он перестанет получать поддержку любого рода.
Начало работы с типами
Код для этой статьи можно найти в репозитории Kite на Github.
Привет мир типа намеков является
# hello_world.py def hello_world(name: str = 'Joe') -> str: return f'Hello {name}'Здесь мы добавили два элемента подсказки типа. Первый -
: str
после имени, а второй--> str
ближе к концу подписи.Синтаксис работает так, как вы ожидаете: мы помечаем имя как тип
str
и указываем, чтоhello_world
функция должна выводить astr
. Если мы используем нашу функцию, она делает то, что говорит:> hello_world(name='Mark') 'Hello Mark'
Поскольку Python остается динамически непроверенным языком, мы все еще можем стрелять себе в ногу:
> hello_world(name=2) 'Hello 2'
- Что происходит? Ну, как я уже писал во введении, никакой проверки типа не происходит во время выполнения .
Так что пока код не вызывает исключение, все будет работать нормально.
Что же тогда делать с этими определениями типов? Ну, вам нужна проверка типов или IDE, которая читает и проверяет типы в вашем коде (PyCharm, например).
Тип проверка вашей программы
Существует по крайней мере четыре основных реализации проверки типов: Mypy, Pyright, pyre и pytype:
- Mypy активно разрабатывается, в частности, Гвидо ван Россум, создателем Python.
- Pyright был разработан корпорацией Майкрософт и очень хорошо интегрируется с их превосходным кодом Visual Studio;
- Pyre был разработан Facebook с целью быть быстрым (хотя mypy в последнее время стал намного быстрее);
- Pytype has been developed by Google and, besides checking the types as the others do, it can run type checks (and add annotations) on unannotated code.
Поскольку мы хотим сосредоточиться на том, как использовать набор текста с точки зрения Python, мы будем использовать Mypy в этом уроке. Мы можем установить его с помощью
pip
(или ваш менеджер пакетов выбора):$ pip install mypy $ mypy hello_world.py
Прямо сейчас наша жизнь легка: нет ничего, что может пойти не так в нашей
hello_world
функции. Позже мы увидим, что это может быть уже не так.Более продвинутые типы
В принципе, все классы Python являются допустимыми типами, то есть вы можете использовать
str
,int
float
, и т.д. Использование словаря, кортежей и им подобных также возможно, но вам нужно импортировать их из модуля ввода текста.# tree.py from typing import Tuple, Iterable, Dict, List, DefaultDict from collections import defaultdict def create_tree(tuples: Iterable[Tuple[int, int]]) -> DefaultDict[int, List[int]]: """ Return a tree given tuples of (child, father) The tree structure is as follows: tree = {node_1: [node_2, node_3], node_2: [node_4, node_5, node_6], node_6: [node_7, node_8]} """ tree = defaultdict(list) for child, father in tuples: if father: tree[father].append(child) return tree print(create_tree([(2.0,1.0), (3.0,1.0), (4.0,3.0), (1.0,6.0)])) # will print # defaultdict( 'list'="">, {1.0: [2.0, 3.0], 3.0: [4.0], 6.0: [1.0]}
Хотя код прост, он вводит несколько дополнительных элементов:
- Прежде всего,
Iterable
тип дляtuples
переменной. Этот тип указывает, что объект должен соответствоватьcollections.abc.Iterable
спецификации (т. е. реализовать__iter__
). Это необходимо, потому что мы повторяемtuples
вfor
цикле;- Мы задаем типы внутри наших объектов контейнера:
Iterable
содержитTuple
,Tuples
состоят из парint
, и так далее.Хорошо, давайте попробуем ввести проверить его!
$ mypy tree.py tree.py:14: error: Need type annotation for 'tree'
А-а, что происходит? В основном Mypy жалуется на эту линию:
tree = defaultdict(list)
Хотя мы знаем, что возвращаемый тип должен быть
DefaultDict[int, List[int]]
, Mypy не может сделать вывод, что дерево действительно относится к этому типу. Нам нужно помочь ему, указав тип дерева. Это можно сделать аналогично тому, как мы делаем это в подписи:tree: DefaultDict[int, List[int]] = defaultdict(list)
Если мы сейчас снова запустим Mypy, все будет хорошо:
$ mypy tree.py $
Псевдонимы типов
Иногда наш код повторно использует одни и те же составные типы снова и снова. В приведенном выше примере,
Tuple[int, int]
возможно, будет такой случай. Чтобы сделать наше намерение более ясным (и сократить наш код), мы можем использовать псевдонимы типов. Псевдонимы типов очень просты в использовании: мы просто назначаем тип переменной и используем эту переменную в качестве нового типа:Relation = Tuple[int, int] def create_tree(tuples: Iterable[Relation]) -> DefaultDict[int, List[int]]: """ Return a tree given tuples of (child, father) The tree structure is as follow: tree = {node_1: [node_2, node_3], node_2: [node_4, node_5, node_6], node_6: [node_7, node_8]} """ # convert to dict tree: DefaultDict[int, List[int]] = defaultdict(list) for child, father in tuples: if father: tree[father].append(child) return tree
Дженерик
Опытные программисты статически типизированных языков могли бы заметить, что определение a
Relation
как кортеж целых чисел немного ограничивает. Не можетcreate_tree
работать с float, строкой или специальным классом, который мы только что создали?В принципе, нет ничего, что мешает нам использовать его таким образом:
# tree.py from typing import Tuple, Iterable, Dict, List, DefaultDict from collections import defaultdict Relation = Tuple[int, int] def create_tree(tuples: Iterable[Relation]) -> DefaultDict[int, List[int]]: ... print(create_tree([(2.0,1.0), (3.0,1.0), (4.0,3.0), (1.0,6.0)])) # will print # defaultdict( 'list'="">, {1.0: [2.0, 3.0], 3.0: [4.0], 6.0: [1.0]})
Однако если мы спросим мнение Mypy о коде, мы получим ошибку:
$ mypy tree.py tree.py:24: error: List item 0 has incompatible type 'Tuple[float, float]'; expected 'Tuple[int, int]' ...
Есть способ в Python, чтобы исправить это. Это называется
TypeVar
, и он работает, создавая универсальный тип, который не требует предположений: он просто фиксирует его по всему нашему модулю. Использование довольно простое:from typing import TypeVar T = TypeVar('T') Relation = Tuple[T, T] def create_tree(tuples: Iterable[Relation]) -> DefaultDict[T, List[T]]: ... tree: DefaultDict[T, List[T]] = defaultdict(list) ... print(create_tree([(2.0,1.0), (3.0,1.0), (4.0,3.0), (1.0,6.0)]))
Теперь Mypy больше не будет жаловаться, и программисты будут счастливы, поскольку подсказки типа
create_tree
правильно отражают, чтоcreate_tree
работает больше, чем просто целые числа.Обратите внимание, что важно, чтобы
‘T’
внутренняяTypeVar
часть была равна имени переменнойT
.Общие классы: должен ли я использовать a
TypeVar
?То, что я сказал
create_tree
в начале этого раздела, не является 100% точным. ПосколькуT
он будет использоваться в качестве ключа к словарю, он должен быть хэшируемым .Это важно, поскольку поиск ключа в Python работает путем вычисления хэша ключа. Если ключ не хэшируется, поиск будет прерван.
Такие свойства встречаются достаточно часто, что Python предлагает несколько типов, которые могут указывать, что объект должен иметь определенные свойства (например, он должен быть хэшируемым, если мы хотим, чтобы он был ключом к словарю).
Примеры:
typing.Iterable
будет указано, что мы ожидаем, что объект будет итерационным;typing.Iterator
будет указано, что мы ожидаем, что объект будет итератором;typing.Reversible
будет указывать, что мы ожидаем, что объект будет обратимым;typing.Hashable
будет указано, что мы ожидаем объект для реализации__hash__
;typing.Sized
будет указано, что мы ожидаем объект для реализации__len__
;typing.Sequence
будет указывать, что мы ожидаем, что объект будетSized
,Iterable
Reversible
, и реализоватьcount
,index
.Они важны, потому что иногда мы ожидаем использовать эти методы на нашем объекте, но не важно, к какому конкретному классу они принадлежат, пока у них есть необходимые методы. Например, если бы мы хотели создать нашу собственную версию
chain
to chain sequences вместе, мы могли бы сделать следующее:from typing import Iterable, TypeVar T = TypeVar('T') def return_values() -> Iterable[float]: yield 4.0 yield 5.0 yield 6.0 def chain(*args: Iterable[T]) -> Iterable[T]: for arg in args: yield from arg print(list(chain([1, 2, 3], return_values(), 'string'))) [1, 2, 3, 4.0, 5.0, 6.0, 's', 't', 'r', 'i', 'n', 'g']
Эта
return_values
функция немного надуманна, но она иллюстрирует суть: функцияchain
не заботится о том, кто мы, пока мы повторяемы!Любой, соединение, и опционное
Python предоставляет еще несколько функций, которые удобны при написании кода с подсказками типа:
- Любой делает то, что вы думаете, что он делает, помечая объект, чтобы не иметь никакого определенного типа
- Объединение может использоваться как
Union[A, B]
указание на то, что объект может иметь тип A или B- Необязательно используется
Optional[A]
для указания того, что объект относится к типу A или нет . В отличие от реальных функциональных языков, мы не можем ожидать безопасности при отправке Optionals вокруг, так что будьте осторожны. Оно эффектно работает какUnion[A, None]
А. Любители функционального программирования узнают свой любимый вариант (если вы пришли из Scala) или, возможно, (если вы пришли из Haskell).Вызываемые объекты
Python поддерживает передачу функций в качестве аргументов другим функциям, но как мы должны их аннотировать?
Решение заключается в использовании
Callable[[arg1, arg2], return_type]
. Если аргументов много, мы можем сократить их, используя многоточиеCallable[..., return_type]
.В качестве примера предположим, что мы хотим написать свою собственную функцию map / reduce (в отличие от MapReduce от Hadoop!). Мы могли бы сделать это с аннотациями типа, как это:
# mr.py from functools import reduce from typing import Callable, Iterable, TypeVar, Union, Optional T = TypeVar('T') S = TypeVar('S') Number = Union[int, float] def map_reduce( it: Iterable[T], mapper: Callable[[T], S], reducer: Callable[[S, S], S], filterer: Optional[Callable[[S], bool]] ) -> S: mapped = map(mapper, it) filtered = filter(filterer, mapped) reduced = reduce(reducer, filtered) return reduced def mapper(x: Number) -> Number: return x ** 2 def filterer(x: Number) -> bool: return x % 2 == 0 def reducer(x: Number, y: Number) -> Number: return x + y results = map_reduce( range(10), mapper=mapper, reducer=reducer, filterer=filterer ) print(results)
Просто глядя на сигнатуру
map_reduce
мы можем понять, как данные текут через функцию: картограф получает aT
и выводит anS
, фильтр, если нетNone
, фильтруетS
s, а редукторы объединяютS
s в конечномS
итоге .В сочетании с правильными именами подсказками типа можно уточнить, что делает функция, не глядя на реализацию.
Внешний модуль
Аннотирование нашего кода приятно, но как насчет всех других модулей, которые мы могли бы использовать? Данные ученые часто импортируют из, скажем, NumPy или панд. Можно ли аннотировать функции, принимающие массивы NumPy в качестве входных данных?
Ну, есть только один способ узнать это:
# rescale.py import numpy as np def rescale_from_to(array1d: np.ndarray, from_: float=0.0, to: float=5.0) -> np.ndarray: min_ = np.min(array1d) max_ = np.max(array1d) rescaled = (array1d - min_) * (to - from_) / (max_ - min_) + from_ return rescaled my_array: np.array = np.array([1, 2, 3, 4]) rescaled_array = rescale_from_to(my_array)
Теперь мы можем ввести проверить его:
❯ mypy rescale.py rescale.py:1: error: No library stub file for module 'numpy' rescale.py:1: note: (Stub files are from https://github.com/python/typeshed)
Он уже терпит неудачу на линии 1! Здесь происходит то, что numpy не имеет аннотаций типа, поэтому Mypy не может знать, как выполнить проверку (обратите внимание на сообщение об ошибке, что вся стандартная библиотека имеет аннотации типа через проект typeshed.)
Есть несколько способов исправить это:
- Используйте
mypy --ignore-missing-import rescale.py
в командной строке. Это имеет тот недостаток, что он также будет игнорировать ошибки (например, неверное написание имени пакета)- Добавить
# type: ignore
после имени модуля
import numpy as np # type: ignore
- Мы можем создать
.mypy.ini
файл в нашей домашней папке (илиmypy.ini
в папке, где находится наш проект) со следующим содержимым# mypy.ini [mypy] [mypy-numpy] ignore_missing_imports = True
Я лично большой поклонник третьего варианта, потому что как только модуль добавляет поддержку типа, мы можем удалить его из одного файла и сделать с ним. С другой стороны, если мы используем
mypy.ini
в папке, где находится проект, мы можем поместить его в систему управления версиями и заставить каждого сотрудника использовать одну и ту же конфигурацию.Вывод
Мы узнали, как создавать функции и модули с подсказками типа, а также различные возможности сложных типов, обобщений и
TypeVar
. Кроме того, мы рассмотрели, как средство проверки типов, такое как Mypy, может помочь нам поймать ранние ошибки в нашем коде.Подсказки типа являются — и, вероятно, останутся-необязательной функцией в Python. Нам не нужно покрывать весь наш код намеком на тип, чтобы начать, и это одна из основных точек продаж использования типов в Python.
Вместо этого мы можем начать с аннотирования функций и переменных здесь и там, и постепенно начать пользоваться кодом, который имеет все преимущества намеков типа.
По мере того как вы все больше и больше используете подсказки типа, вы будете испытывать, как они могут помочь создать код, который легче для других интерпретировать, ловить ошибки на ранней стадии и поддерживать более чистый API.
Если вы хотите узнать больше о подсказках типа, документация Mypy имеет отличную ссылку на систему типов .
Код для этой статьи можно найти в репозитории Kite на Github.
Python