Объекты и атрибуты
Часто можно услышать, что в Python всё является объектом, и это правда. Но что такое объект? И из чего складывается это “всё”?
Когда переменных в коде становится много, их объединяют в группы — объекты. Добраться до переменной, прикрепленной к объекту можно через точку . . Выглядит это похоже на вызов метода. Для сравнения, вызовем метод __len__ и вычислим длину строки:
print('строка это тоже объект'.__len__()) # выведет 22
А теперь узнаем значение прикрепленной переменной __class__ . Она хранит тип данных и имеется у любого объекта в Python, в том числе у строки:
print('строка это тоже объект'.__class__) # выведет
В этот раз мы заменили __len__ на __class__ и не поставили круглые скобки () , на этом отличия заканчиваются. Прикрепленные переменные и прикрепленные функции — методы — имеют так много общего, что вместе их называют атрибутами.
Объект — это набор атрибутов: прикрепленных переменных и функций. До любого атрибута можно добраться через точку . :
print('строка это тоже объект'.__len__) # выведет print('строка это тоже объект'.__class__) # выведет print('строка это тоже объект'.__doc__) # выведет документацию по типу данных Строка
Объекты в Python встречаются повсюду и у всех них есть атрибуты. Строки, числа, функции и даже модули — все это объекты, а значит у них есть атрибут __class__ :
print(''.__class__) # выведет number = 1 print(number.__class__) # выведет print(print.__class__) # выведет import os # подключаем стандартный модуль os print(os.__class__) # выведет
Чтобы не писать каждый раз длинное название __class__ используют функцию type() . Она умеет работать с любыми данными, благодаря тому что сама обращается к атрибуту __class__ :
print(type('')) # выведет print(type(1)) # выведет print(type(print)) # выведет import os # подключаем стандартный модуль os print(type(os)) # выведет
Попробуйте бесплатные уроки по Python
Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.
Переходите на страницу учебных модулей «Девмана» и выбирайте тему.
Python/Объектно-ориентированное программирование на Python
Согласно Алану Кэю — автору языка программирования Smalltalk — объектно-ориентированным может называться язык, построенный с учетом следующих принципов [1] :
- Все данные представляются объектами
- Программа является набором взаимодействующих объектов, посылающих друг другу сообщения
- Каждый объект имеет собственную часть памяти и может иметь в составе другие объекты
- Каждый объект имеет тип
- Объекты одного типа могут принимать одни и те же сообщения (и выполнять одни и те же действия)
Объекты, типы и классы [ править ]
Определение класса [ править ]
Для определения класса используется оператор class :
class имя_класса(надкласс1, надкласс2, . ): # определения атрибутов и методов класса
У класса могут быть базовые (родительские) классы (надклассы), которые, если они есть, указываются в скобках после имени определяемого класса.
Минимально возможное определение класса выглядит так:
class A: pass
В терминологии Python члены класса называются атрибутами, функции класса — методами, а поля класса — свойствами (или просто атрибутами).
Определения методов аналогичны определениям функций, но (за некоторыми исключениями, о которых ниже) методы всегда имеют первый аргумент, называемый по общепринятому соглашению self :
class A: def m1(self, x): # блок кода метода
Определения атрибутов — это обычные операторы присваивания, которые связывают некоторые значения с именами атрибутов.
class A: attr1 = 2 * 2
В языке Python класс не является чем-то статическим, поэтому добавить атрибуты можно и после определения:
class A: pass def my_method(self, x): return x * x A.m1 = my_method A.attr1 = 2 * 2
Создание экземпляра [ править ]
Для создания объекта — экземпляра класса (то есть, инстанцирования класса), достаточно вызвать класс по имени и задать параметры конструктора:
class Point: def __init__(self, x, y, z): self.coord = (x, y, z) def __repr__(self): return "Point(%s, %s, %s)" % self.coord
>>> p = Point(0.0, 1.0, 0.0) >>> p Point(0.0, 1.0, 0.0)
Переопределив классовый метод __new__ , можно управлять процессом создания экземпляра. Этот метод вызывается до метода __init__ и должен вернуть новый экземпляр либо None (в последнем случае будет вызван __new__ родительского класса). Метод __new__ используется для управления созданием неизменчивых (immutable) объектов, управления созданием объектов в случаях, когда __init__ не вызывается, например, при десериализации (unpickle). Следующий код демонстрирует один из вариантов реализации шаблона Одиночка:
>>> class Singleton(object): obj = None # Атрибут для хранения единственного экземпляра def __new__(cls, *dt, **mp): # класса Singleton. if cls.obj is None: # Если он еще не создан, то cls.obj = object.__new__(cls, *dt, **mp) # вызовем __new__ родительского класса return cls.obj # вернем синглтон . >>> obj = Singleton() >>> obj.attr = 12 >>> new_obj = Singleton() >>> new_obj.attr 12 >>> new_obj is obj # new_obj и obj - это один и тот же объект True
Конструктор, инициализатор, деструктор [ править ]
Специальные методы вызываются при создании экземпляра класса (конструктор), при инициализировании экземпляра класса (инициализатор) и при удалении класса (деструктор). В языке Python реализовано автоматическое управление памятью, поэтому конструктор и деструктор требуются достаточно редко, для ресурсов, требующих явного освобождения.
Следующий класс имеет конструктор, инициализатор и деструктор:
class Line: def __new__(cls): # Конструктор return super(Line, cls).__new__(cls) def __init__(self, p1, p2): # Инициализатор self.line = (p1, p2) def __del__(self): # Деструктор print("Удаляется линия %s - %s" % self.line)
>>> l = Line((0.0, 1.0), (0.0, 2.0)) >>> del l Удаляется линия (0.0, 1.0) - (0.0, 2.0) >>>
В момент вызова деструктора (например, по завершении программы) среда исполнения может быть уже достаточно «истощённой»Шаблон:Что, поэтому в деструкторе следует делать только самое необходимое. Кроме того, не обработанные в деструкторе исключения игнорируются.
Время жизни объекта [ править ]
Обычно время жизни объекта, определённого в программе на Python, не выходит за рамки времени выполнения процесса этой программы.
Для преодоления этого ограничения объект можно сохранить, а после — восстановить. Как правило, при записи объекта производится его сериализация, а при чтении — десериализация.
>>> import shelve >>> s = shelve.open("somefile.db") >>> s['myobject'] = [1, 2, 3, 4, 'свечка'] >>> s.close() >>> import shelve >>> s = shelve.open("somefile.db") >>> print s['myobject'] [1, 2, 3, 4, '\xd1\x81\xd0\xb2\xd0\xb5\xd1\x87\xd0\xba\xd0\xb0']
Инкапсуляция и доступ к свойствам [ править ]
Инкапсуляция является одним из ключевых понятий ООП. Все значения в Python являются объектами, инкапсулирующими код (методы) и данные и предоставляющими пользователям общедоступный интерфейс. Методы и данные объекта доступны через его атрибуты.
Сокрытие информации о внутреннем устройстве объекта выполняется в Python на уровне соглашения между программистами о том, какие атрибуты относятся к общедоступному интерфейсу класса, а какие — к его внутренней реализации. Одиночное подчеркивание в начале имени атрибута говорит о том, что атрибут не предназначен для использования вне методов класса (или вне функций и классов модуля), однако, атрибут все-таки доступен по этому имени. Два подчеркивания в начале имени дают несколько большую защиту: атрибут перестает быть доступен по этому имени. Последнее используется достаточно редко.
Есть существенное отличие между такими атрибутами и личными (private) членами класса в таких языках как C++ или Java: атрибут остается доступным, но под именем вида _ИмяКласса__ИмяАтрибута , а при каждом обращении Python будет модифицировать имя в зависимости от того, через экземпляр какого класса происходит обращение к атрибуту. Таким образом, родительский и дочерний классы могут иметь атрибут с именем, например, «__f», но не будут мешать друг другу.
>>> class parent(object): def __init__(self): self.__f = 2 def get(self): return self.__f . >>> class child(parent): def __init__(self): self.__f = 1 parent.__init__(self) def cget(self): return self.__f . >>> c = child() >>> c.get() 2 >>> c.cget() 1 >>> c.__dict__ '_child__f': 1, '_parent__f': 2> # на самом деле у объекта "с" два разных атрибута
Особым случаем является наличие двух подчеркиваний в начале и в конце имени атрибута. Они используются для специальных свойств и функций класса (например, для перегрузки операции). Такие атрибуты доступны по своему имени, но их использование зарезервировано для специальных атрибутов, изменяющих поведение объекта.
Доступ к атрибуту может быть как прямой:
class A(object): def __init__(self, x): # атрибут получает значение в инициализаторе self.x = x a = A(5) print a.x >>> 5
Так и с использованием свойств с заданными методами для получения, установки и удаления атрибута:
class A(object): def __init__(self, x): self._x = x def getx(self): # метод для получения значения return self._x def setx(self, value): # метод для присваивания нового значения self._x = value def delx(self): # метод для удаления атрибута del self._x x = property(getx, setx, delx, "Свойство x") # определяем x как свойство a = A(5) print a.x # Синтаксис доступа к атрибуту при этом прежний >>> 5
Разумеется, первый способ хорош только если значение атрибута является атомарной операцией по изменению состояния объекта. Если же это не так, то второй способ позволит выполнить все необходимые действия в соответствующих методах.
Существуют два способа централизованно контролировать доступ к атрибутам. Первый основан на перегрузке методов __getattr__() , __setattr__() , __delattr__() , а второй — метода __getattribute__() . Второй метод помогает управлять чтением уже существующих атрибутов.
Эти способы позволяют организовать полностью динамический доступ к атрибутам объекта или, что используется очень часто, имитации несуществующих атрибутов. По такому принципу функционируют, например, все системы RPC для Python, имитируя методы и свойства, реально существующие на удаленном сервере.
Полиморфизм [ править ]
В компилируемых языках программирования полиморфизм достигается за счёт создания виртуальных методов, которые в отличие от невиртуальных можно перегрузить в потомке. В Python все методы являются виртуальными, что является естественным следствием разрешения доступа на этапе исполнения. (Следует отметить, что создание невиртуальных методов в компилируемых языках связано с меньшими накладными расходами на их поддержку и вызов).
>>> class Parent(object): def isParOrPChild(self) : return True def who(self) : return 'parent' >>> class Child(Parent): def who(self): return 'child' >>> x = Parent() >>> x.who(), x.isParOrPChild() ('parent', True) >>> x = Child() >>> x.who(), x.isParOrPChild() ('child', True)
Явно указав имя класса, можно обратиться к методу родителя (как впрочем и любого другого объекта).
>>> class Child(Parent): def __init__(self): Parent.__init__(self)
В общем случае для получения класса-предка применяется функция super .
class Child(Parent): def __init__(self): super(Child, self).__init__()
Используя специально предусмотренное исключение NotImplementedError , можно имитировать чисто виртуальные методы:
>>> class abstobj(object): def abstmeth(self): raise NotImplementedError('Method abstobj.abstmeth is pure virtual') >>> abstobj().abstmeth() Traceback (most recent call last): File "", line 1, in module> File "", line 2, in method NotImplementedError: Method abstobj.abstmeth is pure virtual
Или, с использованием декоратора, так:
>>> def abstract(func): def closure(*dt, **mp): raise NotImplementedError("Method %s is pure virtual" % func.__name__) return closure >>> class abstobj(object): @abstract def abstmeth(self): pass >>> abstobj().abstmeth() Traceback (most recent call last): File "", line 1, in module> File "", line 2, in method NotImplementedError: Method abstobj.abstmeth is pure virtual
Изменяя атрибут __class__ , можно перемещать объект вверх или вниз по иерархии наследования (впрочем, как и к любому другому типу)
>>> c = child() >>> c.val = 10 >>> c.who() 'child' >>> c.__class__ = Parent >>> c.who() 'parent' >>> c.val 10
Однако, в этом случае никакие преобразования типов не делаются, поэтому забота о согласованности данных всецело лежит на программисте. Кроме того, присваивание атрибуту __class__ не должно применяться по поводу и без. Прежде чем решиться на его использование, необходимо рассмотреть менее радикальные варианты реализации изменения объекта, то есть по сути шаблона проектирования State.
Более того, полиморфизм в Python вообще не связан с наследованием, поэтому его можно считать сигнатурно-ориентированным полиморфизмом (signature-oriented polymorphism) [2] . Например, чтобы экземпляру класса «прикинуться» файловым объектом, ему достаточно реализовать методы, относящиеся к файлам (обычно .read() , .readlines() , .close() и т. п.).
Переопределение встроенных типов [ править ]
Встроенные типы и их методы имеют синтаксическую поддержку в языке Python или другие особые «привилегии». Конечно, любая операция может быть представлена синтаксисом вызова функции, однако, для частого применения это неудобно.
Воспользоваться точно такой же синтаксической поддержкой может и любой определённый пользователем класс. Для этого нужно лишь реализовать методы со специальными именами. Самый простой пример — переопределить функцию:
>>> class Add: . def __call__(self, x, y): # переопределение метода, . return x + y # который отвечает за операцию вызова функции . >>> add = Add() >>> add(3, 4) # это эквивалентно add.__call__(3, 4) 7
Аналогично поддаются переопределению все операции встроенных типов. Ещё один пример связан с вычислением длины объекта с помощью функции len() . Эта встроенная функция вызывает специальный метод:
>>> class wrongList(list): # определяем собственный класс для списка . def __len__(self): # который всегда считает, что имеет нулевую длину . return 0 . >>> w = wrongList([1,2,3]) >>> len(w) # это эквивалентно w.__len__() 0
Методы __getitem__,__setitem__,__delitem__,__contains__ позволяют создать интерфейс для словаря или списка( dict ).
Достаточно просто переопределить и числовые типы. Скажем, следующий класс использует инфиксную операцию * :
class Multiplyable: def __init__(self, value): self.value = value def __mul__(self, y): return self.value * y def __rmul__(self, x): return x * self.value def __imul__(self, y): return Multiplyable(self.value * y) def __str__(self): return "Multiplyable(%s)" % self.value >>> m = Multiplyable(1) >>> print m Multiplyable(1) >>> m *= 3 >>> print m Multiplyable(3)
Последний из методов — .__str__() — отвечает за представление экземпляра класса при печати оператором print и в других подобных случаях.
Аналогичные методы имеются и у соответствующих встроенных типов:
>>> int.__add__ slot wrapper '__add__' of 'int' objects> >>> [].__getitem__ built-in method __getitem__ of list object at 0x00DA3D28> >>> class a(object):pass >>> a.__call__ method-wrapper '__call__' of type object at 0x00DDC318>
Не все из них существуют на самом деле: большая часть имитируется интерпретатором Python для удобства программиста. Такое поведение позволяет экономить время при наиболее важных операциях (например, сложение целых не приводит к поиску и вызову метода __add__ у класса int ) и память не расходуется на этот поиск и вызов, но приводит к невозможности изменения методов у встроенных классов.
Отношения между классами [ править ]
Наследование и множественное наследование [ править ]
При описании предметной области классы могут образовывать иерархию, в корне которой стоит базовый класс, а нижележащие классы (подклассы) наследуют свои атрибуты и методы, уточняя и расширяя поведение вышележащего класса (надкласса). Обычно принципом построения классификации является отношение «IS-A» («есть» — между экземпляром и классом) и «AKO» («a kind of» — «разновидность» — между классом и суперклассом) [3] .
Python поддерживает как одиночное наследование, так и множественное, позволяющее классу быть производным от любого количества базовых классов.
>>> class Par1(object): # наследуем один базовый класс - object def name1(self): return 'Par1' >>> class Par2(object): def name2(self): return 'Par2' >>> class Child(Par1, Par2): # создадим класс, наследующий Par1, Par2 (и, опосредованно, object) pass >>> x = Child() >>> x.name1(), x.name2() # экземпляру Child доступны методы из Par1 и Par2 'Par1','Par2'
В Python (из-за «утиной типизации») отсутствие наследования ещё не означает, что объект не может предоставлять тот же самый интерфейс.
Множественное наследование в Python применяется в основном для добавления примесей (mixins) — специальных классов, вносящих некоторую черту поведения или набор свойств [4] .
Порядок разрешения доступа к методам и полям [ править ]
За достаточно простым в использовании механизмом доступа к атрибутам в w:Python кроется довольно сложный алгоритм. Далее будет приведена последовательность действий, производимых интерпретатором при разрешении запроса object.field (поиск прекращается после первого успешно завершённого шага, иначе происходит переход к следующему шагу).
- Если у object есть метод __getattribute__ , то он будет вызван с параметром ‘field’ (либо __setattr__ или __delattr__ в зависимости от действия над атрибутом)
- Если у object есть поле __dict__ , то ищется object.__dict__[‘field’]
- Если у object.__class__ есть поле __slots__ , то ‘field’ ищется в object.__class__.__slots__
- Проверяется object.__class__.__dict__[‘fields’]
- Производится рекурсивный поиск по __dict__ всех родительских классов (при множественном наследовании поиск производится в режиме deep-first, в том порядке как базовые классы перечислены в определении класса-потомка). Алгоритм поиска разный для «классических» и «новых» классов.
- Если у object есть метод __getattr__ , то вызывается он с параметром ‘field’
- Вызывается исключение AttributeError .
Если поиск окончен успешно, то проверяется, является ли атрибут классом «нового стиля». Если является, то проверяется наличие у него метода __get__ (либо __set__ или __delete__ , в зависимости от действия над атрибутом), если метод найден, то происходит следующий вызов object.field.__get__(object) и возвращается его результат (такие атрибуты называется в Python атрибутами со связанным поведением (binded behavior) и используются, например, для создания свойств [5] ).
Эта последовательность распространяется только на пользовательские атрибуты. Системные атрибуты, такие как __dict__ , __len__ , __add__ и другие, имеющие специальные поля в С-структуре описания класса находятся сразу.
«Новые» и «классические» классы [ править ]
В версиях до 2.2 некоторые объектно-ориентированные возможности Python были заметно ограничены. Например, было невозможно наследовать встроенные классы и классы из модулей расширения. Свойства (property) не выделялись явно. Начиная с версии 2.2, объектная система Python была существенно переработана и дополнена. Однако для совместимости со старыми версиями Python было решено сделать две объектные модели: «классические» типы (полностью совместимые со старым кодом) и «новые» [6] . В версии Python3 поддержка «старых» классов будет удалена.
Для построения «нового» класса достаточно унаследовать его от другого «нового». Если нужно создать «чистый» класс, то можно унаследоваться от object — родительского типа для всех «новых» классов.
class OldStyleClass: pass # класс "старого" типа class NewStyleClass(object): pass # и "нового"
Все стандартные классы — классы «нового» типа. [7]
Агрегация. Контейнеры. Итераторы [ править ]
Агрегация, когда один объект входит в состав другого, или отношение «HAS-A» («имеет»), реализуется в Python с помощью ссылок. Python имеет несколько встроенных типов контейнеров: список, словарь, множество. Можно определить собственные классы контейнеров со своей логикой доступа к хранимым объектам. (Следует заметить, что в Python агрегацию можно считать разновидностью ассоциации, так реально объекты не вложены друг в друга в памяти и, более того, время жизни элемента может не зависеть от времени жизни контейнера.)
Следующий класс из модуля utils.py среды web.py является примером контейнера-словаря, дополненного возможностью доступа к значениям при помощи синтаксиса доступа к атрибутам:
class Storage(dict): def __getattr__(self, key): try: return self[key] except KeyError, k: raise AttributeError, k def __setattr__(self, key, value): self[key] = value def __delattr__(self, key): try: del self[key] except KeyError, k: raise AttributeError, k def __repr__(self): return ' + dict.__repr__(self) + '>'
Вот как он работает:
>>> v = Storage(a=5) >>> v.a 5 >>> v['a'] 5 >>> v.a = 12 >>> v['a'] 12 >>> del v.a
Для доступа к контейнерам очень удобно использовать итераторы:
>>> cont = dict(a=1, b=2, c=3) >>> for k in cont: . print k, cont[k] . a 1 c 3 b 2
Ассоциация и слабые ссылки [ править ]
Отношение использования («USE-A») экземпляров одного класса другими является достаточно общим отношением. При использовании один класс обычно зависит от интерфейса другого класса (хотя эта зависимость может быть и взаимной). Если один объект использует другой, он обязательно содержит ссылку на него. Объекты могут ссылаться и друг на друга. В этом случае возникают циклические ссылки. Если ссылающиеся друг на друга объекты удалить, то они уже не могут быть удалены интерпретатором Python с помощью механизма подсчета ссылок. Удалением таких объектов занимается сборщик мусора.
Ассоциацию объектов без присущих ссылкам проблем можно осуществить с помощью слабых ссылок. Слабые ссылки не препятствуют удалению объекта.
Для работы со слабыми ссылками применяется модуль weakref .
Метаклассы [ править ]
Обычных возможностей объектно-ориентированного программирования хватает далеко не всегда. В некоторых случаях требуется изменить сам характер системы классов: расширить язык новыми типами классов, изменить стиль взаимодействия между классами и окружением, добавить некоторые дополнительные аспекты, затрагивающие все используемые в приложении классы, и т. п.
При объявлении метакласса за основу можно взять класс type . Пример:
# описание метакласса class myobject(type): # небольшое вмешательство в момент выделения памяти для класса def __new__(cls, name, bases, dict): print "NEW", cls.__name__, name, bases, dict return type.__new__(cls, name, bases, dict) # небольшое вмешательство в момент инициализации класса def __init__(cls, name, bases, dict): print "INIT", cls.__name__, name, bases, dict return super(myobject, cls).__init__(name, bases, dict) # порождение класса на основе метакласса (заменяет оператор class) MyObject = myobject("MyObject", (), <>) # обычное наследование другого класса из только что порожденного class MySubObject(MyObject): def __init__(self, param): print param # получение экземпляра класса myobj = MySubObject("parameter")
Разумеется, вместо оператора print код метакласса может выполнять более полезные функции: регистрировать класс, передавать действия с классами на удаленную систему, использовать классы для других целей (например, как декларации или ограничения) и т. п.
Методы [ править ]
Метод [ править ]
Синтаксис описания метода ничем не отличается от описания функции, разве что его положением внутри класса и характерным первым формальным параметром self , с помощью которого внутри метода можно ссылаться на сам экземпляр класса (название self является соглашением, которого придерживаются программисты на Python):
class MyClass(object): def mymethod(self, x): return x == self._x
Статический метод [ править ]
Статические методы в Python являются синтаксическими аналогами статических функций в основных языках программирования. Они не получают ни экземпляр ( self ), ни класс ( cls ) первым параметром. Для создания статического метода (только «новые» классы могут иметь статические методы) используется декоратор staticmethod
>>> class D(object): @staticmethod def test(x): return x == 0 . >>> D.test(1) # доступ к статическому методу можно получать и через класс False >>> f = D() >>> f.test(0) # и через экземпляр класса True
Статические методы реализованы с помощью свойств (property).
Метод класса [ править ]
Классовые методы в Python занимают промежуточное положение между статическими и обычными. В то время как обычные методы получают первым параметром экземпляр класса, а статические не получают ничего, в классовые методы передается класс. Возможность создания классовых методов является одним из следствий того, что в Python классы также являются объектами. Для создания классового (только «новые» классы могут иметь классовые методы) метода можно использовать декоратор classmethod
>>> class A(object): def __init__(self, int_val): self.val = int_val + 1 @classmethod def fromString(cls, val): # вместо self принято использовать cls return cls(int(val)) . >>> class B(A):pass . >>> x = A.fromString("1") >>> print x.__class__.__name__ A >>> x = B.fromString("1") >>> print x.__class__.__name__ B
Классовые методы достаточно часто используются для перегрузки конструктора. Классовые методы, как и статические, реализуются через свойства (property).
Мультиметоды [ править ]
Примером для иллюстрации сути мультиметода может служить функция add() из модуля operator :
>>> import operator as op >>> print op.add(2, 2), op.add(2.0, 2), op.add(2, 2.0), op.add(2j, 2) 4 4.0 4.0 (2+2j)
В языке Python достаточно легко реализовать и определённые пользователем мультиметоды [8] . Например, эмулировать мультиметоды можно с помощью модуля multimethods.py (из Gnosis Utils) :
from multimethods import Dispatch class Asteroid(object): pass class Spaceship(object): pass def asteroid_with_spaceship(a1, s1): print "A-> def asteroid_with_asteroid(a1, a2): print "A-> def spaceship_with_spaceship(s1, s2): print "S-> collide = Dispatch() collide.add_rule((Asteroid, Spaceship), asteroid_with_spaceship) collide.add_rule((Asteroid, Asteroid), asteroid_with_asteroid) collide.add_rule((Spaceship, Spaceship), spaceship_with_spaceship) collide.add_rule((Spaceship, Asteroid), lambda x,y: asteroid_with_spaceship(y,x)) a, s1, s2 = Asteroid(), Spaceship(), Spaceship() collision1 = collide(a, s1)[0] collision2 = collide(s1, s2)[0]
Устойчивость объектов [ править ]
Объекты всегда имеют своё представление в памяти компьютера и их время жизни не больше времени работы программы. Однако зачастую необходимо сохранять данные между запусками приложения и/или передавать их на другие компьютеры. Одним из решений этой проблемы является устойчивость объектов (англ. object persistence ) которая достигается с помощью хранения представлений объектов (сериализацией) в виде байтовых последовательностей и их последующего восстановления (десериализация).
Модуль pickle является наиболее простым способом «консервирования» объектов в Python.
Следующий пример показывает, как работает сериализация и десериализация:
# сериализация >>> import pickle >>> p = set([1, 2, 3, 5, 8]) >>> pickle.dumps(p) 'c__builtin__\nset\np0\n((lp1\nI8\naI1\naI2\naI3\naI5\natp2\nRp3\n.' # де-сериализация >>> import pickle >>> p = pickle.loads('c__builtin__\nset\np0\n((lp1\nI8\naI1\naI2\naI3\naI5\natp2\nRp3\n.') >>> print p set([8, 1, 2, 3, 5])
Получаемая при сериализации строка может быть передана по сети, записана в файл или специальное хранилище объектов, а позже — прочитана. Сериализации поддаются не все объекты. Некоторые объекты (например, классы и функции) представляются своими именами, поэтому для десериализации требуется наличие тех же самых классов. Нужно отметить, что нельзя десериализовать данные из непроверенных источников с помощью модуля pickle , так как при этом возможны практически любые действия на локальной системе. При необходимости обмениваться данными по незащищенным каналам или с ненадежными источниками можно воспользоваться другими модулями для сериализации.
В основе сериализации объекта стоит представление его состояния. По умолчанию состояние объекта — это все, что записано в его полях. Пользовательские классы могут управлять сериализацией, предоставляя состояние объекта явным образом (методы __getstate__ , __setstate__ и др.).
На стандартном для Python механизме сериализации построена работа модуля shelve (shelve (англ. глаг.) — ставить на полку; сдавать в архив). Модуль предоставляет функцию open . Объект, который она возвращает, работает аналогично словарю, но объекты сериализуются и сохраняются в файле:
>>> import shelve >>> s = shelve.open("myshelve.bin") >>> s['abc'] = [1, 2, 3] >>> s.close() # . >>> s = shelve.open("myshelve.bin") >>> s['abc'] [1, 2, 3]
Сериализация pickle — не единственная возможная, и подходит не всегда. Для сериализации, не зависящей от языка программирования, можно использовать, например, XML.
Примечания [ править ]
- ↑Introduction to Object-Oriented Programming
- ↑в списке рассылки comp.lang.python
- ↑«AKO» и «IS-A»
- ↑Beazley, 2009
- ↑How-To Guide for Descriptors by R. Hettinger (недоступная ссылка — история) Проверено 2007-10-06 г.Архивировано из первоисточника 6 октября 2007.
- ↑New-style Classes
- ↑Объяснение Гвидо ван Россума об объединении типов и классов
- ↑Charming Python: Multiple dispatch
Литература [ править ]
- David M. Beazley Python Essential Reference. — 4th Edition. — Addison-Wesley Professional, 2009. — 717 с. — ISBN 978-0672329784
Методы класса и статические методы
Когда интерпретатор достигает инструкции class (а не тогда, когда происходит вызов класса), он выполняет все инструкции в ее теле от начала и до конца. Все присваивания, которые производятся в ходе этого процесса, создают имена в локальной области видимости класса, которые становятся атрибутами объекта класса.
Благодаря этому классы напоминают модули и функции:
- Подобно функциям, инструкции class являются локальными областями видимости, где располагаются имена, созданные вложенными операциями присваивания.
- Подобно именам в модуле, имена, созданные внутри инструкции class, становятся атрибутами объекта класса.
Для работы с классом питон создает отдельный объект, описывающий весь класс целиком как набор правил, а не отдельный экземпляр класса. (class object).
У этого объекта тоже могут быть свои поля и методы.
Они нужны для атрибутов и методов, которые относятся не к конкретному экземпляру, а ко всему классу целиком.
Например, для класса, описывающих дату, это может быть список названий месяцев.
Переменные класса
class Date(): month = ['январь', 'февраль', ..] def __init__(self, day, month, year): self.day = day self.month = month self.year = year self.month_name = Date.month[month-1]
Попробуем посчитать, сколько экземпляров класса Circle было создано за время работы программы.
class Circle(): counter = 0 # сколько экземпляров класса было создано def __init__(self, x=0, y=0, r=1): Circle.counter += 1 c = Circle() d = Circle() print(
При чтении переменных идет поиск этой переменной в пространстве имен.
При = изменяется сам объект. (Быть может создается атрибут этого объекта).
class A(object): shared_data = 42 x = A() y = A() print(x.shared_data, y.shared_data, A.shared_data) # 42, 42, 42 A.shared_data = 99 # А - объект - класс print(x.shared_data, y.shared_data, A.shared_data) # 99, 99, 99 # x - объект - экземпляр класса x.shared_data = 100 # создали новый атрибут ЭКЗЕМПЛЯРА класса print(x.shared_data, y.shared_data, A.shared_data) # 100, 99, 99
В классе и экземпляре класса может быть поле с одинаковым именем (не пишите так!):
class B(object): data = 'shared' # присваивание атрибуту класса def __init__(self, data): self.data = data # присваивание атрибуту экземпляра def prn(self): print(self.data, B.data) # атрибут экземпляра, атрибут класса x = B(1) y = B(2) x.prn() # 1 shared y.prn() # 2 shared
Вызов методов
Для любого объекта класса класс допустимы варианты вызова метода экземпляра класса:
экземпляр.метод(аргументы. ) класс.метод(экземпляр, аргументы. )
class A(object): def func(self, value): print(value) x = A() x.func('первый вызов') # первый вызов A.func(x, 'второй вызов') # второй вызов
методы класса и статические методы
@classmethod можно переопределить в наследнике класса
У метода класса есть cls, но нет self
@staticmethod нельзя переопределить при наследовании классов
class Date(object): def __init__(self, day=0, month=0, year=0): self.day = day self.month = month self.year = year @classmethod def from_string(cls, date_as_string): day, month, year = map(int, date_as_string.split('-')) date1 = cls(day, month, year) return date1 @staticmethod def is_date_valid(date_as_string): day, month, year = map(int, date_as_string.split('-')) return day 31 and month 12 and year 3999 date2 = Date.from_string('11-09-2012') is_date = Date.is_date_valid('11-09-2012')
Пример static factory
Допустим, у нас должно быть не более 1 экземпляра данного класса.
Когда какой метод делаем?
- экземпляра класса (self) — функция обращается к атрибутам экземпляра;
- класса — функция не обращается к атрибутам экземпляра класса, но обращается к атрибутам класса;
- static — функция не обращается ни к каким атрибутам класса или объекта.
Атрибуты, словари и слоты в Python
Python по своей природе является очень динамичным языком. Переменные не нужно объявлять, их можно добавлять в качестве атрибутов практически везде. Рассмотрим пустой класс, например, такой:
class MyClass: pass
Это полное определение класса в Python. Конечно, оно ничего не делает, но все же является верным.
В любой более поздний момент времени мы можем «наложить» атрибуты на наш класс следующим образом:
MyClass.class_attribute = 42
Класс имеет это новое class_attribute значение с этого момента.
Если мы инстанцируем этот класс с помощью my_object = MyClass() , мы можем убедиться, что значение class_attribute равно 42:
>>> my_object.class_attribute 42
Конечно, мы также можем добавить атрибуты к нашим экземплярам:
>>> my_object.instance_attribute = 21 >>> my_object.instance_attribute 21
Вы когда-нибудь задумывались, где хранятся эти атрибуты?
Явное лучше неявного. (из Zen of Python)
Python не был бы Python без четко определенного и настраиваемого поведения для атрибутов. Атрибуты «items» в Python хранятся в магическом атрибуте под названием __dict__ . Мы можем получить к нему доступ следующим образом:
class MyClass: class_attribute = "Class" def __init__(self): self.instance_attribute = "Instance" my_object = MyClass() print(my_object.__dict__) print(MyClass.__dict__)
Как видите, class_attribute хранится в __dict__ самого MyClass , тогда как instance_attribute хранится в __dict__ самого my_object .
Это означает, что при каждом обращении к my_object.instance_attribute Python будет искать сначала в my_object.__dict__ , а затем в MyClass.__dict__ . Если атрибут instance_attribute не будет найден ни в одном из словарей, то возникнет ошибка AttributeError .
Побочная заметка
Что такое «item» в Python? Вы видите, что каждая «item» в Python имеет атрибут __dict__ , даже сам класс. Логически, класс типа MyClass имеет тип class , что означает, что сам класс является объектом типа class . Поскольку это может показаться непонятным, я использую разговорный термин «item».
«Взлом» атрибута __dict__
Как всегда в Python, атрибут __dict__ ведет себя как любой другой атрибут в Python. Поскольку Python — язык, предпочитающий передачу по ссылке, мы можем рассмотреть ошибку, которая встречается довольно часто и случайно. Рассмотрим класс AddressBook :
class AddressBook: addresses = []
Теперь давайте создадим несколько адресных книг и создадим несколько адресов:
alices_address_book = AddressBook() alices_address_book.addresses.append(("Sherlock Holmes", "221B Baker St., London")) alices_address_book.addresses.append(("Al Bundy", "9764 Jeopardy Lane, Chicago, Illinois")) bobs_address_book = AddressBook() bobs_address_book.addresses.append(("Bart Simpson", "742 Evergreen Terrace, Springfield, USA")) bobs_address_book.addresses.append(("Hercule Poirot", "Apt. 56B, Whitehaven Mansions, Sandhurst Square, London W1"))
Интересно, что Элис и Боб теперь имеют одну адресную книгу:
>>> alices_address_book.addresses [('Sherlock Holmes', '221B Baker St., London'), ('Al Bundy', '9764 Jeopardy Lane, Chicago, Illinois'), ('Bart Simpson', '742 Evergreen Terrace, Springfield, USA'), ('Hercule Poirot', 'Apt. 56B, Whitehaven Mansions, Sandhurst Square, London W1')] >>> bobs_address_book.addresses [('Sherlock Holmes', '221B Baker St., London'), ('Al Bundy', '9764 Jeopardy Lane, Chicago, Illinois'), ('Bart Simpson', '742 Evergreen Terrace, Springfield, USA'), ('Hercule Poirot', 'Apt. 56B, Whitehaven Mansions, Sandhurst Square, London W1')]
Это происходит потому, что атрибут addresses определен на уровне class . Пустой список создается только один раз ( addresses = [] ), а именно, когда интерпретатор Python создает класс. Таким образом, для любого последующего экземпляра класса AddressBook тот же list будет ссылаться на addresses . Мы можем исправить эту ошибку, перенеся создание пустого list на уровень экземпляра следующим образом:
class AddressBook: def __init__(self): self.addresses = []
Переместив создание пустого списка в конструктор (метод __init__ ), новый список создается каждый раз, когда создается новый экземпляр AddressBook . Таким образом, экземпляры больше не будут непреднамеренно использовать один и тот же list .
Знакомство с Боргом
Можем ли мы как-то специально использовать это поведение? Существует ли сценарий использования, когда мы хотим, чтобы все экземпляры использовали одно и то же хранилище? Оказывается, есть! Существует паттерн проектирования , называемый синглтон . Это гарантирует, что во время выполнения программы существует только один экземпляр класса. Например, это может быть полезно, если используется для класса соединения с базой данных или хранилища конфигурации.
Обратите внимание на то, что вы должны использовать синглтонные классы только иногда, потому что они вводят некоторое глобальное состояние в вашу программу, что затрудняет изолированное тестирование отдельных компонентов вашей программы.
Каким образом в Pythonic можно реализовать паттерн singleton?
Рассмотрим этот класс:
class Borg: _shared = <> def __init__(self): self.__dict__ = self._shared
Этот класс имеет атрибут _shared , инициализированный как пустой массив. Из предыдущих параграфов мы знаем, что экземпляр dict является тем же объектом для класса . Тогда внутри конструктора ( __init__ ) мы устанавливаем __dict__ экземпляра в этот общий словарь. В результате все динамически добавляемые атрибуты становятся общими для каждого экземпляра этого класса.
>>> borg_1 = Borg() >>> borg_2 = Borg() >>> >>> borg_1.value = 42 >>> borg_2.value 42
Почему мы не можем установить __dict__ = <> непосредственно в класс, как например
class Borg: __dict__ = <>
>>> borg_1 = Borg() >>> borg_2 = Borg() >>> >>> borg_1.value = 42 >>> borg_2.value Traceback (most recent call last): File "", line 1, in AttributeError: 'Borg' object has no attribute 'value'
Это связано с тем, что в последнем случае мы задаем атрибут __dict__ самому классу class. Однако мы получаем доступ к атрибуту instance, набирая borg_2.value . Только когда атрибут __dict__ установлен на уровне instance, мы можем использовать наш шаблон Борга. Этого можно добиться, используя конструктор для изменения атрибута __dict__ на уровне экземпляра.
Использование памяти для атрибутов
Динамическое добавление атрибутов во время выполнения на уровне экземпляра или класса сопряжено с определенными затратами. Структура словаря занимает много памяти во внутреннем пространстве Python. В ситуациях, когда вы создаете много (тысячи) экземпляров, это может стать узким местом.
Однако, сначала о главном: Что такое слоты? Хотя в Python вы можете динамически добавлять атрибуты к «вещам», слоты ограничивают эту функциональность. Когда вы добавляете __slots__ атрибут к class , вы предварительно определяете, какие атрибуты-члены вы разрешаете. Давайте посмотрим:
class SlottedClass: __slots__ = ['value'] def __init__(self, i): self.value = i
При таком определении любой экземпляр SlottedClass может обращаться только к атрибуту value . Доступ к другим (динамическим) атрибутам вызовет ошибку AttributeError :
>>> slotted = SlottedClass(42) >>> slotted.value 42 >>> slotted.forbidden_value = 21 AttributeError: 'SlottedClass' object has no attribute 'forbidden_value'
Ограничение возможности динамического добавления атрибутов полезно для уменьшения ошибок во время выполнения, которые могут возникнуть из-за опечаток в именах атрибутов. Однако, что более важно, это ограничение уменьшит использование памяти вашим кодом — в некоторых случаях значительно. Давайте попробуем проверить это.
Мы создаем два класса, один щелевой и один нещелевой. Оба класса обращаются к атрибуту с именем value внутри своего метода __init__ , и в случае класса со слотом это единственный атрибут в __slots__ .
Мы создаем миллион экземпляров для каждого класса и храним эти экземпляры в списке. После этого мы смотрим на размер списка. Список экземпляров класса со щелью должен быть меньше.
import sys class SlottedClass: __slots__ = ['value'] def __init__(self, i): self.value = i class UnSlottedClass: def __init__(self, i): self.value = i slotted = [] for i in range(1_000_000): slotted.append(SlottedClass(i)) print(sys.getsizeof(slotted)) unslotted = [] for i in range(1_000_000): unslotted.append(UnSlottedClass(i)) print(sys.getsizeof(unslotted))
Однако для каждого списка мы получаем обратно значение 8448728 . Как же нам сэкономить память, используя слоты?
Давайте воспользуемся модулем ipython-memory-usage, чтобы проверить, сколько памяти потребляется во время выполнения нашей тестовой программы.
In [1]: def slotted_fn(): . class SlottedClass: . __slots__ = ["value"] . . def __init__(self, i): . self.value = i . . slotted = [] . for i in range(1_000_000): . slotted.append(SlottedClass(i)) . return slotted . . . def unslotted_fn(): . class UnSlottedClass: . def __init__(self, i): . self.value = i . . unslotted = [] . for i in range(1_000_000): . unslotted.append(UnSlottedClass(i)) . return unslotted . . . import ipython_memory_usage.ipython_memory_usage as imu . . imu.start_watching_memory() In [1] used 0.0000 MiB RAM in 0.11s, peaked 0.00 MiB above current, total RAM usage 52.48 MiB In [2]: slotted_fn() Out[2]: . In [2] used 84.9766 MiB RAM in 0.73s, peaked 0.00 MiB above current, total RAM usage 139.00 MiB In [3]: unslotted_fn() Out[3]: . In [3] used 200.1562 MiB RAM in 0.84s, peaked 0.00 MiB above current, total RAM usage 339.16 MiB
Как видите, версия со слотами заняла всего 85 Мб оперативной памяти, а версия без слотов — более 200 Мб, хотя результирующий размер списков одинаков.
Причина этого заключается в том, как Python внутренне обрабатывает dict . Если не указывать __slots__ , Python по умолчанию использует словарь для хранения атрибутов. Этот словарь динамичен по своей природе, может изменяться в размерах, должен быть организован по ключам и т.д. Поэтому Python требуется много памяти для управления словарем.
В щелевой версии класса ключевые характеристики dict больше не нужны, поскольку динамическое изменение размера больше не допускается. Таким образом, Python заранее выделяет память для атрибутов, упомянутых в __slots__ .