PIG DATA

Принципы типизации в языке Пайтон

Принципы типизации в языке Пайтон
Типы и Пайтон - сложная тема с философской точки зрения. Пайтон не является строго типизированным, но преобладающая парадигма программирования основана на классах и является строго типизированной. Прежде, чем мы сможем понять подход Python, нам нужно узнать, что такое тип и почему он связан с классом.

Все есть объект.


Расширенные атрибуты

Некоторые утверждают, что Python строго типизирован, потому что каждый объект в Python имеет тип, который представляет собой класс или метакласс, как записано в __class__. Однако, это очень слабая форма типизации, поскольку ее, как и многое другое в Python, можно легко изменить. Все, что вам нужно сделать, это присвоить другой тип __class__.

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

В этой главе мы рассмотрим связь между классом и типом и то, как Python использует эту связь.




Три типа типов

Еще до начала работы важно понять, что слово "тип" в программировании имеет несколько значений.

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

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

Существует также третье значение, которое соответствует алгебраическому типу данных, и это гораздо более глубокая, почти философская идея, по Говарду Карри, которая на самом деле не имеет существенного отношения к Python или языкам, основанным на классах, которые большинство из нас знает.

Подобные идеи типизации данных являются частью чистого функционального программирования в таких языках, как Haskell, и о них можно много говорить, но это не совсем мейнстрим в смысле таких языков, как Python, jаvascript, Java, C++ и т.д.

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




Примитивные типы данных

В Python все является объектом, но даже в Python мы не можем игнорировать тот факт, что некоторые типы данных являются более примитивными. В ранних компьютерных языках все, с чем нам приходилось работать, были примитивные данные.

Вы могли написать:

a=3
b=3.14159
и:
c="Hello World"

и эти значения 3, 3.14159 и "Hello World" были бы преобразованы в некоторое внутреннее представление и сохранены в соответствующих переменных.

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

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

a=3
число 3 будет храниться как двоичное целое число, строка из нулевых битов, заканчивающаяся на 011. Число:

b=3.14159
будет храниться в совершенно другом формате - обычно с плавающей точкой. Разница между этими двумя форматами в том, что плавающая точка может показать, где находится десятичная точка, а целое число - нет.

Они оба являются числами, но для компьютера это разные типы чисел.

Более тонким и запутанным для новичков является следующее:

c="3"

выглядит, как число, но представляется, как символ. Проще говоря, битовая схема, представляющая целое число 3 и символ "3", совершенно разная.

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

Их философию можно резюмировать следующим образом:

Если это выглядит как число, то и ведет себя как число, а целое число - это просто число с плавающей точкой, не имеющее дробной части.

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

Конечно, эти различия все еще существуют, но компилятор должен справиться с ними и оградить нас от них.

Python этого не делает, но он все равно немного отличается от других языков. В нем есть три типа чисел: целые числа, числа с плавающей запятой и комплексные числа. Он также различает общие целые числа и булевые, которые ограничены 0 или 1.

Целые числа Python отличаются тем, что работают с произвольным количеством цифр. Вы можете использовать:

a=123456789012345678901234567890
print(a*2)

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

Конечно, на практике существует предел, и вы получите переполнение, но это зависит от аппаратного обеспечения и системы Python.

Аналогично, числа с плавающей точкой в Python реализуются так, чтобы соответствовать аппаратному обеспечению или операционной системе. Как правило, это числа двойной точности с плавающей точкой стандарта IEEE.

Стоит отметить, что Python не делает различий между примитивными типами данных и другими типами. Помните, что в Python все является объектом, и все находится в равных условиях.

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

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

Например, вы можете выполнять арифметические действия над числами, но не над символами, которые просто похожи на цифры.

Проще говоря, тип определяет операции, которые вы можете выполнять.

Объявляя тип объекта, вы точно указываете, какие операции можно использовать и какие методы можно вызывать.




Тип на основе класса

Теперь пришло время перейти к более широкому понятию типа, которое встречается во всех языках со строгой типизацией, основанной на классах. Для сравнения с Python ниже приведены примеры на языке C++ или Java

В большинстве языков, основанных на классах, классы считаются типами, а не объектами.

В языке, основанном на классах, объявление класса также создает новый тип данных.

То есть:

Class MyClassA(){
   //множество свойств
}
не только создает новый класс, но и добавляет новый тип данных MyClassA в систему типов.

В этой системе типов объекты имеют определенный тип, и переменные должны быть объявлены, как определенный тип, а переменная может только ссылаться на объекты этого типа.

Теперь, когда вы объявляете переменную типа:

MyClassA myVariable;

система знает, на что ссылается myVariable.

Это позволяет системе проверить, что когда вы пишете:

myVariable.myProperty

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

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

myVariable.myProperty

является действительным.

Однако, как правило, вы можете сделать вывод о его достоверности, прочитав остальную часть программы. Сильная типизация облегчает этот аспект статического анализа, но за это приходится платить.

Обратите внимание, что тип встречается в двух вариантах - переменная имеет объявленный тип, а объект имеет определенный тип.

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

То есть, экземпляр класса имеет тип, и только переменная того же типа может ссылаться на него.




Иерархическая типизация

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

Например:

Класс MyClassB:наследует MyClassA{

   //множество свойств

}

Теперь MyClassB имеет все свойства MyClassA плюс то, что было добавлено, как часть его определения.

Считается, что MyClassB является подклассом MyClassA, и, поскольку он обладает всеми свойствами MyClassA, вы можете использовать его везде, где вы могли бы использовать экземпляр MyClassA.

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


Поэтому в большинстве сильно типизированных языков, основанных на классах, совершенно нормально писать такие вещи, как:

MyClassA myObject=new MyClassB();
и затем продолжать использовать любые свойства, принадлежащие MyClassA.

Итак, правило стало таким - переменная может ссылаться на объект своего объявленного типа или любого подтипа.

Если вы допустите ошибку и попытаетесь использовать свойство, которого нет у MyClassA, то компилятор сообщит вам об ошибке во время компиляции, и вы будете избавлены от ошибки во время выполнения.



Почему иерархическая типизация полезна?

Она полезна, потому что позволяет писать частично общие методы.

Например, предположим, у вас есть иерархический класс Animal и два подкласса Cat и Dog. Пока вы хотите использовать только методы и свойства класса Animal, вы можете использовать иерархическую типизацию, чтобы написать метод, который может принимать Animal в качестве параметра и использовать его с объектами типа Animal, Cat или Dog.

Когда вы пишете метод, работающий с типом, он также работает со всеми его подтипами.

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




Наследование, как модель

Зачем мы вообще используем наследование и почему оно связано с идеей подтипов?

Это сложный вопрос, который вызывает множество споров. Мы уже рассматривали эти идеи в главах 11 и 12, но они также связаны с понятием типа.


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

Идея заключается в том, что в реальном мире объекты связаны друг с другом. Проблема в том, что они связаны сложными способами.

Как мы уже говорили, в программировании основная цель наследования - обеспечить повторное использование кода.

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

Наиболее известным воплощением этой идеи является принцип подстановки Лискова. Он гласит, что везде, где вы можете использовать экземпляр класса, вы можете использовать экземпляр любого подкласса. Поскольку вы можете использовать его вместо базового класса, он должен быть подтипом базового класса. Это объясняется тем, что подкласс имеет все методы базового класса.

Это часто верно, но не всегда.

Например, согласно нашим предыдущим рассуждениям, квадрат является подклассом прямоугольника, но вы не можете использовать квадрат везде, где можно использовать прямоугольник. Причина в том, что вы не можете задать разные значения сторон квадрата. Существует ограничение на прямоугольник, чтобы сделать из него квадрат.

Ограничения и специализации портят прекрасную идею о том, что подтипы могут использоваться вместо своих супертипов. Это означает, что принцип подстановки Лискова является скорее теоретическим упрощением, чем отражением мира.


Это также делает строгую типизацию произвольным теоретическим решением, когда речь идет о правилах использования экземпляров классов. Можно найти способы заставить подклассы всегда работать как подтипы. Например, если вы реализуете квадрат, как прямоугольник, у которого все равно указаны две стороны, вы можете сохранить все методы прямоугольника и обеспечить равенство другим способом. Это далеко не естественно.
Существует также проблема, связанная с тем, что в реальном мире объекты связаны с множеством других объектов. Квадрат является частным случаем прямоугольника и представляет собой n-стороннюю равностороннюю фигуру.

Можно попытаться смоделировать это с помощью множественного наследования в Python, но это обычно гораздо сложнее, чем кажется, когда вы только начинаете работать. Именно по этой причине большинство других языков ограничиваются однократным наследованием. Другие языки добавляют к классам идею интерфейсов - по сути, объявления классов без реализации. Это позволяет использовать ограниченную форму множественного наследования, но ничего не делает для повторного использования кода, заставляя программистов возвращаться к повторному использованию кода по принципу "копируй и вставляй".

Проблема в том, что реальный мир часто плохо моделируется единой иерархией наследования, независимо от того, используется ли она с сильной типизацией или без нее.

Это основная причина, по которой вы можете услышать совет типа "предпочитайте композицию наследованию". Идея о том, что один объект содержит другой объект, во многих отношениях является более простой концепцией для работы. Так, например, объект автомобиля может содержать объект рулевого колеса и четыре объекта дорожного колеса, которые, в свою очередь, содержат объекты колес. Однако это не всегда подходит, как квадрат может содержать объект прямоугольника, а существующие языки плохо поддерживают композицию.

Существует два основных понятия типа. Первый примитивный тип относится к способу представления данных и является истоком этого понятия. Второй связан с классом в объектно-ориентированном программировании.

Во многих объектно-ориентированных языках определение класса вводит новый тип в систему типов.

Сильно типизированный язык вводит правило, согласно которому вы должны объявить тип переменной, и с этого момента переменная может ссылаться только на объекты этого типа.

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

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

Принцип подстановки Лискова часто используется в качестве обоснования иерархической типизации, но это лишь приближение к реальному миру.

В Python переменные не имеют связанного с ними типа и могут ссылаться на объекты любого типа.

В Python объекты имеют ограниченное понятие типа, поскольку их атрибут __class__ установлен на класс или метакласс, который их создал. Однако, важно понимать, что __class__ может быть изменен.

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

Другой подход заключается в использовании защитного программирования для проверки наличия любого атрибута или метода, который вы планируете использовать, с помощью функции hasattr. Это обычно называют "утиной типизацией".

192 просмотра
0 комментариев
Последние

Кликните на изображение чтобы обновить код, если он неразборчив
Комментариев пока нет
PIG DATA
Community о Хрюшах, событиях, технологиях и IT. Создан для людей и маленьких Хрюшек.