Книга: Простой Python. Современный стиль программирования. 2-е изд.
Назад: Глава 9. Функции
Дальше: Глава 11. Модули, пакеты и программы

Глава 10. Ой-ой-ой: объекты и классы

Таинственных объектов не бывает. Они такими просто кажутся.

Элизабет Боуэн

Возьмите объект. Сделайте с ним что-нибудь. Добавьте к нему что-нибудь еще.

Джаспер Джонс

Как я уже говорил ранее, в Python все, от чисел до функций, является объектами. Однако Python скрывает бо'льшую часть функционирования объектных механизмов с помощью особого синтаксиса. Вы можете написать num=7, и, таким образом, у вас появится объект типа int со значением 7 и назначится ссылка на объект по имени num. Заглядывать внутрь объектов нужно только в случае, если вам необходимо создать собственный объект или модифицировать поведение уже существующих. В этой главе вы увидите, как сделать и то и другое.

Что такое объекты

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

Например, целочисленный объект со значением 7 может использовать такие методы, как сложение и умножение, что показано в главе 3. 8 — другой объект. Это значит, что существует класс Integer, встроенный в Python, которому принадлежат объекты 7 и 8. Строки 'cat' и 'duck' также являются объектами и имеют методы, с которыми вы уже познакомились в главе 5, — например, capitalize() и replace().

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

Простые объекты

Начнем мы с простых классов, а разговор о наследовании перенесем на несколько страниц вперед.

Определяем класс с помощью ключевого слова class

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

В главе 2 я сравнил объект с пластмассовой коробкой. Класс похож на форму, по образцу которой создается эта коробка. Например, в Python есть встроенный класс, создающий строковые объекты, такие как 'cat' и 'duck'. Python имеет и множество других встроенных классов, позволяющих создавать другие стандартные типы данных, в том числе списки, словари и т.д. Чтобы создать собственный объект в Python, вам сначала нужно определить класс с помощью ключевого слова class. Рассмотрим несколько простых примеров.

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

109154.png

Мы следуем концепциям об именовании, указанным в PEP-8 ().

Для начала создадим самый простой из возможных классов — пустой:

>>> class Cat():

...     pass

Можно и так:

>>> class Cat:

...     pass

Как и в случае с функциями, нам нужно сказать pass, чтобы указать, что этот класс пуст. Такое определение является минимально необходимым для создания объекта. Вы создаете объект из класса, вызывая имя класса так, как если бы это была функция:

>>> a_cat = Cat()

>>> another_cat = Cat()

В этом случае вызов Cat() создаст два отдельных объекта класса Cat, и мы присвоим им имена a_cat и another_cat. Но наш класс Cat пуст, поэтому объекты, которые мы создали, просто занимают место и ничего не делают.

Хотя нет, кое-что они могут.

Атрибуты

Атрибут — это переменная, расположенная внутри класса или объекта. Во время и после создания объекта или класса вы можете присвоить им атрибуты. Атрибутом может быть любой другой объект. Давайте снова создадим два объекта типа Cat:

>>> class Cat:

...     pass

...

>>> a_cat = Cat()

>>> a_cat

<__main__.Cat object at 0x100cd1da0>

>>> another_cat = Cat()

>>> another_cat

<__main__.Cat object at 0x100cd1e48>

Когда мы определили класс Cat, мы не указали, в каком виде его объект будет выведен на экран. Тут в дело вмешивается Python и выводит на экран что-то вроде <__main__.Catobjectat0x100cd1da0>. В разделе «Магические методы» на с. 215 вы увидите, как можно изменить это поведение, используемое по умолчанию.

Теперь назначим несколько атрибутов нашему первому объекту:

>>> a_cat.age = 3

>>> a_cat.name = "Mr. Fuzzybuttons"

>>> a_cat.nemesis = another_cat

Можем ли мы получить доступ к этим значениям? Надеюсь, что да:

>>> a_cat.age

3

>>> a_cat.name

'Mr. Fuzzybuttons'

>>> a_cat.nemesis

<__main__.Cat object at 0x100cd1e48>

Поскольку nemesis был атрибутом, ссылающимся на другой объект Cat, мы можем использовать атрибут a_cat.nemesis для доступа к нему. Но у этого другого объекта еще нет атрибута name:

>>> a_cat.nemesis.name

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: 'Cat' object has no attribute 'name'

Дадим имя нашему коту:

>>> a_cat.nemesis.name = "Mr. Bigglesworth"

>>> a_cat.nemesis.name

'Mr. Bigglesworth'

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

Когда вы слышите слово «атрибуты», речь, скорее всего, идет об атрибутах объектов. Помимо этого, существуют также атрибуты класса. Чем они различаются, вы увидите в подразделе «Атрибуты классов и объектов» на с. 210.

Методы

Метод — это функция класса или объекта. Метод выглядит так же, как и любая другая функция, но может быть использован особым образом — этот вопрос мы рассмотрим далее в этой главе, в подразделе «Свойства для доступа к атрибутам» и в разделе «Типы методов».

Инициализация

Если вы хотите присвоить атрибуты объекту во время его создания, вам понадобится специальный метод инициализации __init__:

>>> class Cat:

...     def __init__(self):

...         pass

Так выглядят реальные определения классов в Python. Согласен, __init__() и self смотрятся странно. __init__() — это особое имя метода, который инициа­лизирует отдельный объект с помощью определения его класса. Аргумент self указывает на сам объект.

Когда вы указываете __init__() в определении класса, его первым параметром должен быть объект с именем self. Несмотря на то что в Python self не является зарезервированным словом, оно применяется довольно часто. Никому из тех, кто будет читать ваш код позже (включая вас!), не придется гадать, что вы имели в виду, когда использовали слово self.

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

>>> class Cat():

...     def __init__(self, name):

...         self.name = name

...

>>>

Теперь мы можем создать объект класса Cat, передав строку для параметра name:

>>> furball = Cat('Grumpy')

Эта строка кода делает следующее:

выполняет поиск определения класса Cat;

• инстанцирует (создает) новый объект в памяти;

• вызывает метод объекта __init__(), передавая только что созданный объект под именем self и другой аргумент ('Grumpy') в качестве значения параметра name;

• сохраняет в объекте значение переменной name;

• возвращает новый объект;

прикрепляет к объекту переменную furball.

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

А что со значением name, которое мы передали? Оно было сохранено как атрибут объекта. Вы можете прочитать и записать его непосредственно:

>>> print('Our latest addition: ', furball.name)

Our latest addition: Grumpy

Помните, внутри определения класса Cat вы получаете доступ к атрибуту name с помощью конструкции self.name. Когда вы создаете реальный объект и присваиваете его переменной вроде furball, то ссылаетесь на этот атрибут как furball.name.

Не обязательно иметь метод __init__() в описании каждого класса — он используется для того, чтобы различать объекты одного класса. Это не то, что в некоторых других языках называется конструктором. Python уже создал объект для вас. Метод __init__() следует рассматривать скорее как средство инициализации.

109163.png

Вы можете создать много отдельных объектов из одного класса. Но помните, что в Python данные реализованы как объекты, поэтому и сам класс является объектом. Однако в вашей программе может существовать только один объект для каждого класса. Если класс Cat уже определен, как мы сделали в этой главе, то второй такой класс создать не получится. Он становится чем-то вроде Горца — должен остаться только один.

Наследование

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

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

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

Наследование от родительского класса

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

Итак, попробуем что-нибудь унаследовать. В следующем примере мы определим пустой класс, который называется Car. Далее мы определим подкласс класса Car, который называется Yugo. Вы определяете подкласс с помощью все того же ключевого слова class, но указываете внутри скобок имя родительского класса classYugo(Car), как показано ниже:

>>> class Car():

...     pass

...

>>> class Yugo(Car):

...     pass

...

Проверить, унаследован ли класс от другого класса, можно с помощью функции issubclass():

>>> issubclass(Yugo, Car)

True

Далее создадим объекты каждого класса:

>>> give_me_a_car = Car()

>>> give_me_a_yugo = Yugo()

Производный класс выступает уточненной версией родительского класса: если говорить в терминах объектно-ориентированных языков, Yugo является Car. Объект с именем give_me_a_yugo — это экземпляр класса Yugo, но он также наследует все то, что может делать класс Car. В нашем случае классы Car и Yugo абсолютно бесполезны, поэтому попробуем новые определения, которые действительно могут что-то сделать:

>>> class Car():

...     def exclaim(self):

...         print("I'm a Car!")

...

>>> class Yugo(Car):

...     pass

...

Наконец, создадим по одному объекту каждого класса и вызовем их методы exclaim:

>>> give_me_a_car = Car()

>>> give_me_a_yugo = Yugo()

>>> give_me_a_car.exclaim()

I'm a Car!

>>> give_me_a_yugo.exclaim()

I'm a Car!

Не сделав ничего особенного, класс Yugo унаследовал метод exclaim() класса Car. Фактически класс Yugo говорит, что он является классом Car, а это может привести к кризису самоопределения. Посмотрим, как решить проблему.

109171.png

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

Переопределение методов

Как вы только что увидели, новый класс изначально наследует все, что находится в его родительском классе. Далее вы узнаете, как можно заменить, или переопределить, родительские методы. Класс Yugo должен как-то отличаться от класса Car, иначе зачем вообще создавать новый класс. Изменим способ работы метода exclaim() для класса Yugo:

>>> class Car():

...     def exclaim(self):

...         print("I'm a Car!")

...

>>> class Yugo(Car):

...     def exclaim(self):

...         print("I'm a Yugo! Much like a Car, but more Yugo-ish.")

...

Теперь создадим объекты этих классов:

>>> give_me_a_car = Car()

>>> give_me_a_yugo = Yugo()

Что они говорят?

>>> give_me_a_car.exclaim()

I'm a Car!

>>> give_me_a_yugo.exclaim()

I'm a Yugo! Much like a Car, but more Yugo-ish.

В этих примерах мы переопределили метод exclaim(). Переопределять можно любые методы, включая __init__(). Вот еще один пример — с классом Person. Создадим подклассы, которые представляют докторов (MDPerson) и адвокатов (JDPerson):

>>> class Person():

...     def __init__(self, name):

...         self.name = name

...

>>> class MDPerson(Person):

...     def __init__(self, name):

...         self.name = "Doctor " + name

...

>>> class JDPerson(Person):

...     def __init__(self, name):

...         self.name = name + ", Esquire"

...

В этих случаях метод инициализации __init__() принимает те же аргументы, что и родительский класс Person, но по-разному сохраняет значение переменной name внутри объекта:

>>> person = Person('Fudd')

>>> doctor = MDPerson('Fudd')

>>> lawyer = JDPerson('Fudd')

>>> print(person.name)

Fudd

>>> print(doctor.name)

Doctor Fudd

>>> print(lawyer.name)

Fudd, Esquire

Добавление метода

В производный класс можно добавить и метод, которого не было в родительском классе. Вернемся к классам Car и Yugo и определим новый метод need_a_push() только для класса Yugo:

>>> class Car():

...     def exclaim(self):

...         print("I'm a Car!")

...

>>> class Yugo(Car):

...     def exclaim(self):

...         print("I'm a Yugo! Much like a Car, but more Yugo-ish.")

...     def need_a_push(self):

...         print("A little help here?")

...

Далее создадим объекты классов Car и Yugo:

>>> give_me_a_car = Car()

>>> give_me_a_yugo = Yugo()

Объект класса Yugo может реагировать на вызов метода need_a_push():

>>> give_me_a_yugo.need_a_push()

A little help here?

А объект общего класса Car — нет:

>>> give_me_a_car.need_a_push()

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: 'Car' object has no attribute 'need_a_push'

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

Получаем помощь от своего родителя с использованием метода super()

Мы видели, как производный класс может добавить или переопределить метод родительского класса. Но что, если нужно будет вызвать этот родительский метод? «Рад, что вы спросили», — говорит метод super(). Сейчас мы определим новый класс EmailPerson, который представляет объект класса Person с адресом электронной почты. Для начала запишем наше привычное определение класса Person:

>>> class Person():

...     def __init__(self, name):

...         self.name = name

...

Обратите внимание на то, что вызов метода __init__() в следующем подклассе имеет дополнительный параметр email:

>>> class EmailPerson(Person):

...     def __init__(self, name, email):

...         super().__init__(name)

...         self.email = email

Когда вы определяете метод __init__() для своего класса, вы заменяете метод __init__() родительского класса, который больше не вызывается автоматически. В результате вам нужно вызывать его явно. Происходит следующее:

метод super() получает определение родительского класса Person;

• метод __init__() вызывает метод Person.__init__(). Последний заботится о том, чтобы передать аргумент self суперклассу, поэтому вам нужно просто дать ему любые необязательные аргументы. В нашем случае единственным аргументом класса Person() будет name;

строка self.email=email — это новый код, который отличает класс EmailPerson от класса Person.

Теперь создадим одну персону:

>>> bob = EmailPerson('Bob Frapples', '')

Мы должны иметь доступ к атрибутам name и email:

>>> bob.name

'Bob Frapples'

>>> bob.email

''

Почему бы нам просто не определить новый класс так, как показано далее?

>>> class EmailPerson(Person):

...     def __init__(self, name, email):

...         self.name = name

...         self.email = email

Мы могли бы это сделать, но тогда потеряли бы возможность применять наследование. Мы использовали метод super(), чтобы заставить Person делать свою работу так же, как это делает обычный объект класса Person. Есть и другое преимущество: если определение класса Person в будущем изменится, с помощью метода super() мы сможем гарантировать, что атрибуты и методы, которые класс EmailPerson наследует от класса Person, отреагируют на изменения.

Используйте метод super(), когда потомок делает что-то по-своему, но все еще нуждается в помощи родителя (как и в реальной жизни).

Множественное наследование

Вы только что видели несколько примеров классов, у которых нет родительского класса, и несколько примеров, у которых есть. Однако объекты могут наследовать и от нескольких родительских классов.

Если ваш класс ссылается на метод или атрибут, которого у него нет, Python будет искать его в родительском классе. Что, если у нескольких классов будут атрибуты с одинаковыми именами? Кто победит?

В отличие от наследования у людей, когда доминантный ген побеждает независимо от того, кому он принадлежит, наследование в Python зависит от порядка разрешения методов. Каждый класс Python имеет особый метод mro(), возвращающий список классов, в которых будет выполнен поиск метода или атрибута для объекта этого класса. Похожий атрибут с именем __mro__ представляет собой кортеж, содержащий имена этих классов. Как и в плей-офф, когда действует принцип «внезапной смерти», побеждает первый. Сейчас мы определим родительский класс Animal, два класса-потомка (Horse и Donkey), а затем еще два класса, полученных из них:

>>> class Animal:

...     def says(self):

            return 'I speak!'

...

>>> class Horse(Animal):

...     def says(self):

...         return 'Neigh!'

...

>>> class Donkey(Animal):

...     def says(self):

...         return 'Hee-haw!'

...

>>> class Mule(Donkey, Horse):

...     pass

...

>>> class Hinny(Horse, Donkey):

...     pass

...

Если нам понадобится метод или атрибут класса Mule, Python будет искать его в следующих классах по порядку.

1. Сам объект (типа Mule).

2. Класс объекта (Mule).

3. Первый родительский класс этого класса (Donkey).

4. Второй родительский класс этого класса (Horse).

5. Прародительский класс этого класса (Animal).

Для класса Hinny список выглядит примерно так же, только класс Horse расположится перед классом Donkey:

>>> Mule.mro()

[<class '__main__.Mule'>, <class '__main__.Donkey'>,

<class '__main__.Horse'>, <class '__main__.Animal'>,

<class 'object'>]

>>> Hinny.mro()

[<class '__main__.Hinny'>, <class '__main__.Horse'>,

<class '__main__.Donkey'>, <class '__main__.Animal'>,

class 'object'>]

Так что же говорят эти прекрасные животные?

>>> mule = Mule()

>>> hinny = Hinny()

>>> mule.says()

'hee-haw'

>>> hinny.says()

'neigh'

Мы перечислили родительские классы в порядке «папа, мама», чтобы они разговаривали как «папы». Если бы у классов Horse и Donkey не было метода says(), классы Mule и Hinny использовали бы метод says() прародительского класса Animal и возвратили бы значение 'Ispeak!'.

Примеси

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

Такой родительский класс называют классом-примесью или миксином. Примеси можно использовать для выполнения «сторонних» задач, таких, например, как ведение журнала. Рассмотрим класс-примесь, который форматирует и выводит на экран атрибуты объекта:

>>> class PrettyMixin():

...     def dump(self):

...         import pprint

...         pprint.pprint(vars(self))

...

>>> class Thing(PrettyMixin):

...     pass

...

>>> t = Thing()

>>> t.name = "Nyarlathotep"

>>> t.feature = "ichor"

>>> t.age = "eldritch"

>>> t.dump()

{'age': 'eldritch', 'feature': 'ichor', 'name': 'Nyarlathotep'}

В защиту self

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

Помните класс Car из предыдущих примеров? Снова вызовем метод exclaim():

>>> a_car = Car()

>>> a_car.exclaim()

I'm a Car!

Вот что происходит за кулисами Python.

Выполняется поиск класса (Car) объекта a_car.

Объект a_car передается методу exclaim() класса Car как параметр self.

Ради развлечения вы и сами можете запустить пример таким образом, и он сработает точно так же, как и нормальный синтаксис (a_car.exclaim()):

>>> Car.exclaim(car)

I'm a Car!

Тем не менее нет никаких причин использовать этот более длинный стиль.

Доступ к атрибутам

В Python атрибуты объектов и методы обычно являются общедоступными, поэтому предполагается, что тот, кто их использует, не станет этим злоупотреблять (это иногда называют «установкой на взрослых людей»). Давайте сравним прямой доступ и его альтернативы.

Прямой доступ

Как вы уже видели, вы можете получать и задавать значения атрибутов напрямую:

>>> class Duck:

...     def __init__(self, input_name):

...         self.name = input_name

...

>>> fowl = Duck('Daffy')

>>> fowl.name

'Daffy'

Но что случится, если кто-то решит этим злоупотребить?

>>> fowl.name = 'Daphne'

>>> fowl.name

'Daphne'

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

Геттеры и сеттеры

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

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

Далее в примере мы определим класс Duck, имеющий один атрибут экземпляра hidden_name. Мы не хотим, чтобы кто-то мог обратиться к атрибуту напрямую, поэтому определим два метода: геттер (get_name()) и сеттер (set_name()). К каждому из них обращается свойство с именем name. Я добавил выражение print() в оба метода, чтобы показать момент его вызова:

>>> class Duck():

...     def __init__(self, input_name):

...         self.hidden_name = input_name

...     def get_name(self):

...         print('inside the getter')

...         return self.hidden_name

...     def set_name(self, input_name):

...         print('inside the setter')

...         self.hidden_name = input_name

 

>>> don = Duck('Donald')

>>> don.get_name()

inside the getter

'Donald'

>>> don.set_name('Donna')

inside the setter

>>> don.get_name()

inside the getter

'Donna'

Свойства для доступа к атрибутам

Более питонским решением для закрытия атрибутов является использование свойств.

Существует два способа сделать это. Первый — добавить конструкцию name=pro­perty(get_name,set_name) последней строкой в предыдущее определение класса Duck:

>>> class Duck():

>>>     def __init__(self, input_name):

>>>         self.hidden_name = input_name

>>>     def get_name(self):

>>>         print('inside the getter')

>>>         return self.hidden_name

>>>     def set_name(self, input_name):

>>>         print('inside the setter')

>>>         self.hidden_name = input_name

>>>     name = property(get_name, set_name)

Старый подход с геттерами и сеттерами тоже сработает:

>>> don = Duck('Donald')

>>> don.get_name()

inside the getter

'Donald'

>>> don.set_name('Donna')

inside the setter

>>> don.get_name()

inside the getter

'Donna'

Но теперь вы также можете использовать свойство name, чтобы получить и установить скрытое имя:

>>> don = Duck('Donald')

>>> don.name

inside the getter

'Donald'

>>> don.name = 'Donna'

inside the setter

>>> don.name

inside the getter

'Donna'

Во втором методе вы добавляете декораторы и заменяете имена методов get_name и set_name на name:

@property — размещается перед геттером;

@name.setter — размещается перед сеттером.

В коде они выглядят так:

>>> class Duck():

...     def __init__(self, input_name):

...         self.hidden_name = input_name

...     @property

...     def name(self):

...         print('inside the getter')

...         return self.hidden_name

...     @name.setter

...     def name(self, input_name):

...         print('inside the setter')

...         self.hidden_name = input_name

Вы все еще можете получить доступ к name, как будто это атрибут:

>>> fowl = Duck('Howard')

>>> fowl.name

inside the getter

'Howard'

>>> fowl.name = 'Donald'

inside the setter

>>> fowl.name

inside the getter

'Donald'

109181.png

Если кто-то и догадается, что мы назвали наш атрибут hidden_name, считать и записать его он все равно может непосредственно как fowl.hidden_name. В подразделе «Искажение имен для безопасности» далее в этой главе вы увидите особый способ Python, позволяющий скрыть имена атрибутов.

Свойства для вычисляемых значений

В предыдущих примерах мы использовали свойство name, чтобы обратиться к отдельному атрибуту (в нашем случае hidden_name), который хранится внутри объекта.

Свойство может возвращать и вычисляемое значение. Определим класс Circle, который имеет атрибут radius и вычисляемое свойство diameter:

>>> class Circle():

...     def __init__(self, radius):

...         self.radius = radius

...     @property

...     def diameter(self):

...         return 2 * self.radius

...

Мы создаем объект класса Circle, задав значение его атрибута radius:

>>> c = Circle(5)

>>> c.radius

5

Мы можем обратиться к свойству diameter точно так же, как если бы это был атрибут, подобный radius:

>>> c.diameter

10

А вот и самое интересное — мы можем изменить значение атрибута radius в любой момент, и свойство diameter будет рассчитано на основе текущего значения атрибута radius:

>>> c.radius = 7

>>> c.diameter

14

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

>>> c.diameter = 20

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: can't set attribute

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

Искажение имен для безопасности

В рассмотренном ранее примере с классом Duck мы назвали наш (не полностью) скрытый атрибут hidden_name. Python предлагает соглашения по именованию для атрибутов, которые не должны быть видимы за пределами определения их классов: имена начинаются с двух нижних подчеркиваний (__).

Переименуем атрибут hidden_name в __name, как показано здесь:

>>> class Duck():

...     def __init__(self, input_name):

...         self.__name = input_name

...     @property

...     def name(self):

...         print('inside the getter')

...         return self.__name

...     @name.setter

...     def name(self, input_name):

...         print('inside the setter')

...         self.__name = input_name

...

Теперь проверим, работает ли все так, как нужно:

>>> fowl = Duck('Howard')

>>> fowl.name

inside the getter

'Howard'

>>> fowl.name = 'Donald'

inside the setter

>>> fowl.name

inside the getter

'Donald'

Выглядит неплохо. И вы не можете получить доступ к атрибуту __name:

>>> fowl.__name

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: 'Duck' object has no attribute '__name'

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

>>> fowl._Duck__name

'Donald'

Обратите внимание на то, что на экране не появилась надпись insidethegetter. Хотя эта защита неидеальна, искажение имен препятствует случайному или преднамеренному доступу к атрибуту.

Атрибуты классов и объектов

Вы можете присвоить атрибуты классам, и они будут унаследованы их объектами-потомками:

>>> class Fruit:

...     color = 'red'

...

>>> blueberry = Fruit()

>>> Fruit.color

'red'

>>> blueberry.color

'red'

Но если вы измените значение атрибута в объекте-потомке, атрибут класса не изменится:

>>> blueberry.color = 'blue'

>>> blueberry.color

'blue'

>>> Fruit.color

'red'

Если позже измените атрибут класса, на существующих объектах-потомках это не отразится:

>>> Fruit.color = 'orange'

>>> Fruit.color

'orange'

>>> blueberry.color

'blue'

But it will affect new ones:

>>> new_fruit = Fruit()

>>> new_fruit.color

'orange'

Типы методов

Одни методы являются частью самого класса, другие — частью объектов, созданных из этого класса, а какие-то не являются ни тем ни другим.

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

• Если у метода есть декоратор @classmethod, то это метод класса и его первым аргументом должен быть cls (имя может быть любым, кроме зарезервированного слова class), который ссылается на класс.

Если у метода есть декоратор @staticmethod, то это статический метод и его первым аргументом будет не объект и не класс.

В следующих подразделах мы рассмотрим эти вопросы более детально.

Методы объектов

Когда вы видите начальный аргумент self в методах внутри определения класса, вы имеете дело с методом объекта: такие методы вы обычно пишете, когда создаете собственный класс. Первый параметр метода экземпляра — это self, Python передает объект методу, когда вы его вызываете. Это единственные методы, которые мы уже рассмотрели.

Методы классов

В противоположность ему метод класса влияет на весь класс целиком. Любое изменение, которое происходит с классом, влияет на все его объекты. Внутри определения класса декоратор @classmethod показывает, что следующая функция является методом класса. Первым параметром метода также является сам класс. По традиции этот параметр называется cls, поскольку слово class зарезервировано и не может быть использовано в данной ситуации. Определим метод класса для А, который будет подсчитывать количество созданных объектов:

>>> class A():

...     count = 0

...     def __init__(self):

...         A.count += 1

...     def exclaim(self):

...         print("I'm an A!")

...     @classmethod

...     def kids(cls):

...         print("A has", cls.count, "little objects.")

...

>>>

>>> easy_a = A()

>>> breezy_a = A()

>>> wheezy_a = A()

>>> A.kids()

A has 3 little objects.

Обратите внимание на то, что мы вызвали метод A.count (атрибут класса) вместо self.count (который является атрибутом объекта). В методе kids() мы использовали вызов cls.count, но с тем же успехом могли бы применить вызов A.count.

Статические методы

Третий тип методов не влияет ни на классы, ни на объекты: он находится внутри класса только для удобства, чтобы не располагаться где-то отдельно. Это статический метод, которому предшествует декоратор @staticmethod, не имеющий в качестве начального параметра ни self, ни класс class. Рассмотрим пример, который является рекламой класса CoyoteWeapon:

>>> class CoyoteWeapon():

...     @staticmethod

...     def commercial():

...         print('This CoyoteWeapon has been brought to you by Acme')

...

>>>

>>> CoyoteWeapon.commercial()

This CoyoteWeapon has been brought to you by Acme

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

Утиная типизация

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

Используем уже знакомый нам инициализатор __init__() для всех трех классов Quote, но добавим две новые функции:

who() возвращает значение сохраненной строки person;

says() возвращает сохраненную строку words, имеющую особую пунктуацию.

Посмотрим на них в действии:

>>> class Quote():

...     def __init__(self, person, words):

...         self.person = person

...         self.words = words

...     def who(self):

...         return self.person

...     def says(self):

...         return self.words + '.'

...

>>> class QuestionQuote(Quote):

...      def says(self):

...          return self.words + '?'

...

>>> class ExclamationQuote(Quote):

...      def says(self):

...          return self.words + '!'

...

>>>

Мы не меняли способ инициализации классов QuestionQuote и ExclamationQuote, поэтому не переопределяли их методы __init__(). Далее Python автоматически вызывает метод __init__() родительского класса Quote, чтобы сохранить переменные объекта person и words. Таким образом, мы можем получить доступ к атрибуту self.words в объектах, созданных с помощью подклассов QuestionQuote и ExclamationQuote.

Создадим несколько объектов:

>>> hunter = Quote('Elmer Fudd', "I'm hunting wabbits")

>>> print(hunter.who(), 'says:', hunter.says())

Elmer Fudd says: I'm hunting wabbits.

 

>>> hunted1 = QuestionQuote('Bugs Bunny', "What's up, doc")

>>> print(hunted1.who(), 'says:', hunted1.says())

Bugs Bunny says: What's up, doc?

 

>>> hunted2 = ExclamationQuote('Daffy Duck', "It's rabbit season")

>>> print(hunted2.who(), 'says:', hunted2.says())

Daffy Duck says: It's rabbit season!

Три разные версии метода says() обеспечивают разное поведение трех классов. Так выглядит традиционный полиморфизм в объектно-ориентированных языках. Python пошел немного дальше и позволяет вызывать методы who() и says() для любых объектов, в том числе и для этих методов. Определим класс BabblingBrook, который не имеет никакого отношения к нашим охотнику и его жертвам (потомкам класса Quote), созданным ранее:

>>> class BabblingBrook():

...     def who(self):

...         return 'Brook'

...     def says(self):

...         return 'Babble'

...

>>> brook = BabblingBrook()

Теперь запустим методы who() и says() разных объектов, один из которых (brook) совершенно не связан с остальными:

>>> def who_says(obj):

...     print(obj.who(), 'says', obj.says())

...

>>> who_says(hunter)

Elmer Fudd says I'm hunting wabbits.

>>> who_says(hunted1)

Bugs Bunny says What's up, doc?

>>> who_says(hunted2)

Daffy Duck says It's rabbit season!

>>> who_says(brook)

Brook says Babble

Благодаря старинной поговорке «Если нечто выглядит как утка, плавает как утка и крякает как утка, то, скорее всего, это и есть утка» подобное поведение иногда называют утиной типизацией (рис. 10.1). Кто мы такие, чтобы спорить с мудрецом, рассуждающим об утках?

10_01.tif 

Рис. 10.1. Утиная типизация — это не значит печатать текст, сложив губы уточкой

Магические методы

Теперь вы можете создавать и использовать простые объекты. То, что мы изучим в этом разделе, может вас удивить — в хорошем смысле этого слова.

Когда вы пишете что-то вроде a=3+8, откуда целочисленные объекты со значения­ми 3 и 8 узнают, как реализовать операцию +? А если вы введете конструкцию name="Daffy"+""+"Duck", как Python узнает, что оператор + теперь означает конкатенацию строк? И что подскажет переменным a и name, как применить =, чтобы получить результат? Вы можете получить доступ к этим операторам, используя специальные методы Python (или, что звучит более драматично, магические методы).

Имена этих методов начинаются и заканчиваются двойными подчеркиваниями (__). Почему? Как правило, такие имена не выбирают в качестве имен для переменных. Вы уже видели один такой метод: __init__() инициализирует только что созданный объект с помощью описания его класса и любых аргументов, которые передаются в этот метод. Вы также видели (см. подраздел «Искажение имен для безопасности»), как использование в именах двойного нижнего подчеркивания помогает исказить имена атрибутов класса и его методов.

Предположим, у вас есть простой класс Word и вы хотите написать для него метод equals(), который сравнивает два слова, игнорируя регистр. Так и есть, объект класса Word, содержащий значение 'ha', будет считаться равным другому объекту, который содержит значение 'HA'.

В следующем примере показана наша первая попытка, где мы вызываем equals() — обычный метод. self.text — это текстовая строка, которую содержит объект класса Word. Метод equals() сравнивает ее с текстовой строкой, содержащейся в объекте word2 (другом объекте класса Word):

>>> class Word():

...    def __init__(self, text):

...        self.text = text

...

...    def equals(self, word2):

...        return self.text.lower() == word2.text.lower()

...

Далее создадим три объекта Word с помощью трех разных текстовых строк:

>>> first = Word('ha')

>>> second = Word('HA')

>>> third = Word('eh')

Когда строки 'ha' и 'HA' сравниваются в нижнем регистре, они должны быть равными:

>>> first.equals(second)

True

Но строка 'eh' не совпадет со строкой 'ha':

>>> first.equals(third)

False

Мы определили метод equals() для преобразования строки в нижний регистр и сравнения. Но было бы неплохо, если б мы могли просто сказать first==second, как в случае встроенных типов Python. Реализуем такую возможность. Изменим имя метода equals() на особое имя __eq__() (вы узнаете, зачем я это сделал, через минуту):

>>> class Word():

...     def __init__(self, text):

...         self.text = text

...     def __eq__(self, word2):

...         return self.text.lower() == word2.text.lower()

...

Проверим, работает ли это:

>>> first = Word('ha')

>>> second = Word('HA')

>>> third = Word('eh')

>>> first == second

True

>>> first == third

False

Магия! Все, что нам было нужно, — указать особое имя метода для проверки на равенство __eq__(). В табл. 10.1 и 10.2 приведены имена самых полезных магических методов.

Таблица 10.1. Магические методы для сравнения

__eq__(self, other)

self == other

__ne__(self, other)

self != other

__lt__(self, other)

self < other

__gt__(self, other)

self > other

__le__(self, other)

self <= other

__ge__(self, other)

self >= other

Таблица 10.2. Магические методы для вычислений

__add__(self, other)

self + other

__sub__(self, other)

selfother

__mul__(self, other)

self * other

__floordiv__(self, other)

self // other

__truediv__(self, other)

self / other

__mod__(self, other)

self % other

__pow__(self, other)

self ** other

Не обязательно использовать такие математические операторы, как + (магический метод __add__()) и (магический метод __sub__()), только для работы с числами. Например, строковые объекты используют + для конкатенации и * для дуплицирования. Существует множество других методов, задокументированных онлайн по адресу . Наиболее распространенные из них представлены в табл. 10.3.

Таблица 10.3. Другие магические методы

__str__(self)

str(self)

__repr__(self)

repr(self)

__len__(self)

len(self)

Часто, помимо __init__(), вы будете применять и метод __str__(). Он нужен для того, чтобы выводить данные на экран. Этот метод используется методами print(), str() и строками форматирования, о которых вы можете прочитать в главе 5. Интерактивный интерпретатор применяет функцию __repr__() для того, чтобы выводить на экран переменные. Если вы не определите хотя бы один из этих методов, то увидите на экране ваш объект, преобразованный в строку по умолчанию:

>>> first = Word('ha')

>>> first

<__main__.Word object at 0x1006ba3d0>

>>> print(first)

<__main__.Word object at 0x1006ba3d0>

Добавим в класс Word методы __str__() и __repr__(), чтобы он лучше смотрелся:

>>> class Word():

...     def __init__(self, text):

...         self.text = text

...     def __eq__(self, word2):

...         return self.text.lower() == word2.text.lower()

...     def __str__(self):

...         return self.text

...     def __repr__(self):

...         return 'Word("'  self.text  '")'

...

>>> first = Word('ha')

>>> first          # используется __repr__

Word("ha")

>>> print(first)   # используется __str__

ha

Чтобы узнать о других специальных методах, обратитесь к документации Python ().

Агрегирование и композиция

Наследование может сослужить хорошую службу, когда вы хотите, чтобы класс-потомок бо'льшую часть времени вел себя как родительский класс (потомок является родителем). Возможность создавать иерархии наследования довольно заманчива, но иногда композиция или агрегирование имеет больше смысла. В чем разница? При композиции один объект является частью другого. Утка является птицей (наследование), но имеет хвост (композиция). Хвост не похож на утку — он является частью утки. В следующем примере создадим объекты bill и tail и предоставим их новому объекту duck:

>>> class Bill():

...     def __init__(self, description):

...         self.description = description

...

>>> class Tail():

...     def __init__(self, length):

...         self.length = length

...

>>> class Duck():

...     def __init__(self, bill, tail):

...         self.bill = bill

...         self.tail = tail

...     def about(self):

...         print('This duck has a', self.bill.description, 'bill and a',

                  self.tail.length, 'tail')

...

>>> а_tail = Tail('long')

>>> а_bill = Bill('wide orange')

>>> duck = Duck(а_bill, а_tail)

>>> duck.about()

This duck has a wide orange bill and a long tail

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

Когда использовать объекты, а когда — что-то другое

Рассмотрим несколько рекомендаций, которые помогут вам понять, где лучше разместить свой код и данные — в классе, в модуле (их мы рассмотрим в главе 11) или в чем-то совершенно ином.

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

• Классы, в отличие от модулей, поддерживают наследование.

• Если вам нужен только один объект, модуль может быть лучшим выбором. Независимо от того, сколько обращений к модулю имеется в программе, загрузится только одна копия. (Программистам на Java и С++: можете использовать модули в Python как синглтоны.)

• Если у вас есть несколько переменных, которые содержат разные значения и могут быть переданы в качестве аргументов в несколько функций, лучше всего определить их как классы. Например, вы можете использовать словарь с ключами size и color, чтобы представить цветное изображение. Вы можете создать разные словари для каждого изображения в программе и передавать их в качестве аргументов в функции scale() и transform(). Правда, по мере добавления новых ключей и функций разбираться со всем этим станет трудно. Поэтому более предпочтительно определить класс Image с атрибутами size или color и методами scale() и transform(). Тогда все данные и методы для работы с цветными изображениями будут определены в одном месте.

• Используйте самые простые решения. Словарь, список или кортеж проще, компактнее и быстрее, чем модуль, который, в свою очередь, проще, чем класс.

Совет от Гвидо ван Россума: «Избегайте усложнения структур данных. Кортежи лучше объектов (можно воспользоваться именованными кортежами). Предпочитайте простые поля функциям, геттерам и сеттерам. (Встроенные типы данных — ваши друзья.) Используйте больше чисел, строк, кортежей, списков, множеств, словарей. Взгляните также на библиотеку collections, особенно на класс deque».

Более новой альтернативой является класс данных, он рассматривается далее в этой главе.

Именованные кортежи

Поскольку Гвидо только что упомянул их, а я еще не говорил, самое время рассмотреть именованные кортежи. Именованный кортеж — это подкласс кортежей, с помощью которых вы можете получить доступ к значениям по имени (используя конструкцию .имя) и позиции (используя конструкцию [смещение]).

Рассмотрим пример из предыдущего раздела и преобразуем класс Duck в именованный кортеж, сохранив bill и tail как простые строковые аргументы. Функцию namedtuple можно вызвать, передав ей два аргумента:

имя;

строку, содержащую имена полей, разделенные пробелами.

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

>>> from collections import namedtuple

>>> Duck = namedtuple('Duck', 'bill tail')

>>> duck = Duck('wide orange', 'long')

>>> duck

Duck(bill='wide orange', tail='long')

>>> duck.bill

'wide orange'

>>> duck.tail

'long'

Именованный кортеж можно сделать также из словаря:

>>> parts = {'bill': 'wide orange', 'tail': 'long'}

>>> duck2 = Duck(**parts)

>>> duck2

Duck(bill='wide orange', tail='long')

В предыдущем коде обратите внимание на конструкцию **parts. Это аргумент —ключевоеслово. Он извлекает ключи и значения словаря parts и передает их в качестве аргументов в Duck(), что приводит к тому же эффекту, как и:

>>> duck2 = Duck(bill = 'wide orange', tail = 'long')

Именованные кортежи неизменяемы, но вы можете заменить одно или несколько полей и вернуть другой именованный кортеж:

>>> duck3 = duck2._replace(tail='magnificent', bill='crushing')

>>> duck3

Duck(bill='crushing', tail='magnificent')

Мы могли бы определить duck как словарь:

>>> duck_dict = {'bill': 'wide orange', 'tail': 'long'}

>>> duck_dict

{'tail': 'long', 'bill': 'wide orange'}

Вы можете добавить поля в словарь:

>>> duck_dict['color'] = 'green'

>>> duck_dict

{'color': 'green', 'tail': 'long', 'bill': 'wide orange'}

Но не в именованный кортеж:

>>> duck.color = 'green'

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: ' Duck' object has no attribute 'color'

Вспомним плюсы использования именованных кортежей.

Они выглядят и действуют как неизменяемый объект.

• Они более эффективны, чем объекты, с точки зрения времени и занимаемого места.

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

Вы можете использовать их как ключ словаря.

Классы данных

Многие люди создают объекты для хранения данных (с помощью атрибутов объектов), а не для задания определенного поведения (с помощью методов). Вы уже видели, как именованные кортежи могут стать альтернативным хранилищем данных. В Python 3.7 появились классы данных.

Перед вами простой старый объект, имеющий один атрибут name:

>> class TeenyClass():

...     def __init__(self, name):

...         self.name = name

...

>>> teeny = TeenyClass('itsy')

>>> teeny.name

'itsy'

Реализация той же функциональности с помощью классов данных будет выглядеть несколько иначе:

>>> from dataclasses import dataclass

>>> @dataclass

... class TeenyDataClass:

...     name: str

...

>>> teeny = TeenyDataClass('bitsy')

>>> teeny.name

'bitsy'

Помимо декоратора @dataclass, вам нужно определить атрибуты класса с помощью аннотаций переменных (/). Это выглядит как имя:тип или имя:тип=значение, например color:str или color:str="red". Типом может быть любой тип объектов в Python, включая созданные вами классы, а не только встроенные, такие как str или int.

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

>>> from dataclasses import dataclass

>>> @dataclass

... class AnimalClass:

...     name: str

...     habitat: str

...     teeth: int = 0

...

>>> snowman = AnimalClass('yeti', 'Himalayas', 46)

>>> duck = AnimalClass(habitat='lake', name='duck')

>>> snowman

AnimalClass(name='yeti', habitat='Himalayas', teeth=46)

>>> duck

AnimalClass(name='duck', habitat='lake', teeth=0)

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

Вы можете обращаться к атрибутам этого объекта точно так же, как и к атрибутам других объектов:

>>> duck.habitat

'lake'

>>> snowman.teeth

46

О классах данных можно говорить еще долго. Обратитесь к этому руководству (/) или прочтите официальную (довольно объемную) документацию (/).

attrs

Вы уже видели, как создаются классы и как в них добавляются атрибуты, а также как много текста для этого требуется набрать: надо определить метод-инициализатор __init__(), присвоить значения его аргументов атрибутам класса и создать методы, в имени которых присутствует двойное подчеркивание, например __str__(). Именованные кортежи и классы данных предлагают альтернативное решение, которое можно встретить в стандартной библиотеке, — если вам нужно создать простую коллекцию данных, проще всего воспользоваться этими вариантами.

В статье The One Python Library Everyone Needs () сравниваются простые классы, именованные кортежи и классы данных. В ней по множеству причин рекомендуется использовать сторонний пакет attrs (), который позволяет меньше набирать, меньше проверять данные и т.д. Взгляните сами и решите для себя, что вам больше нравится — эта библиотека или встроенные решения.

Читайте далее

В следующей главе мы обратимся к более высокому уровню структурирования кода — к модулям и пакетам.

Упражнения

10.1. Создайте класс Thing, не имеющий содержимого, и выведите его на экран. Затем создайте объект example этого класса и также выведите его. Совпадают ли выведенные значения?

10.2. Создайте новый класс с именем Thing2 и присвойте переменной letters значение 'abc'. Выведите на экран значение letters.

10.3. Создайте еще один класс, который, конечно же, называется Thing3. В этот раз присвойте значение 'xyz' атрибуту объекта letters. Выведите на экран значение атрибута letters. Понадобилось ли вам создавать объект класса, чтобы сделать это?

10.4. Создайте класс Element, имеющий атрибуты объекта name, symbol и number. Создайте объект этого класса со значениями 'Hydrogen', 'H' и 1.

10.5. Создайте словарь со следующими ключами и значениями: 'name':'Hydrogen', 'symbol':'H', 'number':1. Далее создайте объект с именем hydrogen класса Element с помощью этого словаря.

10.6. Для класса Element определите метод с именем dump(), который выводит на экран значения атрибутов объекта (name, symbol и number). Создайте объект hydrogen из этого нового определения и используйте метод dump(), чтобы вывести на экран его атрибуты.

10.7. Вызовите функцию print(hydrogen). В определении класса Element измените имя метода dump на __str__, создайте новый объект hydrogen и затем снова вызовите метод print(hydrogen).

10.8. Модифицируйте класс Element, сделав атрибуты name, symbol и number приватными. Определите свойство получателя для каждого атрибута, возвращающее его значение.

10.9. Определите три класса: Bear, Rabbit и Octothorpe. Для каждого из них определите всего один метод — eats(). Он должен возвращать значения 'berries' (для Bear), 'clover' (для Rabbit) или 'campers' (для Octothorpe). Создайте по одному объекту каждого класса и выведите на экран то, что ест указанное животное.

10.10. Определите три класса: Laser, Claw и SmartPhone. Каждый из них имеет только один метод — does(). Он возвращает значения 'disintegrate' (для Laser), 'crush' (для Claw) или 'ring' (для SmartPhone). Далее определите класс Robot, который содержит по одному объекту каждого из этих классов. Определите метод does() для класса Robot, который выводит на экран все, что делают его компоненты.

Мы это сделаем, даже если вы не хотите.

Вы встретите множество примеров двойных подчеркиваний в именах Python. Для экономии времени некоторые люди называют их dunder.

Недорогая и не очень качественная машина 1980-х годов.

У мула (mule) папа — осел, а мама — лошадь. У лошака (hinny) папа — конь, а мама — ослица.

Вы умеете хранить секреты? Я, кажется, нет.

Назад: Глава 9. Функции
Дальше: Глава 11. Модули, пакеты и программы

dofermerdat
этот. породы кроликов с фотографиями мясныефермер ру форум рассадакрупнейшие сельскохозяйственные районы зарубежной европыкрс мясо