Тонкости использования языка Python: Часть 2. Типы данных

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

Олег Цилюрик, преподаватель тренингового отделения, Global Logic

Фото автораОлег Иванович Цилюрик, много лет был разработчиком программного обеспечения в крупных центрах разработки: ВНИИ РТ, НПО "Дельта", КБ ПМ. Последние годы работал над проектами в области промышленной автоматики, IP телефонии и коммуникаций. Автор нескольких книг. Преподаватель тренингового отделения международной софтверной компании Global Logic.



22.11.2013

Введение

На данный момент существует множество публикаций по различным аспектам программирования на языке Python (в разделе "Ресурсы" находится перечень статей, опубликованных на портале developerWorks). Но в данном цикле мы будем обсуждать возможности Python, которые не часто упоминаются в стандартных источниках.

В этой статье мы проведём некоторое исследование способов типизации объектов, доступных в Python. Это может быть особенно интересно для программистов, работающих с языками программирования со строгой типизацией, например, PASCAL, C++ или Java, так как подход, применяемый для динамической типизации в Python, радикально отличается от вышеупомянутых языков. Поэтому этот вопрос необходимо подробно рассмотреть для лучшего восприятия отличительных принципов программирования на Python.


Динамическая типизация

В языках со строгой типизацией переменных используется статический способ типизации. Так, в PASCAL, C++, Java и других подобных языках любой объект должен быть предварительно описан с привязкой его имени к одному из типов данных. При этом используемый тип данных может быть как предопределён в языке, так и объявлен разработчиком. Статический способ типизации может отличаться в том, что типизация будет структурной (как в PASCAL) или именной (как в C++), но любой объект будет относиться к типу, использовавшемуся для его создания, на всём протяжении своего жизненного цикла. Это относится даже к языкам с нестрогой типизацией, таким как ранний FORTRAN, Perl или BASIC, не требующих явного предварительного объявления переменных — тип объекта остаётся неизменным от момента его создания на протяжении всей его жизни.

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

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

Естественно, что при таком подходе информация о типе данных объекта должна храниться и быть доступна программисту динамически. В листинге 1 представлен пример с использованием динамической типизации (полный код примера можно найти в файле tp2.py в архиве python_types.tgz в разделе "Материалы для скачивания").

Листинг 1. Динамическое изменение типа данных в Python
#!/usr/bin/python
# -*- coding: utf-8 -*-

def show():
    msgt = '{}'.format( type( i ) )
    msgi = 'id={:09x}'.format( id( i ) )
    msgv = '{}'.format( str( i ) )
    try:
        msgh = 'hash={:012d}'.format( hash( i ) )
    except TypeError:
        msgh = 'не хэшируемый тип!'
    print( 'i - это : {:23} |{} |{} ==> {}'.format( msgt, msgi, msgh, msgv ) )

i = 1; show()
i = 1.5e-2; show()
i = complex( 3.0, 5.5 ); show()
i = "теперь это UNICODE строка"; show()
i = [ 1, 2, 3 ]; show()
i = [ 3 * x for x in range( 3 ) ]; show()   # списковая сборка
i = [ [ x, x**2 ] for x in range( 3 ) ]; show()
i = ( 1, 2, 3 ); show()
i = { 1, 2, 3 }; show()                     # множество
i = { 1:"one", 2:"two", 3:"three" }; show() # словарь
i = lambda x: "фиктивная функция"; show()
i = compile( 'lambda x: "ещё одна фиктивная функция"', '', 'eval' ); show()
i = iter( '12345' ); show()

class own1:
    def __init__( self, id ):
        self.id = id

i = own1( 987 ); show()
i = own1; show()

class own2:
    def __init__( self, id ):
        self.id = id
    def __hash__( self ):
        return None

i = own2( 987 ); show()
i = own2; show()

Здесь одна и та же переменная i проходит через ряд операций присваивания, на каждом из которых она меняет свой тип и значение. Ниже приведен фрагмент вывода с результатами запуска данного примера для версий Python 2 и 3 с соответствующими отличиями:

$ python ./tp2.py
i - это : <type 'int'>     |id=0084e10b0 |hash=000000000001 ==> 1
i - это : <type 'float'>   |id=0084ea304 |hash=-02061781074 ==> 0.015
i - это : <type 'complex'> |id=0b765d9c8 |hash=-00345735165 ==> (3+5.5j)
i - это : <type 'str'>     |id=0b7652360 |hash=000132808738 ==> теперь это ...
i - это : <type 'list'>    |id=0b76c0f6c |не хэшируемый тип! ==> [1, 2, 3]
i - это : <type 'list'>    |id=0b7662cec |не хэшируемый тип! ==> [0, 3, 6]
i - это : <type 'list'>    |id=0b76c0f6c |не хэшируемый тип! ==> [[0, 0], [1, 1] ...
i - это : <type 'tuple'> |id=0b7661784 |hash=-00378539185 ==> (1, 2, 3)
i - это : <type 'set'>   |id=0b76acf7c |не хэшируемый тип! ==> set([1, 2, 3])
i - это : <type 'dict'>  |id=0b766379c |не хэшируемый тип! ==> {1: 'one', 2: 'two' ...
i - это : <type 'function'> |id=0b765b10c |hash=-00881435888 ==> <function <lambda> ...
i - это : <type 'code'>  |id=0b76578d8 |hash=-02089338066 ==> <code object <module> ...
i - это : <type 'iterator'> |id=0b766e52c |hash=-00881430958 ==> <iterator object ...
i - это : <type 'instance'> |id=0b766e4ac |hash=-00881430966 ==> <__main__.own1 ...
i - это : <type 'classobj'> |id=0b765626c |hash=-00881437146 ==> __main__.own1
i - это : <type 'instance'> |id=0b766e54c |не хэшируемый тип! ==> <__main__.own2 ...
i - это : <type 'classobj'> |id=0b765620c |hash=-00881437152 ==> __main__.own2
 $ python3 ./tp2.py
i - это : <class 'int'>    |id=0475b1580 | hash=000000000001 ==> 1
i - это : <class 'float'>  |id=00902370c | hash=001578400478 ==> 0.015
i - это : <class 'complex'> |id=0b73dd1e8 | hash=-01068741806 ==> (3+5.5j)
i - это : <class 'str'>   |id=0b743df60 | hash=001282498349 ==> теперь это ...
i - это : <class 'list'>  |id=0b7475c6c | не хэшируемый тип! ==> [1, 2, 3]
i - это : <class 'list'>  |id=0b747574c | не хэшируемый тип! ==> [0, 3, 6]
i - это : <class 'list'>  |id=0b7475c6c | не хэшируемый тип! ==> [[0, 0], [1, 1] ...
i - это : <class 'tuple'> |id=0b7474194 | hash=-00378539185 ==> (1, 2, 3)
i - это : <class 'set'>   |id=0b746ab1c | не хэшируемый тип! ==> {1, 2, 3}
i - это : <class 'dict'>  |id=0b7505bdc |не хэшируемый тип! ==> {1: 'one', 2: 'two' ...
i - это : <class 'function'> |id=0b742c26c |hash=-00881578970 ==> <function <lambda> ...
i - это : <class 'code'>  |id=0b742eb10 |hash=-00858576570 ==> <code object <module> ...
i - это : <class 'str_iterator'> |id=0b743530c |hash=-00881576656 ==> <str_iterator ...
i - это : <class '__main__.own1'> |id=0b743538c |hash=-00881576648 ==> <__main__.own1 ...
i - это : <class 'type'> | id=009118fdc | hash=-01064232707 ==> <class '__main__.own1'>
i - это : <class '__main__.own2'> |id=0b74353c |не хэшируемый тип! ==> <__main__.own2 ...
i - это : <class 'type'> | id=009116c14 | hash=001083250369 ==> <class '__main__.own2'>

К значениям id и hash мы вернёмся позже, но уже сейчас видно, как изменяется тип переменной i и соответствующее этому типу значение (последняя колонка, после разделителя '==>'). Переменная i, начав с числовых значений, последовательно становится строкой, списком, множеством, функцией, классом и объектом этого класса... - то есть фактически всем чем угодно! Эта особенность Python принципиально отличает его от классических языков программирования.


Изменяемые и неизменяемые данные

Все типы данных в Python относятся к одной из 2-х категорий: изменяемые (mutable) и неизменяемые (unmutable).

Во всех русскоязычных переводах используется терминология «изменяемый-неизменяемый». Это не самый удачный вариант, так как он вносит неоднозначность, ассоциируясь с некоей константностью. Термины "мутирующий-немутирующий" были бы уместнее и точнее отображали бы суть происходящего: может ли объект этого типа изменять свою структурность? Например: строка s = 'abcdef' - это неизменяемый тип, так как в Python нельзя, в отличие от C/C++ изменить некоторый одиночный символ в строке, например, через s[ 2 ] = 'z', не говоря уже о том, чтобы вставить символ внутрь строки. Но можно сделать s = s[ :2 ] + 'z' = s[ 3: ] и получить в результате, требуемую строку 'abzdef', только это будет совершенно другая строка, размещённая по совершенно другому адресу в памяти, а s — переустановленная ссылка на эту новую строку. Но изменить строку или её длину (её структурность) по текущей ссылке — невозможно. В этом и состоит неизменяемость объекта — это не константность, так как его значение можно изменить, но это будет уже ссылка на другой объект с этим новым значением.

Многие из предопределённых типов данных Python — это типы неизменяемых объектов: числовые данные (int, float, complex), символьные строки (class 'str'), кортежи (tuple). Другие типы определены как изменяемые: списки (list), множества (set), словари (dict). Вновь определяемые пользователем типы (классы) могут быть определены как неизменяемые или изменяемые. Изменяемость объектов определённого типа является принципиально важной характеристикой, определяющей, может ли объект такого типа выступать в качестве ключа для словарей (dict) или нет, как будет показано в следующем разделе.


Ключи в словарях

Словари — один из встроенных сложно структурированных типов (коллекций) Python (на ряду с списками, кортежами, множествами). Но словари в Python имеют особое, исключительное значение, так как сам интерпретатор Python весь основывается на словарях, а текущее пространство имён в точке исполнения — это словарь, ключами которого являются имена объектов.

Ключами элементов словаря могут быть численные значения, строки, кортежи (tuple) и объекты собственных классов или даже функции, как показано ниже:

def pow2( i ):
    return i * i

d = { 1:"111", 'two':"222", (3, 5, 7):"333", pow2:"444" }; print( d )

Эти типы данных объединяет то, что все они являются неизменяемыми (unmutable). Изменяемые типы данных не могут быть ключами словаря, и их использование в таком качестве вызовет исключение TypeErrorпериода выполнения с сообщением "unhashable type" (нехэшируемый тип) (исключение возникнет прямо во время исполнения, так как интерпретатор не может определить хэшируемость типа ключа до его фактического выполнения). Как становится понятно, неизменяемые типы данных Python — это типы данных для которых не определено значение, возвращаемое функцией hash(). Теперь можно снова вернуться к результатам запуска листинга 1, где видно для каких типов данных возвращается значение вызова hash(), а для каких такой вызов возбуждает исключение TypeError.

Ещё одно значение, показанное в таблице, это значение, возвращаемое вызовом id() - уникальный для каждого объекта идентификатор, из числа специальных атрибутов объектов. Для 32-битной реализации на платформе x86 id() представляется логическим адресом размещения объекта в оперативной памяти. Но это совершенно необязательно для других платформ. Важное требование состоит в том, чтобы для любых 2-х объектов их id() различались, так как этот атрибут уникально идентифицирует объект.

Словарь, как можно понять из документации, организован как хэш-таблица на 2**N входов, где N — такое минимальное число, что 2**(N-1) превышает текущее число элементов словаря (с ростом словаря значение N может увеличиваться). Неизученным остался вопрос, как создать свой собственный класс, объекты которого могут (или не могут) быть использованы в качестве ключей при построении словарей. В листинге 2 представлен сценарий tp-hash.py, в котором демонстрируется работа с хэшируемыми и нехэшируемыми типами данных.

Листинг 2. Хэшируемые и нехэшируемые типы данных
#!/usr/bin/python
# -*- coding: utf-8 -*-

def show_instances( v ):
    print( "объект: {0} (число таких объектов {1})".format( v, v.numInstances ) )
    try:
        print( "{0}, class: {1}, dictionary: {2}".format( type( v ),
               v.__class__, v.__dict__ ) )
        print( "id=0x{0:012x}, hash={1:012}[0x{0:012x}]".format( id( v ),
               hash( v ), hash( v ) ) )
    except TypeError:
        print( "Не хэшируемый объект! : {0}".format( type( v ) ) )

class key0( object ):
    numInstances = 0
    def __init__( self, id ):
        self.id = id
        key0.numInstances = key0.numInstances + 1

class key1( key0 ):
    numInstances = 0
    def __init__( self, id ):
        key0.__init__( self, id )
        key1.numInstances = key1.numInstances + 1
    def __hash__( self ):
        return self.id * self.id

class key2( key0 ):
    numInstances = 0
    def __init__( self, id ):
        key0.__init__( self, id )
        key2.numInstances = key2.numInstances + 1
    def __hash__( self ):
        return 123

class key3( key0 ):
    numInstances = 0
    def __init__( self, id ):
        key0.__init__( self, id )
        key3.numInstances = key3.numInstances + 1
    def __hash__( self ):
        raise TypeError

class key4( key0 ):
    numInstances = 0
    def __init__( self, id ):
        key0.__init__( self, id )
        key4.numInstances = key4.numInstances + 1
    def __hash__( self ):
        return None

tk = ( key0, key1, key2, key3, key4 )       # кортеж выбираемых классов
for clas in tk:                             # выбор класса!
    print( "--------------------------------" )
    t = [ clas( x ) for x in range( 1, 4 ) ] # список объектов выбранного класса
    show_instances( t[ 2 ] )
    try:
        d = { t[ 0 ]:"#1", t[ 1 ]:"#2", t[ 2 ]:"#3" }; # словарь с ключами из классов
        print( d )
        print( d[ t[ 1 ] ] )
    except TypeError:
        print( "Не может использоваться как ключ словаря!" )
    print( "---------------------------------------------------------" )

Мы не будем показывать результирующий вывод этой программы, так как он получится очень объёмным. Но можно сделать краткие выводы (которые, кстати, совпадают для Python 2 и Python 3):

  • любой класс, определённый программистом, может быть хэшируемым, и его объекты в таком случае могут использоваться в качестве ключей для словарей;
  • если в классе (class key0) не был определен собственный метод __hash__(), то будет использоваться его реализация из базового класса object, который возвращает в этом случае id();
  • вы можете произвольно переопределить результат вызова hash() для объектов собственного класса (class key1), реализовав в классе собственный метод __hash__();
  • хэш-функция словаря строится на базе возвратов hash(), но не тождественна им: если класс (class key2) даже возвращает константное значение hash() для любых созданных объектов этого класса, то и в этом случае на нём строится нормально функционирующий словарь;
  • для того, чтобы класс был идентифицирован как изменяемый (class key4) достаточно чтобы его собственная реализация __hash__() возвращала None - это будет "сигналом" для интерпретатора, что такой класс нельзя использовать в качестве ключа для словарей.

В показанном примере есть ещё несколько любопытных деталей из области динамической типизации, которые следует упомянуть:

  • tk (в примере) — это последовательность классов (в данном случае кортеж, но это может быть и список, и множество, и другие контейнерные типы) - не объектов этих классов, а именно классов как определений типов;
  • присвоение в цикле переменной clas поочерёдно различных значений класса из этой последовательности, с последующим созданием необходимых объектов (в нужном количестве) класса clas (имя переменной);

Заключение

В этой статье были рассмотрены некоторые неочевидные вопросы представления типов данных в Python, необычные для программиста, использующего "классические" языки программирования. К этим деталям мы будем неоднократно возвращаться в ходе дальнейшего рассмотрения, но уже при поверхностном рассмотрении становится видной особая гибкость подобного подхода, в чём-то напоминающая возможности языка Lisp. Из этой же гибкости проистекает высокая скорость разработки программного обеспечения на подобных языках. Но эта же гибкость может стать причиной тяжёлых ошибок вследствие неправильно понятой семантики языка.


Загрузка

ОписаниеИмяРазмер
приёмы функционального программированияpython_functional.tgz25KB

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


  • Bluemix

    Узнайте больше информации о платформе IBM Bluemix, создавайте приложения, используя готовые решения!

  • developerWorks Premium

    Эксклюзивные инструменты для построения вашего приложения. Узнать больше.

  • Библиотека документов

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=954098
ArticleTitle=Тонкости использования языка Python: Часть 2. Типы данных
publish-date=11222013