Это вторая часть моих размышлений на тему «Питон, каким я хотел бы его видеть», и в ней мы подробнее рассмотрим систему типов.
Для этого нам снова придется углубиться в особенности реализации языка Python и его интерпретатора CPython. Если вы программист Python, типы данных всегда были для вас второстепенным вопросом.
Они существуют где-то там сами по себе и как-то взаимодействуют друг с другом, но чаще всего об их существовании думаешь только тогда, когда происходит ошибка.
И тогда исключение сообщает вам, что один из типов данных ведет себя не так, как вы от него ожидали.
Python всегда гордился своей реализацией системы типов.
Я помню, как много лет назад читал документацию, в которой был целый раздел о преимуществах утиной печати.
Давайте проясним: да, для практических целей утиная печать — хорошее решение.
Если вы ничем не ограничены и вам не приходится бороться с типами данных, потому что их не существует, вы можете создавать очень красивые API. Python позволяет особенно легко решать повседневные проблемы.
Почти все API, которые я реализовал на Python, не работали на других языках программирования.
Даже такая простая вещь, как интерфейс командной строки (библиотека щелкнуть ) просто не работает на других языках, и основная причина в том, что вам приходится постоянно бороться с типами данных.
Вопрос о добавлении статической типизации в Python был поднят недавно, и я искренне надеюсь, что лед наконец-то тронулся.
Я попытаюсь объяснить, почему я против явной типизации и почему я надеюсь, что Python никогда не пойдет по этому пути.
Что такое «система типов»? Система типов — это набор правил, согласно которым типы взаимодействуют друг с другом.
Существует целая отрасль информатики, посвященная исключительно типам данных, что само по себе впечатляет, но даже если вас не интересует теория, вам будет трудно игнорировать систему типов.
Я не буду слишком углубляться в систему типов по двум причинам.
Во-первых, я сам не до конца разбираюсь в этой области, а во-вторых, на самом деле совсем не обязательно все понимать, чтобы «чувствовать» связи между типами данных.
Мне важно учитывать их поведение, поскольку оно влияет на архитектуру интерфейсов, и о типизации я буду говорить не как теоретик, а как практик (на примере построения красивого API).
Системы типов могут иметь множество характеристик, но самое важное различие между ними — это объем информации, которую тип данных раскрывает о себе, когда вы пытаетесь с ним работать.
Возьмем, к примеру, Python. У него есть типы.
Вот число 42, и если вы спросите это число, какого оно типа, оно ответит, что оно целое.
Это исчерпывающая информация, и она позволяет интерпретатору определить набор правил, согласно которым целые числа могут взаимодействовать друг с другом.
Однако в Python отсутствует одна вещь: составные типы данных.
Все типы данных в Python примитивны, а это означает, что вы можете работать только с одним из них одновременно, в отличие от составных типов.
Самый простой составной тип данных, встречающийся в большинстве языков программирования, — это структуры.
В Python их как таковых нет, но во многих случаях библиотекам необходимо определять свои собственные структуры, например модели ORM в Django и SQLAlchemy. Каждый столбец в базе данных представлен дескриптором Python, который соответствует полю в структуре, и когда вы говорите, что первичный ключ называется id и это IntegerField(), вы определяете модель как составной тип данных.
Составные типы не ограничиваются только структурами.
Когда вам нужно работать более чем с одним числом, вы используете коллекции (массивы).
Для этого в Python есть списки, и каждый элемент списка может иметь совершенно произвольный тип данных, в отличие от списков в других языках программирования, которые имеют заданный тип элемента (например, список целых чисел).
Фраза «список целых чисел» всегда имеет больше значения, чем просто список.
С этим можно спорить, ведь всегда можно пройтись по списку и увидеть тип каждого элемента, но что делать с пустым списком? Если у вас есть пустой список в Python, вы не можете определить его тип данных.
Та же проблема возникает при использовании значения None. Допустим, у вас есть функция, которая принимает аргумент «Пользователь».
Если вы передадите ему параметр None, вы никогда не узнаете, что это должен был быть объект User. Каково решение этой проблемы? Не иметь нулевых указателей и иметь массивы с явно указанными типами элементов.
Все знают, что это верно для Haskell, но есть и другие языки, менее враждебные по отношению к разработчикам.
Например, Rust — язык программирования, который нам ближе и понятнее, так как очень похож на C++.
А в Rust очень мощная система типов.
Как передать значение «пользователь не указан», если нет нулевых указателей? Например, в Rust для этого есть необязательные типы.
Таким образом, выражение Option представляет собой помеченное перечисление, которое оборачивает значение (в данном случае конкретного пользователя), и это означает, что можно передать либо Some(user), либо None. Поскольку переменная теперь может либо иметь значение, либо не иметь значения, весь код, работающий с этой переменной, должен уметь обрабатывать случаи корректной передачи значения None, иначе он просто не будет компилироваться.
Серое будущее Раньше существовало четкое разделение между динамически типизированными интерпретируемыми языками и статически типизированными компилируемыми языками.
Новые тенденции меняют устоявшиеся правила игры.
Первым признаком того, что мы вступаем на неизведанную территорию, стало появление языка C#.
Это статически типизированный компилируемый язык, и поначалу он был очень похож на Java. По мере развития языка C# в его системе типов начали появляться новые возможности.
Важнейшим событием стало появление обобщенных типов, которые позволили строго типизировать коллекции, не обрабатываемые компилятором (списки и словари).
Дальше — больше: создатели языка ввели возможность отказа от статической типизации переменных для целых блоков кода.
Это очень удобно, особенно при работе с данными, предоставляемыми веб-сервисами (JSON, XML и т. д.), поскольку позволяет выполнять потенциально небезопасные операции, перехватывать исключения из системы типов и уведомлять пользователей о неверных данных.
В настоящее время система типов языка C# является очень мощной и поддерживает универсальные типы с ковариантными и контравариантными спецификациями.
Он также поддерживает работу с типами, допускающими нулевые указатели.
Например, был добавлен оператор объединения null («?Э») для предоставления значений по умолчанию для объектов, представленных как null. Хотя C# уже зашел слишком далеко, чтобы избавиться от нулей, все узкие места находятся под контролем.
Другие статически типизированные компилируемые языки также пробуют новые подходы.
Таким образом, C++ всегда был статически типизированным языком, но его разработчики начали экспериментировать с выводом типов на многих уровнях.
Времена итераторов вроде MyType ::const_iterator ушли, и теперь почти во всех случаях вы можете использовать автотипы, и компилятор подставит вам нужный тип данных.
Язык программирования Rust также очень хорошо выполняет вывод типов и позволяет писать статически типизированные программы вообще без указания типов переменных:
Я верю, что в будущем нас ждут мощные системы типов.use std::collections::HashMap; fn main() { let mut m = HashMap::new(); m.insert("foo", vec!["some", "tags", "here"]); m.insert("bar", vec!["more", "here"]); for (key, values) in m.iter() { println!("{} = {}", key, values.connect("; ")); } }
Но, на мой взгляд, это не приведет к концу динамической типизации; скорее, эти системы будут развиваться по пути статической типизации с выводом локального типа.
Python и явная типизация Некоторое время назад на одной из конференций кто-то убедительно доказывал, что статическая типизация — это здорово, и язык Python в ней остро нуждается.
Я не помню точно, чем закончилось это обсуждение, но результатом стал проект mypy, который в сочетании с синтаксисом аннотаций был предложен в качестве золотого стандарта набора текста на Python 3.
Если вы не видели эту рекомендацию, она предлагает следующее решение: from typing import List
def print_all_usernames(users: List[User]) -> None:
for user in users:
print(user.username)
Я искренне убежден, что это не лучшее решение.
Причин много, но главная проблема в том, что система типов Python, к сожалению, уже не так хороша.
По сути, язык имеет разную семантику в зависимости от того, как на него посмотреть.
Чтобы статическая типизация имела смысл, система типов должна быть хорошо реализована.
Если у вас есть два типа, вы всегда должны знать, как типы должны взаимодействовать друг с другом.
В Python этого нет. Семантика типов Python Если вы читали предыдущую статью о системе слотов, вы должны помнить, что типы в Python ведут себя по-разному в зависимости от уровня, на котором они реализованы (C или Python).
Это очень специфическая особенность языка и такого вы больше нигде не увидите.
При этом на раннем этапе разработки многие языки программирования реализуют фундаментальные типы данных на уровне интерпретатора.
В Python просто нет «фундаментальных» типов, но есть целая группа типов данных, реализованных в C. И это не только примитивы и фундаментальные типы, они могут быть чем угодно, без всякой логики.
Например, класс Collections.OrderedDict написан на Python, а класс Collections.defaultdict из того же модуля — на C. Это создает массу проблем для интерпретатора PyPy, который хочет как можно лучше эмулировать исходные типы.
Это нужно для того, чтобы получить хороший API, в котором не будут заметны какие-либо различия с CPython. Очень важно понимать, в чем основное отличие слоя интерпретатора, написанного на C, от остального языка.
Другой пример — модуль re в версиях Python до 2.7. В более поздних версиях он был полностью переписан, но основная проблема по-прежнему актуальна: интерпретатор не работает как язык программирования.
Модуль re имеет функцию компиляции для компиляции регулярного выражения в шаблон.
Эта функция принимает строку и возвращает объект шаблона.
Это выглядит примерно так: >>> re.compile('foobar')
<_sre.SRE_Pattern object at 0x1089926b8>
Мы видим, что объект шаблона определен в модуле _sre, который является внутренним модулем, и тем не менее он нам доступен: >>> type(re.compile('foobar'))
<type '_sre.SRE_Pattern'>
К сожалению, это не так, поскольку модуль _sre на самом деле не содержит этого объекта: >>> import _sre
>>> _sre.SRE_Pattern
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'SRE_Pattern'
Ладно, это не первый и не единственный раз, когда парень обманывает нас по поводу своего местонахождения, и в любом случае это внутренний тип.
Давайте двигаться дальше.
Нам известен тип шаблона (_sre.SRE_Pattern), и он является потомком объектного класса: >>> isinstance(re.compile(''), object)
True
Мы также знаем, что все объекты реализуют некоторые очень распространенные методы.
Например, экземпляры таких классов имеют метод __repr__: >>> re.compile('').
__repr__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __repr__
Что происходит? Ответ весьма неожиданный.
По неизвестным мне причинам в Python до версии 2.7 объект шаблона SRE имел собственный слот tp_getattr. В этом слоте реализована собственная логика поиска атрибутов, которая обеспечивает доступ к собственным атрибутам и методам.
Если вы исследуете этот объект с помощью метода dir(), вы заметите, что многие вещи просто отсутствуют: >>> dir(re.compile(''))
['__copy__', '__deepcopy__', 'findall', 'finditer', 'match',
'scanner', 'search', 'split', 'sub', 'subn']
Это небольшое исследование поведения объекта-образца приводит нас к довольно неожиданным результатам.
Вот что происходит на самом деле.
Тип данных утверждает, что он наследуется от объекта.
Это верно для CPython, но не для самого Python. На уровне Python этот тип не связан с интерфейсом типа object. Каждый вызов, проходящий через интерпретатор, будет работать, в отличие от вызовов, проходящих через язык Python. Так, например, type(x) будет работать, а x.__class__ — нет. Что такое подкласс.
Приведенный выше пример показывает нам, что в Python может быть класс, который наследуется от другого класса, но его поведение не будет соответствовать базовому классу.
И это важный вопрос, если мы говорим о статической типизации.
Таким образом, в Python 3 вы не сможете реализовать интерфейс для типа dict, если не напишите его на C. Причина этого ограничения в том, что этот тип диктует поведение видимым объектам, которое просто невозможно реализовать.
Это невозможно.
Поэтому, когда вы применяете аннотацию типа и заявляете, что функция принимает в качестве аргумента словарь с ключами в виде строк и значениями в виде целых чисел, по вашей аннотации невозможно будет определить, принимает ли функция словарь или объект с поведение словаря, или может ли оно быть передано в подкласс словаря.
Неопределенное поведение Странное поведение объекта шаблона регулярного выражения было изменено в Python 2.7, но проблема осталась.
Как было показано на примере словарей, язык ведет себя по-разному в зависимости от того, как написан код, и полностью понять точную семантику системы типов просто невозможно.
Очень странное поведение внутренностей интерпретатора второй версии Python можно увидеть при сравнении типов экземпляров классов.
В третьей версии изменены интерфейсы и такое поведение для нее уже не актуально, но фундаментальную проблему все равно можно обнаружить на многих уровнях.
Давайте возьмем сортировку наборов в качестве примера.
Множества в Python — очень полезный тип данных, но при сравнении они ведут себя очень странно.
В Python 2 у нас есть функция cmp(), которая принимает два объекта в качестве аргументов и возвращает числовое значение, указывающее, какой из переданных аргументов больше.
Вот что произойдет, если вы попытаетесь сравнить два экземпляра заданного объекта: >>> cmp(set(), set())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot compare sets using cmp()
Почему это? Честно говоря, понятия не имею.
Возможно, причина в том, как операторы сравнения работают с множествами, а в cmp() это не работает. И в то же время экземпляры объектов Frozensets удивительно хорошо сравниваются: >>> cmp(frozenset(), frozenset())
0
Если одно из этих множеств не пусто, мы снова получим исключение.
Почему? Ответ прост: это оптимизация интерпретатора CPython, а не поведение языка Python. Пустой замороженный набор всегда имеет одно и то же значение (это неизменяемый тип, и мы не можем добавлять к нему элементы), поэтому это всегда один и тот же объект. Когда два объекта имеют в памяти одинаковый адрес, функция cmp() сразу возвращает 0. Я не сразу смог понять, почему это происходит, так как код функции сравнения в Python 2 слишком сложен и запутан, но эта функция имеет несколько путей, которые могут привести к такому результату.
Дело не только в том, что это баг.
Дело в том, что в Python нет четкого понимания того, как типы взаимодействуют друг с другом.
Вместо этого на все поведение системы типов Python всегда был один ответ: «так работает CPython».
Трудно переоценить объем работы, проделанной в PyPy для обратного проектирования поведения CPython. Учитывая, что PyPy написан на Python, возникает интересная проблема.
Если бы язык программирования Python описывался так, как реализована текущая часть языка Python, у PyPy было бы гораздо меньше проблем.
Поведение на уровне экземпляра Теперь представим, что у нас гипотетически есть версия Python, в которой исправлены все описанные проблемы.
И даже в этом случае мы не сможем добавить в язык статические типы.
Причина в том, что на уровне Python типы не имеют значения; что более важно, так это то, как объекты взаимодействуют друг с другом.
Например, объекты datetime обычно можно сравнивать с другими объектами.
Но если вы хотите сравнить два объекта datetime друг с другом, то это можно сделать только в том случае, если их часовые пояса совместимы.
Также результат многих операций может быть непредсказуемым, пока вы внимательно не изучите задействованные в них объекты.
Результатом объединения двух строк в Python 2 может быть либо юникодная, либо байтовая строка.
Различные API кодирования или декодирования из системы кодеков могут возвращать разные объекты.
Python как язык слишком динамичен, чтобы аннотации типов могли работать хорошо.
Только представьте, какую важную роль в языке играют генераторы, ведь они могут выполнять множество операций преобразования типов на каждой итерации.
Введение аннотаций типов будет иметь в лучшем случае неоднозначный эффект. Однако более вероятно, что это негативно повлияет на архитектуру API. Как минимум, если эти аннотации не удалить перед запуском программы, они замедлят выполнение кода.
Аннотации типов никогда не позволят эффективно статическую компиляцию без превращения Python во что-то, чем Python не является.
Багаж и семантика Я думаю, что мое личное негативное отношение к Python проистекает из абсурдной сложности, до которой достиг этого языка.
Ему просто не хватает спецификаций, а взаимодействие между типами сегодня стало настолько запутанным, что мы, возможно, никогда не сможем во всем этом разобраться.
Костылей и всех этих мелких поведенческих особенностей так много, что единственная возможная спецификация языка на сегодняшний день — это детальное описание того, как работает интерпретатор CPython. На мой взгляд, учитывая всё вышесказанное, введение аннотаций типов практически не имеет смысла.
Если кто-то в будущем захочет разработать новый язык программирования, который в основном является динамически типизированным, ему следует потратить дополнительное время на четкое описание того, как должна работать система типов.
JavaScript делает это достаточно хорошо, вся семантика встроенных типов подробно описана, даже в тех случаях, когда это не имеет смысла, и это, на мой взгляд, хорошая практика.
Если у вас есть четкое понимание того, как работает семантика языка, в будущем вам будет легко оптимизировать скорость интерпретатора или даже добавить опциональную статическую типизацию.
Поддержание связной и хорошо документированной языковой архитектуры позволяет избежать многих проблем.
Архитекторам будущих языков программирования обязательно следует избегать всех ошибок, которые допустили разработчики PHP, Python и Ruby, где поведение языка в конечном итоге объясняется поведением интерпретатора.
Я считаю, что Python вряд ли изменится к лучшему.
Чтобы избавить язык от всего этого тяжелого наследия, требуется слишком много времени и усилий.
Переведено Дредатур , текст читается как %username%.
Теги: #python #программирование #python3 #интерпретатор #cpython #pain
-
Этапы Ремонта Компьютеров И Ноутбуков
19 Oct, 24 -
Жирафа
19 Oct, 24 -
Trustzone: Доверенная Ос И Ее Приложения
19 Oct, 24 -
Пенсионный Фонд Против Нпф
19 Oct, 24