Duck typing что это
Duck typing или “так ли прост старина foreach?”
Полный ответ на этот вопрос такой: «Для того чтобы конструкция foreach успешно компилировалась необходимо, чтобы у объекта был метод GetEnumerator(), который вернет объект с методом MoveNext() и свойством Current, а если такого метода нет, то тогда будем искать интерфейсы IEnumerable и IEnumerable ».
Причин у такого «утиного» поведения две.
Давайте вспомним старые времена языка C# 1.0, когда язык был простым и понятным, и в нем не было никаких обобщений (generics), LINQ-ов и других замыканий. Но раз не было generic-ов, то «обобщение» и повторное использование было основано на полиморфизме и типе object, что, собственно и делалось в классах коллекций и их итераторов.
В качестве этих самых итераторов выступали пара интерфейсов IEnumerable и IEnumerator, при этом последний в свойстве Current возвращал object. А раз так, то использование интерфейса IEnumerator для перебора элементов строго типизированной коллекции значимых типов приводило бы к упаковке и распаковке этого значения на каждой итерации, что, согласитесь, может быть весьма накладно, когда речь идет о столь распространенной операции как перебор элементов.
Чтобы решить эту проблему и было принято решение использовать хак с утиной типизацией, и забить немного на принципы ООП в угоду производительности. В таком случае, класс мог реализовать интерфейс IEnumerable явно и предоставить дополнительный метод GetEnumerator(), который бы возвращал строготипизированный енумератор, свойство Current которого возвращало конкретный тип, например, DateTime без какой либо упаковки.
Ок. Мы разобрались с «динозаврами», а как насчет реального мира? Ведь на дворе, все же не каменный век, СОМ-ы уже дали дуба, Дон Бокс уже не пишет книг, и в нашу с вами дверь уже во всю ломятся гики, навязывая нам всякие функциональные вкусности. Есть ли какие-то выгоды от подобного поведения сейчас?
ПРИМЕЧАНИЕ
Да-да. Я знаю, что я уже все уши прожужжал изменяемыми енумераторами вообще и изменяемыми значимыми типами в частности, но здесь-то мы с вами помимо всего прочего попробуем найти объяснение причин такого поведения. Так что потерпите еще немного:)
Все дело в том, что использование структуры в качестве итератора совместно с «утиной» природой цикла foreach предотвращает от выделения памяти в куче при использовании этой конструкции:
В первом примере, за счет «утиной» природы вызывается метод GetEnumerator() класса List, возвращающий объект значимого типа, который будет спокойно себе жить в стеке без каких либо дополнительных выделений памяти в управляемой куче. Во втором же случае, мы приводим переменную list к интерфейсу, что приведет к вызову метода интерфейса и, соответственно, упаковке итератора. Да, разработчики языка C# положили на полиморфизм и ряд других принципов ООП только ради повышения эффективности.
var list = new List <1, 2, 3>;
Именно по этой причине первый цикл while выведет ожидаемые 1, 2, 3, а второй цикл while … ну, проверьте сами.
Подобное решение (использование изменяемой структуры) кажется сверх микро оптимизацией, но не стоит забывать, что циклы foreach могут быть вложенными, и что не все работают на многоядерных процессорах с гигабайтами памяти. Прежде чем принять такое решение, команда BCL провела серьезные исследования, которые показали, что использование структур действительно того стоит.
ПРИМЕЧАНИЕ
Только не стоит сразу же пользоваться этим примером при реализации собственных итераторов или других вспомогательных классов. Использование структур – это оптимизация сама по себе, использование же изменяемых структур – это серьезнейшее решение, так что вы должны очень четко отдавать себе отчет в том, какие выгоды вы получаете, что готовы настолько пожертвовать безопасностью.
Небольшое дополнение: а для чего нужен вызов Dispose?
Еще одной особенностью реализации цикла foreach является то, что он вызывает метод Dispose итератора. Ниже представлена упрощенная версия кода, генерируемая компилятором при переборе переменной list в цикле foreach:
Может возникнуть резонный вопрос о том, откуда у итератора могут возникнуть управляемые ресурсы? Ну, да, при переборе коллекции в памяти и правда им взяться не откуда, но не стоит забывать, что енумераторы в языке C# могут использовать не только как итераторы для коллекций в памяти; нам никто не мешает сделать итератор, возвращающий построчно содержимое файла:
ПРИМЕЧАНИЕ
Подробнее об итераторах в языке C# можно почитать в заметке … Итераторы в языке C#.
З.Ы. А кто сразу сможет ответить на такой вопрос: а зачем мне нужно два метода ReadByLine и ReadByLineImpl, почему бы мне не воспользоваться лишь одним методом?
З.Ы.Ы. Кстати, блок foreachэто далеко не единственный пример утиной типизации в языке C#, а сколько еще примеров вы можете вспомнить?
Протоколы в 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.
Утипизация в C#
Многогранный Шерлок Холмс и Эраст Фандорин, идеальный аристократ, очень чтили дедуктивный метод и оба достигли в его применении потрясающих успехов. «Отбросьте все невозможное, то, что останется, и будет ответом, каким бы невероятным он ни казался» — так говорил сэр Артур Конан Дойль устами своего героя.
Однако же, на дедуктивных умозаключениях наука о рассуждениях не оканчивается — не стоит забывать еще и о индукции. О ней и о приближенных материях и будет трактовать сей трактат.
Тест на утку
Индукция — это процесс логических умозаключений, основывающийся на переходе от частных положений к общему. Известный «тест на утку» является ярким примером подобного процесса:
If it walks like a duck and quacks like a duck, it must be a duck
(Если что-то ходит, как утка, и крякает, как утка, то это утка)
Применительно к языкам программирования — в частности, к объектно-ориентированным — «тест на утку» приобретает особый смысл. В динамически типизированных языках семантика использования того или иной объекта определяется текущим набором его методов и свойств. К примеру, в статически типизированном языке, не поддерживающем утипизацию, можно создать функцию, принимающую в качестве параметра объект класса Duck и вызывающую методы Quack и Walk этого объекта. В утипизированном же (и, как следствие, в динамически типизированном) языке аналогичная функция может принимать в качестве параметра некий абстрактный объект и попытаться вызывать у него те же методы (возможно, проверив их наличие). В статически типизированном языке подобного поведения можно достигнуть с использованием наследования и реализации интерфейсов, но это будет выражением формы is-a, в отличие от has-a в динамических языках.
Вкратце объяснив, что же такое утипизация и почему она недоступна в статически типизированных языках, я сейчас слегка опровергну сам себя. Для кого-то, возможно, это не будет откровением, но тем не менее. Некое подобие утиной типизации есть и в C#. Вот об этом сейчас поподробней.
foreach
Если открыть C# Language Specification и заглянуть в раздел 8.8.4, то можно увидеть следующее:
Таким образом, следующий код прекрасно скомпилируется и выведет на консоль то, что ему положено:
using System;
using System.Collections. Generic ;
namespace DuckTyping
<
class Collection
<
public Enumerator GetEnumerator()
<
return new Enumerator();
>
>
class Enumerator
<
private bool moveNext = true ;
public bool MoveNext()
<
if (!moveNext)
return false ;
moveNext = false ;
return true ;
>
>
Collection Initializers
Довольно приятное нововведение C# 3.0. Напомню:
Все бы ничего, но осталось определить термин «Collection». И вот с этим возникли проблемы.
Пункт «наличие хотя бы одного. » требует небольшого пояснения. Заключенный в фигурные скобки список элементов не является «списком элементов, которые надо добавить» — это «список аргументов для методов Add() ». Список аргументов, таким образом, может быть гетерогенным. Другими словами, имея такой класс:
вполне легальным будет следующий инициализатор:
Поскольку разрешение перегрузок выполняется отдельно для каждого элемента из списка инициализации выполняется отдельно, то будут задействованы все три метода.
Вместо заключения
Шерлок Холмс, Эраст Петрович и ваш покорный слуга раскланиваются. Надеюсь, было интересно.
«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 принимает итерируемые строки, а не итерации объекта любого типа: