Немного ООП-теории

Отрывок из книги «Практическое программирование, третье издание» Пола Грайса, Дженнифер Кэмпбелл, Джейсона Монтохо.

In this excerpt:
* Encapsulation
* Polymorphism
* Inheritance

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

Инкапсуляция

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

Полиморфизм

Полиморфизм означает «иметь более одной формы». В программировании это означает, что выражение, включающее переменную, может делать разные вещи в зависимости от типа объекта, на который ссылается переменная. Например, если obj относится к строке, то obj[1:3] создает двухсимвольную строку. С другой стороны, если obj относится к списку, то же выражение создает двухэлементный список. Точно так же выражение left + right может создать число, строку или список, в зависимости от типов левого и правого.

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

​ ​def​ non_blank_lines(thing):
​     ​"""Return the number of nonblank lines in thing."""​
​ 
​     count = 0
​     ​for​ line ​in​ thing:
​         ​if​ line.strip():
​             count += 1
​     ​return​ count

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

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

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

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

Вот пример. Допустим, мы управляем людьми в университете. Есть студенты и преподаватели. (Это грубое упрощение для иллюстрации наследования; мы игнорируем административный персонал, опекунов, поставщиков продуктов питания и так далее.)

И у студентов, и у преподавателей есть имена, почтовые адреса и адреса электронной почты; у каждого студента также есть номер студента, список пройденных курсов и список курсов, которые он или она посещает в настоящее время. У каждого преподавателя есть номер факультета и список курсов, которые он или она преподает в настоящее время. (Опять же, это упрощение.)

У нас будет класс Faculty и класс Student. Нам нужно, чтобы они оба имели имена, адреса и адреса электронной почты, но дублирование кода, как правило, плохо; поэтому мы избежим этого, также определив класс, возможно, с именем Member, и отслеживая эти функции в Member. Затем мы создадим Faculty и Student подклассы Member:

​ ​class​ Member:
​     ​""" A member of a university. """​
​ 
​     ​def​ __init__(self, name: str, address: str, email: str) -> None:
​         ​"""Create a new member named name, with home address and email address.​
​ ​        """​
​ 
​         self.name = name
​         self.address = address
​         self.email = email
​ 
​ ​class​ Faculty(Member):
​     ​""" A faculty member at a university. """​
​ 
​     ​def​ __init__(self, name: str, address: str, email: str,
​                  faculty_num: str) -> None:
​         ​"""Create a new faculty named name, with home address, email address,​
​ ​        faculty number faculty_num, and empty list of courses.​
​ ​        """​
​ 
​         super().__init__(name, address, email)
​         self.faculty_number = faculty_num
​         self.courses_teaching = []
​ 
​ 
​ ​class​ Student(Member):
​     ​""" A student member at a university. """​
​ 
​     ​def​ __init__(self, name: str, address: str, email: str,
​                  student_num: str) -> None:
​         ​"""Create a new student named name, with home address, email address,​
​ ​        student number student_num, an empty list of courses taken, and an​
​ ​        empty list of current courses.​
​ ​        """​
​ 
​         super().__init__(name, address, email)
​         self.student_number = student_num
​         self.courses_taken = []
​         self.courses_taking = []

Оба заголовка класса — класс Faculty(Member): и class Student(Member): — сообщают Python, что факультет и студент являются подклассами класса Member. Это означает, что они наследуют все атрибуты класса Member.

Первая строка как Faculty.__init__, так и Student.__init__ вызывает функцию super, которая создает ссылку на надклассовую часть объекта, Member. Это означает, что обе эти первые строки вызывают method __init__, унаследованный от класса Member. Обратите внимание, что мы просто передаем соответствующие параметры в качестве аргументов для этого вызова, как и при вызове любого метода.

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

​ ​>>>​​ ​​paul​​ ​​=​​ ​​Faculty(​​'Paul Gries'​​,​​ ​​'Ajax'​​,​​ ​​'[email protected]'​​,​​ ​​'1234'​​)​
​ ​>>>​​ ​​paul.name​
​ Paul Gries
​ ​>>>​​ ​​paul.email​
​ [email protected]
​ ​>>>​​ ​​paul.faculty_number​
​ 1234
​ ​>>>​​ ​​jen​​ ​​=​​ ​​Student(​​'Jen Campbell'​​,​​ ​​'Toronto'​​,​​ ​​'[email protected]'​​,​
​ ​...​​               ​​'4321'​​)​
​ ​>>>​​ ​​jen.name​
​ Jen Campbell
​ ​>>>​​ ​​jen.email​
​ [email protected]
​ ​>>>​​ ​​jen.student_number​
​ 4321

Оба объекта Faculty и Student унаследовали функции, определенные в классе Member.

Часто вам нужно расширить поведение, унаследованное от суперкласса. Например, мы можем написать метод a __str__ внутри класса Member:

​ ​def​ __str__(self) -> str:
​     ​"""Return a string representation of this Member.​
​ 
​ ​    >>> member = Member('Paul', 'Ajax', '[email protected]')​
​ ​    >>> member.__str__()​
​ ​    'Paul​​\\​​nAjax​​\\​​[email protected]'​
​ ​    """​
​ 
​     ​return​ ​'{}​​\n​​{}​​\n​​{}'​.format(self.name, self.address, self.email)

С добавлением этого метода в класс Member его наследуют как Faculty, так и Student:

​ ​>>>​​ ​​paul​​ ​​=​​ ​​Faculty(​​'Paul'​​,​​ ​​'Ajax'​​,​​ ​​'[email protected]'​​,​​ ​​'1234'​​)​
​ ​>>>​​ ​​str(paul)​
​ 'Paul\nAjax\[email protected]'
​ ​>>>​​ ​​print(paul)​
​ Paul
​ Ajax
​ [email protected]

Однако этого недостаточно: для класса Faculty мы хотим расширить то, что делает Member’s __str__, добавив номер факультета и список курсов, которые преподает преподаватель, а строка Student должна включать эквивалентную информацию о студенте.

Мы снова воспользуемся super, чтобы получить доступ к унаследованному методу Member.__str__ и добавить специфичную для Faculty информацию:

​ ​def​ __str__(self) -> str:
​     ​"""Return a string representation of this Faculty.​
​ 
​ ​    >>> faculty = Faculty('Paul', 'Ajax', '[email protected]', '1234')​
​ ​    >>> faculty.__str__()​
​ ​    'Paul​​\\​​nAjax​​\\​​[email protected]​​\\​​n1234​​\\​​nCourses: '​
​ ​    """​
​ 
​     member_string = super().__str__()
​ 
​     ​return​ ​'''{}​​\n​​{}​​\n​​Courses: {}'''​.format(
​         member_string,
​         self.faculty_number,
​         ​' '​.join(self.courses_teaching))

При этом мы получаем желаемый результат:

​ ​>>>​​ ​​paul​​ ​​=​​ ​​Faculty(​​'Paul'​​,​​ ​​'Ajax'​​,​​ ​​'[email protected]'​​,​​ ​​'1234'​​)​
​ ​>>>​​ ​​str(paul)​
​ 'Paul\nAjax\[email protected]\n1234\nCourses: '
​ ​>>>​​ ​​print(paul)​
​ Paul
​ Ajax
​ [email protected]
​ 1234
​ Courses:

Надеемся, вам понравился этот отрывок. Вы можете продолжить чтение Practical Programming, Third Edition Пола Грайса, Дженнифер Кэмпбелл и Джейсона Монтохо прямо на Medium:



Или купите книгу прямо с The Pragmatic Bookshelf. В рамках нашей Весенней распродажи 2023 года вы можете сэкономить 50 % на этой книге, используя код GROW2023 до полуночи по восточному времени 17 апреля 2023 года.



Чтобы получить печатную копию, посетите bookshop.org.