Введение в Python и обработку данных

Спецкурс кафедры Теоретической информатики мехмата МГУ

Подробная информация по курсу: http://rmbk.me/mm_python/

Лекция 8: ООП

Объектно-ориентированное программирование в Python

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

Три основные концепции ООП:

  • Инкапсуляция — это определение классов — пользовательских типов данных, объединяющих своё содержимое в единый тип и реализующих некоторые операции или методы над ним. Классы обычно являются основой модульности, инкапсуляции и абстракции данных в языках ООП.
  • Наследование — способ определения нового типа, когда новый тип наследует элементы (свойства и методы) существующего, модифицируя или расширяя их.
  • Полиморфизм — позволяет единообразно ссылаться на объекты различных классов (обычно внутри некоторой иерархии). Это делает классы ещё удобнее и облегчает расширение и поддержку программ, основанных на них.

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

В языке Python поддержаны эти концепции.

In [ ]:
# Классы
print(int)
print(list)
print(dict)

# Проверка принадлежности к классу
print(isinstance(12, int))
print(isinstance([1, 2], list))
print(isinstance({'a': 1}, dict))

Классы в Python

В Python можно создавать собственные классы. Названия им дают в CamelCase формате.

Синтаксис простого класса:

class ClassName:
    pass

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

In [ ]:
class Cat:
    """Документацию к классам можно писать как к функциям"""
    pass

class Dog:
    """Класс, описывающий собаку"""
    pass

# Можем создать экземпляр нашего класса с помощью "вызова" класса
c = Cat()
print('Экземпляр нашего класса:\n', c, end='\n\n')

# Можем создавать их много
a = []
for _ in range(5):
    curr_c = Cat()
    a.append(curr_c)
    
print('Много экземпляров:\n', a, end='\n\n')

# Можем вывести методы экземпляра нашего "пустого" класса
print('Методы экземпляра класса:\n', dir(c), end='\n\n')

Классы и экземпляры

Чтобы определять данные класса используют "магический" метод __init__.

Первым обязательным аргументом он принимает self — ссылку на созданный экземпляр класса. Следующие аргументы выбираем мы сами по необходимости — добавим аргумент name.

Далее в self, т.е. в наш текущий экземпляр, сохраним заданное имя. Другими словами, мы задаём атрибут name.

In [ ]:
class Cat:
    def __init__(self, name):
        self.name = name

cat = Cat('Пушок')
print(cat)
print(cat.name)

Давайте рассмотрим __str__, с его помощью можно изменить вывод print.

In [ ]:
class Cat:
    def __init__(self, name):
        self.name = name
    
    # Вызывается при методе str(cat), который используется внутри print
    def __str__(self):
        return self.name

cat = Cat('Пушок')
print(cat)
print(cat.name)

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

Атрибуты можно менять снаружи класса. Можно даже добавлять новые атрибуты снаружи, но так лучше не делать.

In [ ]:
cat = Cat('Пушок')
print(cat)

cat.name = 'Пушочек'
print(cat)

Общие данные для класса

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

Давайте считать количество созданных котиков.

In [ ]:
class Cat:
    count = 0
    
    def __init__(self, name, owner=None):
        self.name = name
        self.owner = owner or 'без хозяина'
        Cat.count += 1
        
    def __str__(self):
        return f'Cat(имя: {self.name}, хозяин: {self.owner})'
    
    def __del__(self):
        print('RIP:', self.name)
        Cat.count -= 1


a = Cat('Пушок')
b = Cat('Барсик', 'Дарья')
c = Cat(name='Леопольд', owner='Алексей')

print(a, b, c, sep='\n')
print()

print('Количество:', Cat.count)
# Можно и не от класса, а от экзепляра
# Тогда интерпретатор проверяет, что у него нет такого атрибута, и берет от класса
print('Количество через экземпляр:', a.count)
print()

# Объекты можно удалять через del, тогда предварительно вызывается __del__
# Однако нет гарантии, что он вызовется по завершении работы интерпретатора
# Поэтому лучше явно закрывать файлы, соединения и проч
del a
print('Количество:', Cat.count)

Методы

Методы это функции в классе, которые имеют доступ к экземпляру, от которого вызываются.

Они могут менять внутренние данные (состояние), выдавать какую-либо информацию и прочие задачи.

In [ ]:
class Human:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.pets = []
    
    def add_pet(self, pet):
        print(f'{self.name} приютил: {pet.name}')
        self.pets.append(pet)

class Cat:
    def __init__(self, name):
        self.name = name

a = Cat('Пушок')
b = Cat('Барсик')

pavel = Human('Павел', age=24)
alex = Human('Алексей', age=25)

pavel.add_pet(a)
alex.add_pet(b)

Внутренние данные классов

Концепция инкапсуляции преполагает еще защиту внутренних методов и атрибутов от внешних изменений вне предложенного интерфейса (private, protected и public методы и атрибуты в некоторых языках).

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

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

In [ ]:
class Human:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    def _say(self, text):
        print(text)
        
    def say_name(self):
        self._say(f"Привет, я {self._name}!")
        
    def say_how_old(self):
        self._say(f"Мне {self._age} лет.")

alex = Human(name='Алексей', age=23)
alex.say_name()
alex.say_how_old()
print()

# Не рекомендуется!
print(alex._name)
alex._say('Тыгыдык')

Property

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

Для начала рассмотрим предыдущий пример с необходимостью менять возраст у людей, но без property.

In [ ]:
class Human:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    def set_age(self, age_in):
        if 0 > age_in or age_in > 140:
            # Невалидные данные, не будем менять ничего
            print(f'Ошибка! Возраст не может быть: {age_in}')
            return
        self._age = age_in
        
    def say_name(self):
        print(f'Привет, я {self._name}!')
        
    def say_how_old(self):
        print(f'Мне {self._age} лет.')
        
        
pavel = Human('Павел', age=24)
pavel.say_name()
pavel.say_how_old()
print()

pavel.set_age(-545)
pavel.say_how_old()
print()

pavel.set_age(25)
pavel.say_how_old()
print()

A теперь с property:

In [ ]:
class Human:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, age_in):
        if 0 > age_in or age_in > 140:
            print(f'Ошибка! Возраст не может быть: {age_in}')
            return
        self._age = age_in
    
    @age.deleter
    def age(self):
        print(f'Ошибка! Возраст нельзя удалить')
        
    def say_name(self):
        print(f"Привет, я {self._name}!")
        
    def say_how_old(self):
        print(f"Мне {self._age} лет.")
        
        
pavel = Human('Павел', age=24)
pavel.say_name()
pavel.say_how_old()
print()

pavel.age = -545
pavel.say_how_old()
print()

pavel.age = 25
pavel.say_how_old()
print()

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

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

In [ ]:
class Human:
    def __init__(self, name):
        self.name = name
        
    @staticmethod
    def is_age_valid(age):
        return 0 <= age < 140

# К таким методам можно обращаться от класса
print(Human.is_age_valid(18))

# И от объектов
pavel = Human('Павел')
print(pavel.is_age_valid(143))

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

Наследование классов нужно для изменения поведения конкретного класса, а также расширения его функционала.

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

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

In [ ]:
class Animal:
    def __init__(self, name=None):
        self.name = name
        
    def say(self):
        raise NotImplementedError('Поведение не определено')
        
        
class Cat(Animal):
    def __init__(self, name, owner=None):
        super().__init__(name)
        self.owner = owner or 'без хозяина'
   
    def say(self):
        print(f'{self.name}: мяу')
        
        
class Cow(Animal):
    def __init__(self, name):
        super().__init__(name)
   
    def say(self):
        print(f'{self.name}: му-му')
        
animals = [
    Cat('Пушок', owner='Павел'),
    Cat('Сосиска'),
    Cow('Бурёнка'),
]

for animal in animals:
    animal.say()

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

In [ ]:
class A:
    pass
        
class B:
    pass

class C(A, B):
    pass

Работа с ошибками в Python

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

Давайте посмотрим на классическую ошибку. Мы видим ее путь исполнения (Traceback) и конкретную строку, приведшую к остановке.

In [ ]:
1 / 0

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

Давайте посмотрим на иерархию исключений в стандартной библиотеке Python. Все исключения наследуются от базового класса BaseException:

BaseException
    +-- SystemExit
    +-- KeyboardInterrupt
    +-- GeneratorExit
    +-- Exception
        +-- StopIteration
        +-- AssertionError
        +-- AttributeError
        +-- LookupError
            +-- IndexError
            +-- KeyError
        +-- OSError
        +-- SystemError
        +-- TypeError
        +-- ValueError

Например, KeyboardInterrupt вызывается при нажатии сочетания Ctrl+C в консоли.

Давайте попробуем пополучать разные типы ошибок.

In [ ]:
a = 'n42'

int(a)
In [ ]:
a = '42'
b = 1

a + b
In [ ]:
a = [1, 2]

a[2]
In [ ]:
a = {
    'A': 1
}

a['B']
In [ ]:
class A:
    pass

A.count

Обработка исключений

Чтобы "ловить" исключения в Python используются блоки try-except, в таком случае интерпретатор сможет продолжить работу.

In [ ]:
try:
    1 / 0
except:
    print('Возникла какая-то ошибка')

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

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

In [ ]:
try:
    1 / 0
except ZeroDivisionError:
    print('Возникла ошибка деления на ноль')

Также у блока try except может быть блок else. Блок else вызывается в том случае, если никакого исключения не произошло

In [ ]:
while True:
    try:
        raw = input('Введите число: ')
        number = int(raw)
    except ValueError:
        print('Некорректное значение!')
    else:
        break

Генерация исключений

In [ ]:
import os.path

filename = '/file/not/found'
try:
    if not os.path.exists(filename):
        raise ValueError('Файл не существует', filename)
except ValueError as err:
    print(err)
In [ ]:
# Задачка! Написать код по работе с API: http://www.marinespecies.org/rest/