Создание классов и объектов
В языке программирования Python классы создаются с помощью инструкции class , за которой следует произвольное имя класса, после которого ставится двоеточие, далее с новой строки и с отступом реализуется тело класса:
class ИмяКласса: код_тела_класса
Если класс является дочерним, то родительские классы перечисляются в круглых скобках после имени класса.
Объект создается путем вызова класса по его имени. При этом после имени класса обязательно ставятся скобки:
ИмяКласса()
То есть класс вызывается подобно функции. Однако в случае вызова класса происходит не выполнение его тела, как это происходило бы при вызове функции, а создается объект. Поскольку в программном коде важно не потерять ссылку на только что созданный объект, то обычно его связывают с переменной. Поэтому создание объекта чаще всего выглядит так:
имя_переменной = ИмяКласса()
В последствии к объекту обращаются через связанную с ним переменную.
Пример «пустого» класса и двух созданных на его основе объектов:
>>> class A: . pass . >>> a = A() >>> b = A()
Класс как пространство имен
С точки зрения пространства имен класс можно представить подобным модулю. Также как в модуле в классе могут быть свои переменные со значениями и функции. Также как в модуле у класса есть собственное пространство имен, доступ к которому возможен через имя класса:
>>> class B: . n = 5 . def adder(v): . return v + B.n . >>> B.n 5 >>> B.adder(4) 9
Однако в случае классов используется особая терминология. Пусть имена, определенные в классе, называются атрибутами этого класса. В примере имена n и adder – это атрибуты класса B . Атрибуты-переменные часто называют полями или свойствами (в других языках понятия «поле» и «свойство» не совсем одно и то же). Полем является n . Атрибуты-функции называются методами. Методом в классе B является adder . Количество свойств и методов в классе может быть любым.
Класс как шаблон для создания объектов
На самом деле классы – не модули. Они своего рода шаблоны, от которых создаются объекты-экземпляры. Такие объекты наследуют от класса его атрибуты. Вернемся к нашему классу B и создадим на его основе два объекта:
>>> class B: . n = 5 . def adder(v): . return v + B.n . >>> a = B() >>> b = B()
У объектов, связанных с переменными a и b , нет собственного поля n . Однако они наследуют его от своего класса:
>>> a.n 5 >>> a.n is B.n True
То есть поля a.n и B.n – это одно и то же поле, к которому можно обращаться и через имя a , и через имя b , и через имя класса. Поле одно, ссылок на него три.
Однако что произойдет в момент присваивания этому полю значения через какой-нибудь объект-экземпляр?
>>> a.n = 10 >>> a.n 10 >>> b.n 5 >>> B.n 5
В этот момент у экземпляра появляется собственный атрибут n , который перекроет (переопределит) родительский, то есть тот, который достался от класса.
>>> a.n is B.n False >>> b.n is B.n True
При этом присвоение через B.n отразится только на b и B , но не на a :
>>> B.n = 100 >>> B.n, b.n, a.n (100, 100, 10)
Иная ситуация нас ожидает с атрибутом adder . При создании объекта от класса функция adder не наследуется как есть, а как бы превращается для объекта в одноименный метод:
>>> B.adder is b.adder False >>> type(B.adder) >>> type(b.adder)
Через имя класса мы вызываем функцию adder :
>>> B.adder(33) 133
Через имя объекта вызываем метод adder :
>>> b.adder(33) Traceback (most recent call last): File "", line 1, in TypeError: adder() takes 1 positional argument but 2 were given
В сообщении об ошибке говорится, что adder принимает только один аргумент, а было передано два. Откуда появился второй, если в скобках было указано только одно число?
Дело в том, что в отличии от функции в метод первым аргументом всегда передается объект, к которому применяется этот метод. То есть выражение b.adder(33) как бы преобразовывается в adder(b, 33) . Сам же b.adder как объект типа method хранит сведения, с каким классом он связан и какому объекту-экземпляру принадлежит:
>>> b.adder >
В нашем случае, чтобы вызывать adder через объекты-экземпляры, класс можно переписать так:
>>> class B: . n = 5 . def adder(obj, v): . return v + obj.n . >>> b = B() >>> b.adder(33) 38
В коде выше при вызове метода adder переменной-параметру obj присваивается объект, связанный с переменной, к которой применяется данный метод. В данном случае это объект, связанный с b . Если adder будет вызван на другой объект, то уже он будет присвоен obj :
>>> a = B() >>> a.n = 9 >>> a.adder(3) 12
В Python переменную-параметр метода, которая связывается с экземпляром своего класса, принято называть именем self. Таким образом, более корректный код будет таким:
>>> class B: . n = 5 . def adder(self, v): . return v + self.n
Можем ли мы все также вызывать adder как функцию, через имя класса? Вполне. Только теперь в функцию надо передавать два аргумента:
>>> B.adder(B, 200) 205 >>> B.adder(a, 200) 209
Здесь первым аргументом в функцию передается объект, у которого есть поле n лишь только потому, что далее к этому полю обращаются через выражение self.n .
Однако если атрибут определен так, что предполагается его работа в качестве метода, а не функции, то через класс его уже не вызывают (нет смысла, логика программы этого не подразумевает).
С другой стороны, в ООП есть понятие «статический метод». По сути это функция, которая может вызываться и через класс, и через объект, и которой первым аргументом не подставляется объект, на который она вызывается. В Python статический метод можно создать посредством использования специального декоратора.
Атрибут __dict__
В Python у объектов есть встроенные специальные атрибуты. Мы их не определяем, но они есть. Одним из таких атрибутов объекта является свойство __dict__ . Его значением является словарь, в котором ключи – это имена свойств экземпляра, а значения – текущие значения свойств.
>>> class B: . n = 5 . def adder(self, v): . return v + self.n . >>> w = B() >>> w.__dict__ <> >>> w.n = 8 >>> w.__dict__
В примере у экземпляра класса B сначала нет собственных атрибутов. Свойство n и метод adder – это атрибуты объекта-класса, а не объекта-экземпляра, созданного от этого класса. Лишь когда мы выполняем присваивание новому полю n экземпляра, у него появляется собственное свойство, что мы наблюдаем через словарь __dict__ .
В следующем уроке мы увидим, что свойства экземпляра обычно не назначаются за пределами класса. Это происходит в методах классах путем присваивание через self . Например, self.n = 10 .
Атрибут __dict__ используется не только для просмотра свойств объекта. С его помощью можно удалять, добавлять свойства, а также изменять их значения.
>>> w.__dict__['m'] = 100 >>> w.__dict__ >>> w.m 100
Практическая работа
Напишите программу по следующему описанию. Есть класс «Воин». От него создаются два экземпляра-юнита. Каждому устанавливается здоровье в 100 очков. В случайном порядке они бьют друг друга. Тот, кто бьет, здоровья не теряет. У того, кого бьют, оно уменьшается на 20 очков от одного удара. После каждого удара надо выводить сообщение, какой юнит атаковал, и сколько у противника осталось здоровья. Как только у кого-то заканчивается ресурс здоровья, программа завершается сообщением о том, кто одержал победу.
Курс с примерами решений практических работ:
pdf-версия
X Скрыть Наверх
Объектно-ориентированное программирование на Python
Python: статические методы, методы класса и экземпляра класса
Согласно модели данных Python, язык предлагает три вида методов: статические, класса и экземпляра класса. Давайте посмотрим, что же происходит за кулисами каждого из видов методов. Понимание принципов их работы поможет в создании красивого и эффективного кода. Начнём с самого простого примера, в котором демонстрируются все три вида методов.
class ToyClass:
def instancemethod(self):
return 'instance method called', self
@classmethod
def classmethod(cls):
return 'class method called', cls @staticmethod
def staticmethod():
return 'static method called'
Методы экземпляра класса
Это наиболее часто используемый вид методов. Методы экземпляра класса принимают объект класса как первый аргумент, который принято называть self и который указывает на сам экземпляр. Количество параметров метода не ограничено.
Используя параметр self , мы можем менять состояние объекта и обращаться к другим его методам и параметрам. К тому же, используя атрибут self.__class__ , мы получаем доступ к атрибутам класса и возможности менять состояние самого класса. То есть методы экземпляров класса позволяют менять как состояние определённого объекта, так и класса.
Встроенный пример метода экземпляра — str.upper() :
>>> "welcome".upper() # 'WELCOME'
Методы класса
Методы класса принимают класс в качестве параметра, который принято обозначать как cls . Он указывает на класс ToyClass, а не на объект этого класса. При декларации методов этого вида используется декоратор classmethod .
Методы класса привязаны к самому классу, а не его экземпляру. Они могут менять состояние класса, что отразится на всех объектах этого класса, но не могут менять конкретный объект.
Встроенный пример метода класса — dict.fromkeys() — возвращает новый словарь с переданными элементами в качестве ключей.
dict.fromkeys('AEIOU') #
Статические методы
Статические методы декларируются при помощи декоратора staticmethod . Им не нужен определённый первый аргумент (ни self , ни cls ).
Их можно воспринимать как методы, которые “не знают, к какому классу относятся”.
Таким образом, статические методы прикреплены к классу лишь для удобства и не могут менять состояние ни класса, ни его экземпляра.
С теорией достаточно. Давайте разберёмся с работой методов, создав объект нашего класса и вызвав поочерёдно каждый из методов: instancemethod, classmethod and staticmethod.
>>> obj = ToyClass()
>>> obj.instancemethod()
('instance method called', ToyClass instance at 0x10f47e7a0>)>>> ToyClass.instancemethod(obj)
('instance method called', ToyClass instance at 0x10f47e7a0>)
Пример выше подтверждает то, что метод instancemethod имеет доступ к объекту класса ToyClass через аргумент self . Кстати, вызов функции obj.instancemethod() используется лишь для удобства, то есть можно использовать и ToyClass.instancemethod(obj) .
Теперь давайте вызовем метод класса:
>>> obj.classmethod()
('class method called', )
Мы видим, что метод класса classmethod() имеет доступ к самому классу ToyClass, но не к его конкретному экземпляру объекта. Запомните, в Python всё является объектом. Класс тоже объект, который мы можем передать функции в качестве аргумента.
Заметьте, что self и cls — не обязательные названия и эти параметры можно называть иначе.
def instancemethod(self, . )
def classmethod(cls, . )-------то же самое, что и----------def instancemethod(my_object, . )
def classmethod(my_class, . )
Это лишь общепринятые обозначения, которым следуют все. Тем не менее они должны находиться первыми в списке параметров.
Вызовем статический метод:
>>> obj.staticmethod()
static method called
Да, это может вас удивить, но статические методы можно вызывать через объект класса. Вызов через точку нужен лишь для удобства. На самом же деле в случае статического метода никакие аргументы ( self или cls ) методу не передаются.
То есть статические методы не могут получить доступ к параметрам класса или объекта. Они работают только с теми данными, которые им передаются в качестве аргументов.
Теперь давайте вызовем те же самые методы, но на самом классе.
>>> ToyClass.classmethod()
('class method called', )>>> ToyClass.staticmethod()
'static method called'>>> ToyClass.instancemethod()
TypeError: unbound method instancemethod()
must be called with ToyClass instance as
first argument (got nothing instead)
Метод класса и статический метод работают, как нужно. Однако вызов метода экземпляра класса выдаёт TypeError, так как метод не может получить на вход экземпляр класса.
Теперь, когда вы знаете разницу между тремя видами методов, давайте рассмотрим реальный пример для понимания того, когда и какой метод стоит использовать. Пример взят отсюда.
from datetime import dateclass Person:
def __init__(self, name, age):
self.name = name
self.age = age@classmethod
def from_birth_year(cls, name, year):
return cls(name, date.today().year - year)@staticmethod
def is_adult(age):
return age > 18person1 = Person('Sarah', 25)
person2 = Person.from_birth_year('Roark', 1994)>>> person1.name, person1.age
Sarah 25>>> person2.name, person2.age
Roark 24>>> Person.is_adult(25)
True
Когда использовать каждый из методов?
Выбор того, какой из методов использовать, может показаться достаточно сложным. Тем не менее с опытом этот выбор делать гораздо проще.
Чаще всего метод класса используется тогда, когда нужен генерирующий метод, возвращающий объект класса. Как видим, метод класса from_birth_year используется для создания объекта класса Person по году рождения, а не возрасту.
Статические методы в основном используются как вспомогательные функции и работают с данными, которые им передаются.
Запомнить
- Методы экземпляра класса получают доступ к объекту класса через параметр self и к классу через self.__class__ .
- Методы класса не могут получить доступ к определённому объекту класса, но имеют доступ к самому классу через cls .
- Статические методы работают как обычные функции, но принадлежат области имён класса. Они не имеют доступа ни к самому классу, ни к его экземплярам.
- Даже если вы программируете только ради интереса, изучение ООП в 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
Объектно-ориентированное программирование
Python имеет множество встроенных типов, например, int, str и так далее, которые мы можем использовать в программе. Но также Python позволяет определять собственные типы с помощью классов . Класс представляет некоторую сущность. Конкретным воплощением класса является объект.
Можно еще провести следующую аналогию. У нас у всех есть некоторое представление о человеке, у которого есть имя, возраст, какие-то другие характеристики Человек может выполнять некоторые действия — ходить, бегать, думать и т.д. То есть это представление, которое включает набор характеристик и действий, можно назвать классом. Конкретное воплощение этого шаблона может отличаться, например, одни люди имеют одно имя, другие — другое имя. И реально существующий человек будет представлять объект этого класса.
Класс определяется с помощью ключевого слова class :
class название_класса: атрибуты_класса методы_класса
Внутри класса определяются его атрибуты, которые хранят различные характеристики класса, и методы — функции класса.
Создадим простейший класс:
class Person: pass
В данном случае определен класс Person, который условно представляет человека. В данном случае в классе не определяется никаких методов или атрибутов. Однако поскольку в нем должно быть что-то определено, то в качестве заменителя функционала класса применяется оператор pass . Этот оператор применяется, когда синтаксически необходимо определить некоторый код, однако мы не хотим его, и вместо конкретного кода вставляем оператор pass.
После создания класса можно определить объекты этого класса. Например:
class Person: pass tom = Person() # определение объекта tom bob = Person() # определение объекта bob
После определения класса Person создаются два объекта класса Person — tom и bob. Для создания объекта применяется специальная функция — конструктор , которая называется по имени класса и которая возвращает объект класса. То есть в данном случае вызов Person() представляет вызов конструктора. Каждый класс по умолчанию имеет конструктор без параметров:
tom = Person() # Person() - вызов конструктора, который возвращает объект класса Person
Методы классов
Методы класса фактически представляют функции, которые определенны внутри класса и которые определяют его поведение. Например, определим класс Person с одним методом:
class Person: # определение класса Person def say_hello(self): print("Hello") tom = Person() tom.say_hello() # Hello
Здесь определен метод say_hello() , который условно выполняет приветствие — выводит строку на консоль. При определении методов любого класса следует учитывать, что все они должны принимать в качестве первого параметра ссылку на текущий объект, который согласно условностям называется self . Через эту ссылку внутри класса мы можем обратиться к функциональности текущего объекта. Но при самом вызове метода этот параметр не учитывается.
Используя имя объекта, мы можем обратиться к его методам. Для обращения к методам применяется нотация точки — после имени объекта ставится точка и после нее идет вызов метода:
объект.метод([параметры метода])
Например, обращение к методу say_hello() для вывода приветствия на консоль:
tom.say_hello() # Hello
В итоге данная программа выведет на консоль строку «Hello».
Если метод должен принимать другие параметры, то они определяются после параметра self , и при вызове подобного метода для них необходимо передать значения:
class Person: # определение класса Person def say(self, message): # метод print(message) tom = Person() tom.say("Hello METANIT.COM") # Hello METANIT.COM
Здесь определен метод say() . Он принимает два параметра: self и message. И для второго параметра — message при вызове метода необходимо передать значение.
self
Через ключевое слово self можно обращаться внутри класса к функциональности текущего объекта:
self.атрибут # обращение к атрибуту self.метод # обращение к методу
Например, определим два метода в классе Person:
class Person: def say(self, message): print(message) def say_hello(self): self.say("Hello work") # обращаемся к выше определенному методу say tom = Person() tom.say_hello() # Hello work
Здесь в одном методе — say_hello() вызывается другой метод — say() :
self.say("Hello work")
Поскольку метод say() принимает кроме self еще параметры (параметр message), то при вызове метода для этого параметра передается значение.
Причем при вызове метода объекта нам обязательно необходимо использовать слово self , если мы его не используем:
def say_hello(self): say("Hello work") # ! Ошибка
То мы столкнемся с ошибкой
Конструкторы
Для создания объекта класса используется конструктор. Так, выше когда мы создавали объекты класса Person, мы использовали конструктор по умолчанию, который не принимает параметров и который неявно имеют все классы:
tom = Person()
Однако мы можем явным образом определить в классах конструктор с помощью специального метода, который называется __init__() (по два прочерка с каждой стороны). К примеру, изменим класс Person, добавив в него конструктор:
class Person: # конструктор def __init__(self): print("Создание объекта Person") def say_hello(self): print("Hello") tom = Person() # Создание объекта Person tom.say_hello() # Hello
Итак, здесь в коде класса Person определен конструктор и метод say_hello() . В качестве первого параметра конструктор, как и методы, также принимает ссылку на текущий объект — self. Обычно конструкторы применяются для определения действий, которые должны производиться при создании объекта.
Теперь при создании объекта:
tom = Person()
будет производится вызов конструктора __init__() из класса Person, который выведет на консоль строку «Создание объекта Person».
Атрибуты объекта
Атрибуты хранят состояние объекта. Для определения и установки атрибутов внутри класса можно применять слово self . Например, определим следующий класс Person:
class Person: def __init__(self, name): self.name = name # имя человека self.age = 1 # возраст человека tom = Person("Tom") # обращение к атрибутам # получение значений print(tom.name) # Tom print(tom.age) # 1 # изменение значения tom.age = 37 print(tom.age) # 37
Теперь конструктор класса Person принимает еще один параметр — name. Через этот параметр в конструктор будет передаваться имя создаваемого человека.
Внутри конструктора устанавливаются два атрибута — name и age (условно имя и возраст человека):
def __init__(self, name): self.name = name self.age = 1
Атрибуту self.name присваивается значение переменной name. Атрибут age получает значение 1.
Если мы определили в классе конструктор __init__, мы уже не сможем вызвать конструктор по умолчанию. Теперь нам надо вызывать наш явным образом опреледеленный конструктор __init__, в который необходимо передать значение для параметра name:
tom = Person("Tom")
Далее по имени объекта мы можем обращаться к атрибутам объекта — получать и изменять их значения:
print(tom.name) # получение значения атрибута name tom.age = 37 # изменение значения атрибута age
В принципе нам необязательно определять атрибуты внутри класса — Python позволяет сделать это динамически вне кода:
class Person: def __init__(self, name): self.name = name # имя человека self.age = 1 # возраст человека tom = Person("Tom") tom.company = "Microsoft" print(tom.company) # Microsoft
Здесь динамически устанавливается атрибут company, который хранит место работы человека. И после установки мы также можем получить его значение. В то же время подобное определение чревато ошибками. Например, если мы попытаемся обратиться к атрибуту до его определения, то программа сгенерирует ошибку:
tom = Person("Tom") print(tom.company) # ! Ошибка - AttributeError: Person object has no attribute company
Для обращения к атрибутам объекта внутри класса в его методах также применяется слово self:
class Person: def __init__(self, name): self.name = name # имя человека self.age = 1 # возраст человека def display_info(self): print(f"Name: Age: ") tom = Person("Tom") tom.display_info() # Name: Tom Age: 1
Здесь определяется метод display_info(), который выводит информацию на консоль. И для обращения в методе к атрибутам объекта применяется слово self: self.name и self.age
Создание объектов
Выше создавался один объект. Но подобным образом можно создавать и другие объекты класса:
class Person: def __init__(self, name): self.name = name # имя человека self.age = 1 # возраст человека def display_info(self): print(f"Name: Age: ") tom = Person("Tom") tom.age = 37 tom.display_info() # Name: Tom Age: 37 bob = Person("Bob") bob.age = 41 bob.display_info() # Name: Bob Age: 41
Здесь создаются два объекта класса Person: tom и bob. Они соответствуют определению класса Person, имеют одинаковый набор атрибутов и методов, однако их состояние будет отличаться.
При выполнении программы Python динамически будет определять self — он представляет объект, у которого вызывается метод. Например, в строке:
tom.display_info() # Name: Tom Age: 37
Это будет объект tom
bob.display_info()
Это будет объект bob
В итоге мы получим следующий консольный вывод:
Name: Tom Age: 37 Name: Bob Age: 41