что такое утиная типизация
«Duck typing» и C#
Я не буду очень углубляться в саму реализацию. Её можно посмотреть в репозитории ссылка на который будет внизу. Там нет ничего сложного для тех кто уже баловался с генераторами, а для всеx остальных потребуется намного большая статья.
Как этим пользоваться
Представим что у нас есть следующий пример:
Компилятор С# скажет следующее:
Argument type ‘AddCalculator’ is not assignable to parameter type ‘ICalculator’
И это всё. Ошибок компиляции больше нет и все работает как часы.
Как это работает
Здесь используются два независимых генератора. Первый ищет аттрибут Duckable и генерит «базовый» класс для интерфейса. Например, для ICalculator он будет иметь следующий вид:
Второй генератор ищет вызовы методов и присваивания чтобы понять как duckable интерфейс используется. Расмотрим следующий пример:
Поскольку DICalculator это partial class мы можем реализовать подобные расширения для нескольких типов сразу и ничего не сломать. Этот трюк работает не только для методов, но и для пропертей:
Что не работает
На этом хорошие новости закончились. Всё-таки реализовать прямо вездесущий duck typing не получится. Поскольку мы скованы самим компилятором. А именно будут проблемы с дженериками и ref struct-урами. В теории часть проблем с дженериками можно починить, но не все. Например, было бы прикольно чтобы мы могли использовать наши интерфейсы вместе с where как-то вот так:
В таком случае мы могли бы получили прямо zero cost duct typing(и щепотку метапрограмирования, если копнуть глубже), поскольку, мы легко можем заменить partial class на partial struct в реализации нашего duck интерфейса. В результате, было бы сгенерено множестао Do методов для каждого уникального TCalcualtor как это происходит со структурами. Но увы, компилятор нам скажет, что ничего такого он не умеет.
На этом все. Спасибо за внимание!
Утиная типизация ‘Duck Typing’ в Python
Понимание утиной типизации в Python.
Утиная типизация заключается в том, что вместо проверки типа чего-либо в Python мы склонны проверять, какое поведение оно поддерживает, зачастую пытаясь использовать это поведение и перехватывая исключение, если оно не работает.
Например, мы можем проверить, является ли что-то целым, пытаясь преобразовать его в целое число:
Python программисты говорят «если это похоже на утку и крякает как утка, то это утка». Не нужно проверять ДНК утки, чтобы понять утка ли это, нужно просто посмотреть на ее поведение.
Утиная типизация «DuckTyping» настолько глубоко заложена и распространена в Python, что она действительно повсюду, как вода для рыбы: мы даже не думаем об этом. В Python очень часто проще предположить поведение объектов, вместо проверки их типов.
Слова, ориентированные на поведение, важны: нас не волнует, что такое объект, нам важно, что он может сделать.
Содержание:
Последовательности sequence состоят из двух основных поведенческих факторов: они имеют длину, и их можно индексировать от 0 до числа, которое меньшей длины последовательности. Они также могут быть зациклены.
Строки, кортежи и списки являются последовательностями:
Строки и кортежи являются неизменяемыми последовательностями, т.е. их нельзя изменить, а списки являются изменяемыми последовательностями.
Последовательности sequence обычно имеют еще несколько вариантов поведения, которые представлены в разделе «Общие операции с последовательностями»
Iterable : можем ли мы использовать их в циклах?
Callables : это функция?
В Python можно думать о вызываемых объектах как о похожих вещах. Многие из встроенных функций на самом деле являются классами. Но их называют функциями, потому что они могут быть вызваны, что является единственным поведением функций. Поэтому классы в Python также могут быть функциями.
Mapping : это словарь?
Вы можете спросить, что такое словарный объект? Это зависит от того, что вы подразумеваете под этим вопросом.
Функция gzip.open в модуле gzip также возвращает файловые объекты. Эти объекты имеют все методы, которые есть у файлов, за исключением сжатия или распаковки при чтении/записи данных в сжатые файлы.
Файловые объекты являются отличным примером этого:
Подробнее о менеджерах контекста смотрите на странице «Контекстный менеджер with в Python».
Какое поведение поддерживает этот объект? Это крякает, как утка? Это идет как утка?
Другие примеры.
Идея утиной типизации ‘Duck Typing’ в языке программирования Python повсеместна.
Метод объединения строк str.join также работает с любыми повторяемыми строками, а не только со списками строк:
Функция csv.reader работает со всеми объектами, похожими на файлы, но он также работает с любыми повторяемыми объектами, которые будут возвращать строки строк с разделителями. Так что функция csv.reader даже примет список строк:
Что в Python не поддерживает «Утиную типизацию»?
Это обеспечит соответствующую ошибку для объектов, которые объект Thing не знает, как добавить.
Многие функции в Python также требуют строки, которые определяется как «объект, который наследуется от класса str «. Например, метод присоединения строк str.join принимает итерируемые строки, а не итерации объекта любого типа:
Если утиная типизация повсюду, какой смысл знать об этом?
Типы данных: [[Class]], instanceof и утки
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/instanceof.
Время от времени бывает удобно создавать так называемые «полиморфные» функции, то есть такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты.
Для реализации такой возможности нужен способ определить тип переменной.
Оператор typeof
Мы уже знакомы с простейшим способом – оператором typeof.
…Но все объекты, включая массивы и даты для typeof – на одно лицо, они имеют один тип ‘object’ :
Поэтому различить их при помощи typeof нельзя, и в этом его основной недостаток.
Секретное свойство [[Class]]
Метод также можно использовать с примитивами:
При тестировании кода в консоли вы можете обнаружить, что если ввести в командную строку <>.toString.call(. ) – будет ошибка. С другой стороны, вызов alert( <>.toString. ) – работает.
Эта ошибка возникает потому, что фигурные скобки < >в основном потоке кода интерпретируются как блок. Интерпретатор читает <>.toString.call(. ) так:
Фигурные скобки считаются объектом, только если они находятся в контексте выражения. В частности, оборачивание в скобки ( <>.toString. ) тоже сработает нормально.
Поэтому узнать тип таким образом можно только для встроенных объектов.
Метод Array.isArray()
Но этот метод – единственный в своём роде.
Оператор instanceof
Оператор instanceof позволяет проверить, создан ли объект данной функцией, причём работает для любых функций – как встроенных, так и наших.
Заметим, что оператор instanceof – сложнее, чем кажется. Он учитывает наследование, которое мы пока не проходили, но скоро изучим и затем вернёмся к instanceof в главе Проверка класса: «instanceof».
Утиная типизация
Альтернативный подход к типу – «утиная типизация», которая основана на одной известной пословице: «If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck (who cares what it really is)».
В переводе: «Если это выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка (какая разница, что это на самом деле)».
Смысл утиной типизации – в проверке необходимых методов и свойств.
Проверить на дату можно, определив наличие метода getTime :
С виду такая проверка хрупка, её можно «сломать», передав похожий объект с тем же методом.
Но как раз в этом и есть смысл утиной типизации: если объект похож на дату, у него есть методы даты, то будем работать с ним как с датой (какая разница, что это на самом деле).
То есть мы намеренно позволяем передать в код нечто менее конкретное, чем определённый тип, чтобы сделать его более универсальным.
Если говорить словами «классического программирования», то «duck typing» – это проверка реализации объектом требуемого интерфейса. Если реализует – ок, используем его. Если нет – значит это что-то другое.
Пример полиморфной функции
Проверку на массив в этом примере можно заменить на «утиную» – нам ведь нужен только метод forEach :
Итого
Для написания полиморфных (это удобно!) функций нам нужна проверка типов.
У него две особенности:
И, наконец, зачастую достаточно проверить не сам тип, а просто наличие нужных свойств или методов. Это называется «утиная типизация».
Задачи
Полиморфная функция formatDate
Её первый аргумент должен содержать дату в одном из видов:
Для этого вам понадобится определить тип данных аргумента и, при необходимости, преобразовать входные данные в нужный формат.
Для определения примитивного типа строка/число подойдёт оператор typeof.
Примеры его работы:
Протоколы в Python: утиная типизация по-новому
В новых версиях Python аннотации типов получают всё большую поддержку, всё чаще и чаще используются в библиотеках, фреймворках, и проектах на Python. Помимо дополнительной документированности кода, аннотации типов позволяют таким инструментам, как mypy, статически произвести дополнительные проверки корректности программы и выявить возможные ошибки в коде. В этой статье пойдет речь об одной, как мне кажется, интересной теме, касающейся статической проверки типов в Python – протоколах, или как сказано в PEP-544, статической утиной типизации.
Содержание
Утиная типизация
Часто, когда речь заходит о Python, всплывает фраза утиная типизация, или даже что-нибудь вроде:
Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка
Утиная типизация – это концепция, характерная для языков программирования с динамической типизацией, согласно которой конкретный тип или класс объекта не важен, а важны лишь свойства и методы, которыми этот объект обладает. Другими словами, при работе с объектом его тип не проверяется, вместо этого проверяются свойства и методы этого объекта. Такой подход добавляет гибкости коду, позволяет полиморфно работать с объектами, которые никак не связаны друг с другом и могут быть объектами разных классов. Единственное условие, чтобы все эти объекты поддерживали необходимый набор свойств и методов.
Но именно эта гибкость и усложняет раннее обнаружение ошибок типизации. Корректность использования объектов определяется динамически, в момент выполнения программы, и зачастую тестирование – единственный способ отловить подобные ошибки. Статическая проверка типов и корректности программы в данном случае представляет значительную сложность.
Номинальная типизация
Рассмотрим небольшой пример:
Проверка совместимости типов в соответствии с номинальной типизацией и иерархией наследования существует во многих языках программирования. Например, Java, C#, C++ и многие другие языки используют номинальную систему типов.
Структурная типизация
Структурная типизация (structural type system) определяет совместимость типов на основе структуры этих типов, а не на явных декларациях. Подобный механизм может рассматриваться как некоторый аналог утиной типизации, но для статических проверок, в некотором смысле compile time duck typing.
Структурная типизация также довольно широко распространена. Например, интерфейсы в Go – это набор методов, которые определяют некоторую функциональность. Типы, реализующие интерфейсы в Go не обязаны декларировать каким-либо образом, что они реализуют данный интерфейс, достаточно просто реализовать соответствующие методы интерфейса.
Другой пример – это TypeScript, который также использует структурную систему типов:
Python и протоколы
Начиная с версии Python 3.8 (PEP-544), появляется новый механизм протоколов для реализации структурной типизации в Python. Термин протоколы давно существует в мире Python и хорошо знаком всем, кто работает с языком. Можно вспомнить, например, протокол итераторов, протокол дескрипторов, и несколько других.
Новые протоколы в некотором смысле «перегружают» уже устоявшийся термин, добавляя возможность структурно проверять совместимость типов при статических проверках (с помощью, например, mypy). В момент исполнения программы, протоколы в большинстве случаев не имеют какого-то специального значения, являются обычными абстрактными классами ( abc.ABC ), и не предназначены для инстанциирования объектов напрямую.
Рассмотрим следующий пример:
Mypy сообщит нам об ошибке, если переданный в функцию iterate_by объект не будет поддерживать протокол итераций (напомню, у объекта должен быть метод __iter__ возвращающий итератор).
Если мы объявим собственный класс, который будет поддерживать протокол итераций, то mypy сможет точно так же, статически проверить соответствие объектов нашего класса заявленному протоколу.
В стандартной библиотеке (в модуле typing ) определено довольно много протоколов для статических проверок. Полный список и примеры использования встроенных протоколов можно посмотреть в документации mypy.
Пользовательские протоколы
Кроме использования определенных в стандартной библиотеке протоколов, есть возможность определять собственные протоколы. При статической проверке типов mypy сможет подтвердить соответствие конкретных объектов объявленным протоколам, либо укажет на ошибки при несоответствии.
Пример использования
Разберем небольшой пример использования пользовательских протоколов:
Если реализация протокола будет некорректной, то mypy сообщит об ошибке:
В данном примере mypy не только сообщает об ошибке в коде программы, но и подсказывает какой метод протокола не реализован (или реализован неправильно).
Явная имплементация протокола
Помимо неявной имплементации, разобранной в примерах выше, есть возможность явно имплементировать протокол. В таком случае mypy сможет проверить, что все методы и свойства протокола реализованы правильно.
В случае явной имплементации протоколы становятся больше похожи на абстрактные классы ( abc.ABC ), позволяют проверять корректность реализации методов и свойств, а так же использовать реализацию по-умолчанию. Но опять же, явное наследование не является обязательным, соответствие произвольного объекта протоколу mypy сможет проверить при статическом анализе.
Декоратор runtime_checkable
Хотя это и может быть полезно в каких-то случаях, у этого метода есть несколько серьезных ограничений, которые подробно разобраны в PEP-544.
Несколько слов в заключение
Новые протоколы являются продолжением идеи утиной типизации в современном Python, в котором инструменты статической проверки типов заняли довольно важное место. Mypy и другие подобные инструменты теперь имеют возможность использовать структурную типизацию в дополнение к номинальной для проверки корректности кода на Python. Кроме того, если вы используете аннотации и проверки типов в своих проектах, то новые протоколы могут сделать код более гибким, сохраняя при этом безопасность и уверенность, которую дает статическая типизация.
Если вам есть что добавить о достоинствах и недостатках структурной типизации, прошу поделиться своими мыслями в комментариях.
Примечания
Все примеры рассмотренные в статье проверялись в Python 3.9 / mypy 0.812.
Утиная типизация
Опытным программистам концепция утиной типизации наверняка знакома. Для новичков же это словосочетание может звучать довольно странно: какое отношение имеют утки к программированию?
Эта концепция адаптирована из следующего абдуктивного умозаключения:
Если что-то выглядит как утка, плавает как утка и крякает как утка, это наверняка и есть утка.
Пока нам не нужно связывать это выражение с программированием, но уже понятно, что оно поясняет, как можно опознать утку. По сути нам не нужна геномная последовательность интересующего нас животного, чтобы его идентифицировать. Вместо того, чтобы обращаться к внутренним факторам, мы делаем вывод, основываясь на внешнем виде и поведении.
Но шутки в сторону, какое отношение утиная типизация имеет к программированию, особенно к Python — языку, интересующему нас в этой статье?
Динамическая и статическая типизация
Концепция утиной типизация в основном принята в языках программирования, поддерживающих динамическую типизацию, таких как Python и JavaScript. Общей особенностью этих языков является возможность объявления переменных без указаниях их типа. Позднее в коде при желании можно назначать другие типы данных этим переменным. Ниже приведены некоторые примеры на Python:
Однако многие языки, такие как Java или Swift, поддерживают статическую типизацию. При объявлении переменной в них необходимо указывать тип данных для этой переменной. Впоследствии, если мы захотим изменить тип данных, компилятор не позволит это сделать, поскольку это несовместимо с изначальным объявлением. Пример ниже демонстрирует, что произойдёт, когда я попытаюсь сделать то же самое на Swift:
Теоретический пример
В разделе выше мы упоминали, что Python является языком с динамической типизацией, как было показано на самом простом примере, включающем встроенные типы данных. Однако динамическую типизацию можно применять и на пользовательских типах. Давайте рассмотрим теоретический пример ниже:
Принимая во внимание эти наблюдения, стоит понимать основные обозначения утиной типизации. При использовании пользовательских типов для определённых целей, реализация связанных функций важнее, чем точные типы данных. В нашем примере, хоть роботизированная птица и не является настоящей уткой, её реализация функции swim_quack “превращает” её в утку — животное, которое плавает и крякает.
Практические примеры
Итераторы
В Python итерация позволяет перебирать список элементов для выполнения определённых операций. Одним из распространённых способов создания итераций является цикл, имеющий следующий общий формат:
Вызываемые
Сортировка с Len()
Предположим, мы хотим отсортировать список уток, основываясь на длине имени каждой из них. Вот как мы применим утиную типизацию в этом случае:
Заключение
В этой статье мы рассмотрели, что такое утиная типизация, и её связь с динамической типизацией в Python и других языках программирования. На теоретическом примере мы рассмотрели, что означает утиная типизация в Python. Кроме того, я представил три конкретных примера, которые вы можете применять в своих проектах на Python.
Наиболее важный вывод в том, что утиная типизация подчёркивает реализацию связанных выполняемых функций, а конкретные типы данных менее важны.