Объектно-ориентированное программирование
Введение
Очевидно, на базовом синтаксисе знакомство с python не заканчивается, следующий шаг в изучении python это ООП. Объектно-ориентированное программирование отдельный подход к программированию, основанный на создании объектов и работе с ними. Программы написанные с использованием ООП легче расширять и модифицировать в данном блоке убедимся в этом. Попробуем разобраться во всем просто и последовательно. И, конечно, начать стоит со знакомства с классами.
Классы
Объектно-ориентированное программирование построено на классах и на концепциях, или принципах, взаимодействия этих классов.
class SomeClass: pass
Создание класса напоминает создание функции, прописываем ключевое слово class после него пишем название класса и ставим двоеточие. Класс создан.
>>> class Computer: ... operation_system = 'Linux' ... type_of_computer = 'Laptop' ... RAM = 4 ... >>> Computer.
__file__={str}'<input>' __name__={str}'__main__' ... Computer={type}<class'__main__.Computer'> RAM={int}4 operation_system={str}'Linux' type_of_computer={str}'Laptop' sys={module}<module 'sys' (built-in)>
Разумеется пустой класс нам не очень интересен. Переименуем наш класс в Computer и создадим три его атрибута: операционная система, тип устройства и операционная память. Для обращения к атрибутам класса проинициализируем его в консоли для наглядности. Таким образом мы можно сказать зарегистрировали наш класс, а вместе с ним и его атрибуты. Теперь к ним можно обратиться. Делается это также, как мы делали с разными методами разных типов данных. Пишем название класса и через точку нам будут показаны все доступные атрибуты этого класса. Список будет состоять из встроенный методов и трех переменных, объявленных нами. Класс Computer с его атрибутами появился среди прочих специальных переменных.
>>> class Computer: ... operation_system = 'Linux' ... type_of_computer = 'Laptop' ... RAM = 4 ... >>> Computer.operation_system 'Linux' >>> Computer.type_of_computer = 'PC'
__file__={str}'<input>' __name__={str}'__main__' ... Computer={type}<class'__main__.Computer'> RAM={int}4 operation_system={str}'Linux' type_of_computer={str}'PC' sys={module}<module 'sys' (built-in)>
К атрибутам класса можно обратиться, а также можно их изменить. Присвоим type_of_computer новое значение. Мы видим это изменение в области Special Variables.
>>> class Computer: ... operation_system = 'Linux' ... type_of_computer = 'Laptop' ... RAM = 4 ... >>> ex_1 = Computer() >>> ex_2 = Computer() >>> ex_1.RAM = 8 >>> ex_2.operation_system = 'Windows' >>> Computer.type_of_computer = 'PC' >>> Computer.RAM = 3
RAM = {int}8 operation_system = {str}'Linux' type_of_computer = {str}'PC'
RAM = {int}3 operation_system = {str}'Windows' type_of_computer = {str}'PC'
__file__={str}'<input>' __name__={str}'__main__' ... Computer={type}<class'__main__.Computer'> RAM={int}3 operation_system={str}'Linux' type_of_computer={str}'PC' sys={module}<module 'sys' (built-in)>
Мы можем создавать экземпляры классов. Каждый из экземпляров будет ссылаться на оригинальный класс и изменения атрибутов экземпляра класса никак не повлияют на атрибуты оригинального класса. При этом изменения совершаемые над атрибутами в оригинальном классе повлияют на атрибуты его экземпляров, если они не были явно изменены ранее.
>>> class Computer: ... operation_system = 'Linux' ... type_of_computer = 'Laptop' ... RAM = 4 ... >>> Computer.type_of_computer 'Laptop' >>> Computer.ram Traceback (most recent call last): File "/usr/lib/python3.9/code.py", line 90, in runcode exec(code, self.locals) File "<input>", line 1, in <module> AttributeError: type object 'Computer' has no attribute 'ram' >>> getattr(Computer, 'ram', 'error') 'error' >>> getattr(Computer, 'RAM', 'error') 4 >>> getattr(Computer, 'RAM') 4
Мы знаем, что обратиться к атрибуту класса можно через точку, но если мы решим обратиться к несуществующему атрибуту, то получим ошибку. Для обращения к атрибутам классов существует функция getattr(). Первым аргументом функция getattr() принимает имя объекта, вторым имя атрибута, а третьим значение по умолчанию, которое будет возвращено, если переданного имени атрибута в пространстве этого объекта не существует.
>>> class Computer: ... operation_system = 'Linux' ... type_of_computer = 'Laptop' ... RAM = 4 ... >>> ex_1 = Computer() >>> Computer.price = 10000 >>> setattr(Computer, 'color', 'black') >>> setattr(Computer, 'price', 15000)
RAM = {int}4 color = {str}'black' operation_system = {str}'Windows' price = {int}15000 type_of_computer = {str}'PC'
__file__={str}'<input>' __name__={str}'__main__' ... Computer={type}<class'__main__.Computer'> RAM={int}4 color={str}'black' operation_system={str}'Linux' price={int}15000 type_of_computer={str}'Laptop' sys={module}<module 'sys' (built-in)>
Для создания нового атрибута и редактирования уже существующих атрибутов существует функция setattr(). Первый аргумент - имя объекта, второй - имя атрибута, третий - значение, устанавливаемое в качестве нового значения для уже существующего атрибута и в качестве первого значения для несуществующего.
>>> class Computer: ... operation_system = 'Linux' ... type_of_computer = 'Laptop' ... RAM = 4 ... >>> delattr(Computer, 'RAM') >>> delattr(Computer, 'RAM') Traceback (most recent call last): File "/usr/lib/python3.9/code.py", line 90, in runcode exec(code, self.locals) File "<input>", line 1, in <module> AttributeError: RAM >>> hasattr(Computer, 'type_of_computer') True >>> hasattr(Computer, 'RAM') False
__file__={str}'<input>' __name__={str}'__main__' ... Computer={type}<class'__main__.Computer'> operation_system={str}'Linux' type_of_computer={str}'Laptop' sys={module}<module 'sys' (built-in)>
Функция delattr() удаляет атрибут, первый аргумент - имя объекта, второй - имя атрибута.
Функция hasattr() возвратит True, если атрибут есть в пространстве объекта, а False - если нет, первый аргумент - имя объекта, второй - имя атрибута.
Параметр self
class Computer: operation_system = 'Linux' type_of_computer = 'Laptop' RAM = 4 def info(): print('Информация о моем компьютере') print(Computer.info()) ex = Computer() print(ex.info())
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_1.py", line 14, in <module> print(ex.info()) TypeError: info() takes 0 positional arguments but 1 was given Информация о моем компьютере None Process finished with exit code 1
! Method must have a first parametr, usually called 'self' 6
Внутри классов мы можем объявлять методы, в данном случае функция info(). При такой записи pycharm показывает нам одну критическую ошибку. Pycharm подсказывает, что метод класса должен иметь обязательный параметра, который обычно называется self. Мы можем обратиться к методу класса, но не можем обратиться к этому же методу у экземпляра этого класса. Ошибка подсказывает, что передано ноль аргументов, а должен быть передан один. Тут нам и нужен параметр self. Этот параметр нужен для взаимодействия с экземплярами класса.
class Computer: operation_system = 'Linux' type_of_computer = 'Laptop' RAM = 4 def info(self): print('Информация о моем компьютере' + str(self)) ex = Computer() ex_2 = Computer() ex.info() ex_2.info()
Информация о моем компьютере<__main__.Computer object at 0x7fec71592c10> Информация о моем компьютере<__main__.Computer object at 0x7fec71592c40> Process finished with exit code 0
Параметр self, как видно из результата, ссылается на уникальную ячейку памяти выделенную для каждого экземпляра класса.
class Computer: operation_system = 'Linux' type_of_computer = 'Laptop' RAM = 4 def info(self, price, color): self.price = price self.color = color print(f"{price} {color}") ex = Computer() ex_2 = Computer() ex.info(5000, 'black') ex_2.info(10000, 'white')
5000 black 10000 white Process finished with exit code 0
! Instance attribute price defined outside __init__ 7 ! Instance attribute color defined outside __init__ 8
Теперь через этот параметр мы можем создавать или менять локальные свойства экземпляров класса. Благодаря параметру self интерпретатор понимает к какому экземпляру мы обращаемся. Но pycharm опять подсказывает, что мы сделали что-то неправильно. А именно говорит, что не хватает некого инициализатора __init__.
__init__, __new__, __del__
Методы обрамленные двумя нижними подчеркивании с двух сторон называются магическими или методами перегрузки. Их достаточно много и мы постепенно с ними познакомимся. Мы затрагивали пару таких методов в разделе по базовому синтаксису, а теперь познакомимся с ними подробнее. И начнем с метода __init__.
class Computer: def __init__(self, os, top, ram=4): self.operation_system = os self.type_of_computer = top self.RAM = ram def info(self): return f"Мой ПК - {self.type_of_computer}, {self.operation_system}, {self.RAM}" ex = Computer('linux', 'desktop', 8) ex_2 = Computer('windows', 'notebook') print(ex.info()) print(ex_2.info())
Мой ПК - desktop, linux, 8 Мой ПК - notebook, windows, 4 Process finished with exit code 0
Перепишем наши переменные внутри инициализатора __init__. Метод __init__ выполняет роль конструктора класса. Для атрибутов метода __init__ можно прописать значения по умолчанию и тогда можно не прописывать их при создании экземпляра класса, а вот значения атрибутов без значения по умолчанию прописывать придется обязательно. Обратится к этим атрибутам можно внутри любого метода данного класса. А благодаря параметру self перед названием атрибута интерпретатор поймет к какому экземпляру класса относится данный атрибут.
class Computer: def __init__(self, os, top, ram=4): self.operation_system = os self.type_of_computer = top self.RAM = ram self.color = 'black' def info(self): return f"Мой ПК - {self.type_of_computer}, {self.operation_system}, {self.RAM}" def computer_color(self): return f"{self.color}" ex = Computer('linux', 'desktop', 8) ex_2 = Computer('windows', 'notebook') print(ex.info()) print(ex_2.info()) print(ex.computer_color())
Мой ПК - desktop, linux, 8 Мой ПК - notebook, windows, 4 black Process finished with exit code 0
Атрибуты можно не писать внутри параметров самого __init__, ничего не мешает прописать его явно непосредственно в теле __init__, и, конечно, значение такого атрибута при создании экземпляра класса поменять не выйдет.
Можно подумать, что создание экземпляра класса начинается с метода __init__, но на самом деле существует еще один метод, который вызывается перед методом __init__ и вообще перед созданием экземпляра класса. Это еще один магический метод - метод __new__. Если углубляться в процесс создания экземпляра класса, то станет ясно, что настоящий конструктор это все-таки метод __new__, а __init__ это, как было сказано выше, инициализатор. Метод __new__ создает экземпляр класса и далее передает этот экземпляр в параметр self метода __init__. Для более гибкого понимания в каких ситуациях этот метод может быть полезен нужно познакомиться с концепцией наследования и функцией super(). Дело в том, что обычно наследования от класса object достаточно для конструирования экземпляров классов. Но вернемся к этому вопросу позже.
class Computer: # def __new__(cls, *args, **kwargs): # print("Конструктор" + str(cls)) def __init__(self, os, top, ram=4): self.operation_system = os self.type_of_computer = top self.RAM = ram self.color = 'black' def info(self): return f"Мой ПК - {self.type_of_computer}, {self.operation_system}, {self.RAM}" def computer_color(self): return f"{self.color}" def __del__(self): print("Удаление экземпляра" + str(self)) ex = Computer('linux', 'desktop', 8) ex_2 = Computer('windows', 'notebook') print(ex.info()) print(ex_2.info()) print(ex.computer_color())
Мой ПК - desktop, linux, 8 Мой ПК - notebook, windows, 4 black Удаление экземпляра<__main__.Computer object at 0x7f4bd1fb9040> Удаление экземпляра<__main__.Computer object at 0x7f4bd1fb9100> Process finished with exit code 0
Метод __new__ принимает 3 параметра, cls - обязательный параметр, который ссылается на текущий экземпляр класса, и коллекции *args и **kwargs, нужные для принятия произвольного количества аргументов передаваемых при создании экземпляра класса. В данном случае я закомментировал метод __new__, потому что в таком виде он ничего не возвращает, мы увидим лишь его print() при запуске этой программы, а остальные команды выполнены не будут, поскольку метод __new__, который ничего не возвращает, не создает экземпляр класса. Этот пример нужен лишь для знакомства с синтаксисом этого метода. Но в этом примере есть еще один нерассмотренный ранее метод - метод __del__. Если __init__ - инициализатор, то __del__ - финализатор. __del__ вызывается после того как на объект перестают ссылаться все внешние ссылки. Метод __del__ финальный метод, который будет вызван перед удалением экземпляра класса. При чем помещать метод __del__ необязателен в конце, он может быть вызван в любом месте тела класса.
Приватные и Публичные методы и атрибуты
Класс Computer уже поднадоел, давайте создадим новый.
class BankCard: def __init__(self, card_id, cvv, name='Ivan Ivanov', end_date='24.05.2025'): self.id = card_id self.cvv = cvv self.name = name self.end_date = end_date def card_info(self): return f"Card {self.id} owner: {self.name}, end {self.end_date}, cvv {self.cvv}" card_1 = BankCard('4200 3800 5000 4000', 200) card_2 = BankCard('4200 5800 3000 2000', 250) print(card_1.card_info()) print(card_2.cvv) print(card_2.id)
Card 4200 3800 5000 4000 owner: Ivan Ivanov, end 24.05.2025, cvv 200 250 4200 5800 3000 2000 Process finished with exit code 0
Класс BankCard, который будет хранить информацию, по которой можно идентифицировать любую банковскую карту. Сейчас все атрибуты и методы этого класса являются публичными. Это значит, что мы можем обратиться к методу и получить информацию о карте, а также мы можем обратиться к любому атрибуту экземпляра класса напрямую, даже не используя метод. Конечно, для данных, доступ к которым не хотелось бы предоставлять для любого человека, такое поведение класса не совсем подходящее. Для таких данных и существует возможность создания приватных методов и атрибутов.
class BankCard: def __init__(self, card_id, cvv, name='Ivan Ivanov', end_date='24.05.2025'): self.__id = card_id self.__cvv = cvv self.name = name self.end_date = end_date def card_info(self): return f"Card {self.id} owner: {self.name}, end {self.end_date}, cvv {self.cvv}" card_1 = BankCard('4200 3800 5000 4000', 200) card_2 = BankCard('4200 5800 3000 2000', 250) print(card_1.card_info()) print(card_2.cvv) print(card_2.id)
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_2.py", line 14, in <module> print(card_1.card_info()) File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_2.py", line 9, in card_info return f"Card {self.id} owner: {self.name}, end {self.end_date}, cvv {self.cvv}" AttributeError: 'BankCard' object has no attribute 'id' Process finished with exit code 1
Для того, чтобы сделать атрибут приватным, достаточно поставить перед его именем два нижних подчеркивания. Теперь мы не можем обратиться ни к атрибуту, ни к методу, где используются приватные атрибуты, при этом к публичным атрибутам мы по прежнему можем обращаться.
class BankCard: def __init__(self, card_id, cvv, name='Ivan Ivanov', end_date='24.05.2025'): self.__id = card_id self.__cvv = cvv self.name = name self.end_date = end_date def card_info(self): return f"Card {self.__id} owner: {self.name}, end {self.end_date}, cvv {self.__cvv}" card_1 = BankCard('4200 3800 5000 4000', 200) card_2 = BankCard('4200 5800 3000 2000', 250) print(card_1.card_info()) print(card_2.__cvv) print(card_2.__id)
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_2.py", line 16, in <module> print(card_2.__cvv) AttributeError: 'BankCard' object has no attribute '__cvv' Card 4200 3800 5000 4000 owner: Ivan Ivanov, end 24.05.2025, cvv 200 Process finished with exit code 1
Укажем явно внутри метода на право использования приватных методов и тогда получить приватную информацию будет возможно, но только при обращении к соответственному методу. Обратиться к приватным переменным напрямую, минуя метод, по-прежнему будет невозможно, даже написав перед выводом два нижних подчеркивания. Тем самым наши данные становятся инкапсулированными, т.е. скрытыми в нашем методе.
class BankCard: def __init__(self, card_id, cvv, name='Ivan Ivanov', end_date='24.05.2025'): self.__id = card_id self.__cvv = cvv self.name = name self.end_date = end_date def card_info(self): return f"Card {self.__id} owner: {self.name}, end {self.end_date}, cvv {self.__cvv}" def __private_info(self): return f"id {self.__id}, cvv {self.__cvv}" def see_private_info(self): print(self.__private_info()) card_1 = BankCard('4200 3800 5000 4000', 200) card_2 = BankCard('4200 5800 3000 2000', 250) card_2.see_private_info() print(card_2.__private_info())
id 4200 5800 3000 2000, cvv 250 Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_2.py", line 23, in <module> print(card_2.__private_info()) AttributeError: 'BankCard' object has no attribute '__private_info' Process finished with exit code 1
Приватный метод создается также - два нижних подчеркивания перед именем. К такому методу нельзя обратиться вне класса, но можно обратиться внутри класса. Таким образом метод __private_info ничего не вернет, а вот метод see_private_info, внутри которого мы обратимся к нашему приватному методу без проблем выведет его содержимое.
class BankCard: def __init__(self, card_id, cvv, name='Ivan Ivanov', end_date='24.05.2025'): self._id = card_id self._cvv = cvv self.name = name self.end_date = end_date def card_info(self): return f"Card {self._id} owner: {self.name}, end {self.end_date}, cvv {self._cvv}" def _private_info(self): return f"id {self._id}, cvv {self._cvv}" def see_private_info(self): print(self._private_info()) card_1 = BankCard('4200 3800 5000 4000', 200) card_2 = BankCard('4200 5800 3000 2000', 250) card_2.see_private_info() print(card_1._id) print(card_2._cvv)
id 4200 5800 3000 2000, cvv 250 4200 3800 5000 4000 250 Process finished with exit code 0
Существует еще один уровень приватности. Он называется - protected. Как видно из примера к таким атрибутам и методам мы можем обращаться так же как к публичным, ничего этому не препятствует, кроме устного предупреждения от pycharm. Приватность такого уровня используется для разработчиков, написав одно нижнее подчеркивание мы указываем другим программистам, которые будут по какой-то причине пользоваться нашим кодом, что эти методы и атрибуты считаются приватными.
Property
Мы ранее обращались к функциям getattr(), setattr(), delattr(). Теперь немного улучшим эти функции.
class CarPrice: def __init__(self, name, price): self.name = name self.__price = price def get_price(self): return self.__price def set_price(self, value): self.__price = value def del_price(self): del self.__price car_1 = CarPrice('audi', 300000) print(car_1.get_price()) car_1.set_price(200000) print(car_1.get_price()) car_1.del_price() print(car_1.get_price())
300000 200000 Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_2.py", line 21, in <module> print(car_1.get_price()) File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_2.py", line 7, in get_price return self.__price AttributeError: 'CarPrice' object has no attribute '_CarPrice__price' Process finished with exit code 1
Создадим класс CarPrice, у которого будет два атрибута, публичный атрибут name и приватный атрибут price. Создадим три метода для взаимодействия с приватным атрибутом, get_price, set_price, del_price. Теперь мы можем вызывать, переопределять и удалять данный атрибут, но каждый раз оперировать тремя методами для таких простых действий выглядит достаточно нагромождено. Для решения этой проблемы существует функция property().
class CarPrice: def __init__(self, name, price): self.name = name self.__price = price def get_price(self): return self.__price def set_price(self, value): self.__price = value def del_price(self): del self.__price price = property(fget=get_price, fset=set_price, fdel=del_price) car_1 = CarPrice('audi', 300000) print(car_1.price) car_1.price = 400000 print(car_1.price) del car_1.price
300000 400000 Process finished with exit code 0
Передадим наши методы в соответствующие атрибуты функции property(). Теперь ко всем трем свойствам мы можем обращаться через одно ключевое слово, в нашем случае слово price. Согласитесь это выглядит удобнее. Но можно представить эту запись еще компактнее.
class CarPrice: def __init__(self, name, price): self.name = name self.__price = price @property def price(self): return self.__price @price.setter def price(self, value): self.__price = value @price.deleter def price(self): del self.__price # price = property() # price = price.getter(get_price) == price = property(price) # price = price.setter(set_price) == price = price.setter(price) # price = price.deleter(del_price) == price = price.deleter(price) car_1 = CarPrice('audi', 300000) print(car_1.price) car_1.price = 400000 print(car_1.price) del car_1.price
300000 400000 Process finished with exit code 0
В первую очередь посмотрим на закомментированные строки. Свойство property можно представить таким образом, а далее для избежания конфликта имен и превращения этого свойства в декоратор используем для каждого метода одно имя, price в нашем случае. Метод get будет являться главным, опишем его как декоратор @property, остальные два метода декорируем относительно главного декоратора. Таким образом мы получили достаточно гибкий и элегантный способ работы с приватными атрибутами.
class Dollar: def __init__(self, dollar, dollar_course=77): self.__dol = dollar self.__dol_course = dollar_course self.__my_balance = None @property def dol(self): return self.__dol @dol.setter def dol(self, value): self.__dol = value self.__my_balance = None @property def my_rub_balance(self): if self.__my_balance is None: self.__my_balance = self.__dol * self.__dol_course return self.__my_balance vasya = Dollar(300) print(vasya.my_rub_balance) vasya.dol = 500 print(vasya.my_rub_balance)
23100 38500 Process finished with exit code 0
Посмотрим еще один пример использования property. Создадим класс, который будет принимать курс доллара и количество долларов, а возвращать в своем единственном методе эквивалент этого значения в рублях. Начнем с метода my_rub_balance, он будет производить вычисление каждый раз при его выводе, даже в том случае, если количество и курс не изменился, это не самое хорошее поведение, ресурсы расходуются на вычисление результата, который уже известен. Для контролирования этого момента создадим проверку. Определим атрибут my_balance и присвоим ей значение None. Таким образом получается, если баланс ранее не был вычислен мы его вычислим, если был, то просто вернем его. Но отсюда вытекает новая проблема, изменение свойства не сбрасывает значение атрибута my_balance. Для решения этой проблемы обернем атрибут dol в декораторы @property (или @getter) и @setter, при чем внутри сеттера будем каждый раз сбрасывать значение атрибута my_balance. Таким образом мы реализовали решение данной задачи таким образом, чтобы текущий баланс сохранялся в случае неизменности данных и благодаря этому каждый раз не тратились бы ресурсы устройства на вычисление одного и того же действия.
Паттерн 'Моносостояние'
>>> class Table: ... width = 1000 ... height = 500 ... color = 'black' ... >>> table_1 = Table() >>> table_1.width = 1200 >>> table_1.__dict__ {'width': 1200} >>> Table.__dict__ mappingproxy({'__module__': '__main__', 'width': 1000, 'height': 500, 'color': 'black', '__dict__': <attribute '__dict__' of 'Table' objects>, '__weakref__': <attribute '__weakref__' of 'Table' objects>, '__doc__': None})
color = {str}'black' height = {int}500 width = {int}1200
__file__={str}'<input>' __name__={str}'__main__' ... Table={type}<class'__main__.Table'> color={str}'black' height={int}500 width={int}1000 sys={module}<module 'sys' (built-in)>
Напоминаю, атрибуты создаваемые внутри класса будут присвоены всем атрибутам этого класса, но изменение какого-то атрибута повлияет только на локальные свойства этого конкретного атрибута, но никак не повлияет на другие экземпляры этого класса. Паттерн 'Моносостояние' решает эту проблему. Метод __dict__, с которым мы ранее не знакомились, выводит список всех атрибутов класса, а также его атрибутов. Воспользуемся этой информацией для реализации данного паттерна.
>>> class Table: ... __shared_attrs = { ... 'width': 1000, ... 'height': 500, ... 'color': 'black' ... } ... ... def __init__(self): ... self.__dict__ = self.__shared_attrs ... >>> table_1 = Table() >>> table_1.color = 'white' >>> table_2 = Table() >>> table_2.width = 1500
color = {str}'white' height = {int}500 width = {int}1500
color = {str}'white' height = {int}500 width = {int}1500
__file__={str}'<input>' __name__={str}'__main__' ... Table={type}<class'__main__.Table'> color={str}'white' height={int}500 width={int}1500 sys={module}<module 'sys' (built-in)>
Реализуем его следующим образом. Создадим приватную переменную, внутрь которой поместим словарь с атрибутами нашего класса. А после в инициализаторе __init__ присвоим этот словарь методу __dict__ параметра self. Теперь любое изменение любого атрибута любого экземпляра класса повлияет на этот атрибут как в самом классе, так и в каждом его экземпляре.
classmethod и staticmethod
Ранее мы писали самые обычные методы класса, они принимали обязательный параметр self, который является ссылкой на экземпляр класса, но это не единственный вариант определения методов класса.
import math import random class Calculate: PI = math.pi def __init__(self, x): self.x = 30 if self.random(x, 100) > 50: self.x = x print(self.x, self.square_circle(x)) def square_x(self): return self.x ** 2 @staticmethod def random(a, b): return random.randint(a, b) @classmethod def square_circle(cls, r): return cls.PI * (r ** 2) ex_1 = Calculate(4) print(ex_1.square_x()) ex_2 = Calculate(20) print(ex_2.square_x()) print(Calculate.square_circle(15)) print(ex_2.random(4, 100))
30 50.26548245743669 900 20 1256.6370614359173 400 706.8583470577034 6 Process finished with exit code 0
Создадим класс Calculate, у которого будет один собственный атрибут PI равный числу пи. Внутри инициализатора мы создадим один атрибут x, который по умолчанию будет равен 30. На строки 10-13 пока не обращаем внимания. Метод square_x() обычный метод, который возвращает квадрат числа. А далее прописаны два новых метода.
Статические методы создаются при помощи декоратора @staticmethod. Наверное вы уже заметили, что внутри него нет обязательного параметра self и при этом pycharm не видит в такой записи никакой ошибки. Статические методы существуют самостоятельно, они не ссылаются ни на какие экземпляры. Вызвать такие методы можно как через сам класс, так и через его экземпляры и независимо от способа вызова в такой метод нужно передать обязательные параметры, если таковые требуются. Статический метод можно вызывать внутри других методов этого класса. Так например вызовем наш статический метод random(), который возвращает случайное число в диапазоне от 'a' до 'b', и в качестве 'a' будем передавать в него наш атрибут 'x', а в качестве 'b' число 100. Проверка будет заключаться в том, что если при переданном 'x' рандомное число в диапазоне 'x' - 100 будет выше 50, то мы используем 'x', в противном случае используем число по умолчанию, т.е. 30. Так, например, в первом экземпляре передадим число 4 и увидим, что программа в качестве 'x' взяла число 30, это означает, что число из диапазона 4 - 100, выпавшее в результате проверки, больше 50. Во втором экземпляре ситуация противоположная, число из диапазона 20 - 100 оказалось меньше 50, поэтому мы использовали в качестве 'x' число 20.
Следующий метод - square_circle. Это метод класса, о чем говорит декоратор @classmethod. Методы класса можно вызывать напрямую от класса. Параметр cls, с которым мы уже сталкивались, как раз является ссылкой на класс, в нашем случае на Calculate. Методы класса можно вызывать напрямую через класс, т.е. не передавать ссылку на экземпляр класса. Такой метод может обращаться к атрибутам класса, а вот к локальным атрибутам методов класса, например к атрибуту 'x', обратиться не выйдет. И возвращать этот конкретный метод класса будет площадь круга. Так же как и статический метод мы можем вызвать метод класса внутри другого метода. Так, например, будем вызывать его каждый раз при создании каждого нового экземпляра класса, при чем возвращать он будет площадь посчитанную для той переменной 'x', которая передана при создании экземпляра, а не той, которая будет выбрана в результате проверки.
Также обращаю ваше внимание, названия self и cls, для ссылок на экземпляр и на класс, необязательно должны называться так, это названия принятые сообществом, ничего не мешает заменить их на любое другое, их функционал и назначение от этого не изменятся.
__str__, __repr__
Начнем постепенно знакомиться с наиболее ключевыми магическими методами. По другому их называют dunder методы, от английского Double UNDERscore - двойное нижнее подчеркивание.
class Language: def __init__(self, name): self.name = name ex_1 = Language('Python') print(ex_1)
<__main__.Language object at 0x7f5e3539ec10> Process finished with exit code 0
Начнем знакомство с методов для отображения экземпляров класса. Посмотрим на класс Language, если распечатать атрибут функцией print(), то мы увидим не очень дружелюбное для понимания имя.
>>> class Language: ... def __init__(self, name): ... self.name = name ... ... def __repr__(self): ... return f"Экземпляр класса Language" ... ... def __str__(self): ... return f"Я выбрал язык - {self.name}" >>> ex_1 = Language('Python') >>> ex_1 Экземпляр класса Language >>> print(ex_1) Я выбрал язык - Python
ex_1={Language}Я выбрал язык - Python
Метод __repr__ изменит отображение этого имени в отладочном режиме. Поэтому метод __repr__ используется для разработчиков.
Метод __str__ изменит отображение для пользователей.
__len__, __abs__
Просто так применять функции len() и abs() к экземплярам класса не выйдет, нужно сначала определить эти методы.
class LenWord: def __init__(self, word): self.word = word def __len__(self): return len(self.word) class SegmentLen: def __init__(self, a, b): self.a = a self.b = b def __abs__(self): return abs(self.b - self.a) ex_word = LenWord('slovo') print(ex_word.__len__()) ex_len = SegmentLen(10, 50) print(ex_len.__abs__())
5 40 Process finished with exit code 0
Метод __len__ соответственно вернет длину объекта.
А метод __abs__ вернет модуль числа, или говоря по другому, его абсолютное значение.
__add__, __mul__, __sub__, __truediv__
Основным арифметическим операциям тоже соответствуют магические методы.
class Group: def __init__(self, group_count): self.count = group_count def __add__(self, other): if isinstance(other, Group): return self.count + other.count if isinstance(other, (int, float)): return self.count + other def __mul__(self, other): if isinstance(other, Group): return self.count * other.count if isinstance(other, (int, float)): return self.count * other def __sub__(self, other): if isinstance(other, Group): return self.count - other.count if isinstance(other, (int, float)): return self.count - other def __truediv__(self, other): if isinstance(other, Group): return self.count / other.count if isinstance(other, (int, float)): return self.count / other group_1 = Group(50) group_2 = Group(40) print(group_1.__add__(group_2)) # == group_1 + group_2 print(group_2.__add__(100)) # == group_2 + 100 print(group_2 + 200) # == group_2.__add__(200) print(group_1.__mul__(group_2)) # == group_1 * group_2 print(group_1.__sub__(group_2)) # == group_1 - group_2 print(group_2.__sub__(group_1)) # == group_2 - group_1 print(group_2.__truediv__(group_1)) # == group_2 / group_1
90 140 240 2000 10 -10 0.8 Process finished with exit code 0
Метод __add__ для сложения.
Метод __mul__ для умножения.
Метод __sub__ для вычитания.
Метод __truediv__ для деления.
В каждом методе будем проверять является ли переменная 'other' целым или вещественным числом для арифметических операций с числами. А вторя проверка нужна для совершения арифметических операций над экземплярами. Реализовав эти четыре метода мы теперь можем совершать все четыре операции над экземплярами класса Group. При чем порядок записи экземпляров не важен.
Методы сравнения
Просто так сравнивать между собой экземпляры тоже не выйдет.
class Group: def __init__(self, group_count): self.count = group_count def __eq__(self, other): ex = other.count if isinstance(other, Group) else other return self.count == ex def __lt__(self, other): ex = other.count if isinstance(other, Group) else other return self.count < ex def __le__(self, other): ex = other.count if isinstance(other, Group) else other return self.count <= ex group_1 = Group(50) group_2 = Group(40) print(group_1 == group_2) print(group_1 == 50) print(group_1 != group_2) print(group_1 > group_2) print(group_1 < group_2) print(group_1 <= group_2) print(group_1 >= group_2)
False True True True False False True Process finished with exit code 0
Метод __eq__ для оператора ==.
Метод __ne__ для оператора !=.
Метод __lt__ для оператора <.
Метод __gt__ для оператора >
Метод __le__ для оператора <=.
Метод __ge__ для оператора >=.
Возвращают операторы сравнения True или False. Возможно вы обратили внимание, что в классе не реализован метод __ne__, __gt__ и __ge__, но тем не менее операции !=, >, и >= возвращают верный результат. Дело в том, что python понимает каким методом воспользоваться, так например, если метод __ne__ не реализован python попробует поискать метод __eq__ и если найдет его, то проведет инверсную проверку относительно сравнения. Таким образом достаточно в программе реализовать три оператора сравнения и их инверсные пары также станут доступны для использования.
__hash__
Мы уже касались понятия хэш, когда говорили о словарях. Если вспомнить, словари в своей реализации используют хэш-таблицы, поэтому словари работают очень быстро. Функция hash() вычисляет хэш объекта, но только неизменяемого объекта. И как мы помним в качестве ключа словаря мы можем использовать только неизменяемые объекты.
print(hash('slovo')) print(hash((1, 2, 'abc'))) print(hash((1, 2, 'abc')))
859905052852229039 5049973204321871429 5049973204321871429 Process finished with exit code 0
Хэш значение для одного и того же неизменяемого объекта всегда равно. Откуда можно сделать вывод, если объекты равны, то и их хэш значения равны.
class Group: def __init__(self, group_count): self.count = group_count group_1 = Group(50) group_2 = Group(50) print(group_1 == group_2)
False Process finished with exit code 0
Создадим два одинаковых экземпляра одного класса и сравним их. Получим достаточно ожидаемый результат - False. Дело в том, что таким сравнением мы ссылки на ячейку памяти выделенную под конкретный экземпляр и они конечно разные. Для сравнения содержимого атрибута требуется определить уже знакомый метод __eq__. Но сначала посмотрим на хэш этих экземпляров.
class Group: def __init__(self, group_count): self.count = group_count group_1 = Group(50) group_2 = Group(50) print(hash(group_1)) print(hash(group_2))
8765914553793 8765914553784 Process finished with exit code 0
Хэши вычисляются и хэши разные. Это говорит нам еще и том, что экземпляр класса считается неизменяемым объектом.
class Group: def __init__(self, group_count): self.count = group_count def __eq__(self, other): return self.count == other.count group_1 = Group(50) group_2 = Group(50) print(group_1 == group_2) print(hash(group_1)) print(hash(group_2))
True Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_10.py", line 14, in <module> print(hash(group_1)) TypeError: unhashable type: 'Group' Process finished with exit code 1
После реализации __eq__ экземпляры становятся равны, а вот вычисление хэша уже становится невозможным. Мы получаем ошибку, говорящую, что экземпляр класса нехэшируемый тип данных. Тут нам и пригодится метод __hash__.
class Group: def __init__(self, group_count): self.count = group_count def __eq__(self, other): return self.count == other.count def __hash__(self): return hash(self.count) class ForHash: def __init__(self, a, b): self.a = a self.b = b def __eq__(self, other): return self.a == other.a and self.b == other.b def __hash__(self): return hash((self.a, self.b)) group_1 = Group(50) group_2 = Group(50) print(group_1 == group_2) print(hash(group_1)) print(hash(group_2)) ex_1 = ForHash(5, 10) ex_2 = ForHash(5, 10) print(ex_1 == ex_2) print(hash(ex_1)) print(hash(ex_2))
True 50 50 True 7622224536684080658 7622224536684080658 Process finished with exit code 0
Метод __hash__ позволяет вычислять хэш экземпляра класса. Значения хэша целых чисел равны этому же числу, поэтому для наглядности я создал еще один класс с таким же поведением, но который принимает кортеж из двух чисел. И как видно помимо того что хэш вычисляется, так теперь это значение еще и одинаковое в случае равенства экземпляров.
__bool__
Как мы помним любой тип данных в python является объектом, а любой объект можно отнести к одному из логических типов - True либо False. Любой значимый объект, будь то не пустой список, не пустой словарь или, например, не число ноль относятся к логическому типу True, остальные к типу False.
class Params: def __init__(self, height, width): self.h = height self.w = width ex_1 = Params(200, 300) ex_2 = Params(0, 0) print(bool(ex_1)) print(bool(ex_2))
True True Process finished with exit code 0
Применение функции bool() к любому экземпляру вернет True. Даже если передать в качестве аргументов нули. Дело в том, что пока в классе явно не определено поведение метода __bool__, то для отнесения объекта к логическому типу используется метод __len__, этот метод возвращает True, если длина объекта больше нуля.
class Params: def __init__(self, height, width): self.h = height self.w = width def __bool__(self): return self.h != 0 or self.w != 0 ex_1 = Params(200, 300) ex_2 = Params(0, 0) print(bool(ex_1)) print(bool(ex_2))
True False Process finished with exit code 0
Определим метод __bool__, в котором будем проверять является ли значение нулем и в случае хотя бы одного нуля возвращать False.
__call__
Метод __call__ вызывается при вызове класса. Например, когда мы создаем экземпляр класса таким образом: название_экземпляра_класса = название_класса(). Именно круглые скобки в конце названия класса говорят о его выводе и именно в этот момент срабатывает метод __call__. И грубо говоря внутри метода __call__ вызывается метод __new__ и метод __init__. Но вот вызвать экземпляр класса через круглые скобки не выйдет. Тут нам и пригодится метод __call__.
class Callable: def __init__(self, name): self.name = name self.count = 0 def __call__(self, *args, **kwargs): print('Вызов экземпляра', self.name) self.count += 1 ex_1 = Callable('first') ex_1() print(ex_1.count) ex_1() ex_2 = Callable('second') ex_2() ex_2() ex_2() ex_2() print(ex_2.count)
Вызов экземпляра first 1 Вызов экземпляра first Вызов экземпляра second Вызов экземпляра second Вызов экземпляра second Вызов экземпляра second 4 Process finished with exit code 0
Создадим класс Callable и будем внутри создавать экземпляры, у которых будет название и в методе __call__ будем считать количество вызовов каждого экземпляра. При чем работать эти счетчики будут абсолютно независимо. Принимает метод __call__ произвольное количество символов.
class Callable: def __init__(self, name): self.name = name self.count = 0 def __call__(self, *args, **kwargs): print('Вызов экземпляра', self.name) return self.name.title() ex_1 = Callable('first') print(ex_1()) ex_2 = Callable('second') print(ex_2())
Вызов экземпляра first First Вызов экземпляра second Second Process finished with exit code 0
Зачем это может пригодиться? Раз метод __call__ вызывается сразу при вызове экземпляра класса, то мы можем описать внутри него какое-то поведение применимое к каждому вызову экземпляра. Например, пусть каждый раз к имени экземпляра применяется метод title().
__setattr__, __getattribute__, __getattr__, __delattr__
class Params: def __init__(self, height, width): self.h = height self.w = width # def __getattr__(self, item): # return f"__getattr__ {self} - {item}" def __getattribute__(self, item): return f"__getattribute__ {self} - {item}" def __setattr__(self, key, value): print(f'__setattr__ {key} = {value}') def __delattr__(self, item): print(f'__delattr__ {item}') ex_1 = Params(100, 200) ex_2 = Params(300, 400) print(ex_2.w) print(ex_1.f) del ex_2.h
__setattr__ h = 100 __setattr__ w = 200 __setattr__ h = 300 __setattr__ w = 400 __getattribute__ <__main__.Params object at 0x7fdeb3f7fa60> - w __getattribute__ <__main__.Params object at 0x7fdeb3f7fb20> - f __delattr__ h Process finished with exit code 0
Метод __setattr__ вызывается, когда мы устанавливаем новое значение для какого-нибудь атрибута.
Метод __getattribute__ вызывается, когда мы обращаемся к какому-нибудь атрибуту.
Метод __delattr__ вызывается, когда мы удаляем какой-нибудь атрибут.
class Params: def __init__(self, height, width): self.h = height self.w = width def __getattr__(self, item): return f"__getattr__ {self} - {item}" # def __getattribute__(self, item): # return f"__getattribute__ {self} - {item}" def __setattr__(self, key, value): print(f'__setattr__ {key} = {value}') def __delattr__(self, item): print(f'__delattr__ {item}') ex_1 = Params(100, 200) ex_2 = Params(300, 400) print(ex_2.w) print(ex_1.f) del ex_2.h
__setattr__ h = 100 __setattr__ w = 200 __setattr__ h = 300 __setattr__ w = 400 __getattr__ <__main__.Params object at 0x7fdeb3f7fa60> - w __getattr__ <__main__.Params object at 0x7fdeb3f7fb20> - f __delattr__ h Process finished with exit code 0
Метод __getattr__ на первый взгляд работает точно так же как и __getattribute__, за одним исключением __getattribute__ срабатывает при любом обращении к любому атрибуту любого экземпляра, в то время как __getattr__ срабатывает только при обращении к несуществующему атрибуту. В случае обращения к существующему атрибуту мы увидим просто значение этого атрибута. Но ведь атрибут 'w' существует, так почему же мы не видим 400? На самом деле в данном примере атрибут 'w' не существует, в этом можно убедиться применив к этому экземпляру метод __dict__.
class Params: def __init__(self, height, width): self.h = height self.w = width def __getattr__(self, item): return f"__getattr__ {self} - {item}" # def __getattribute__(self, item): # return f"__getattribute__ {self} - {item}" def __setattr__(self, key, value): print(f'__setattr__ {key} = {value}') def __delattr__(self, item): print(f'__delattr__ {item}') ex_1 = Params(100, 200) ex_2 = Params(300, 400) print(ex_2.__dict__) print(ex_2.w) print(ex_1.f) del ex_2.h
__setattr__ h = 100 __setattr__ w = 200 __setattr__ h = 300 __setattr__ w = 400 {} __getattr__ <__main__.Params object at 0x7f6a874d2a90> - w __getattr__ <__main__.Params object at 0x7f6a874d2b50> - f __delattr__ h Process finished with exit code 0
Почему словарь пустой? Потому что __setattr__ ничего не возвращает. Поведение __setattr__ в данном примере описано не совсем правильно.
class Params: def __init__(self, height, width): self.h = height self.w = width def __getattr__(self, item): return f"__getattr__ {self} - {item}" # def __getattribute__(self, item): # return f"__getattribute__ {self} - {item}" def __setattr__(self, key, value): print(f'__setattr__ {key} = {value}') return object.__setattr__(self, key, value) def __delattr__(self, item): print(f'__delattr__ {item}') ex_1 = Params(100, 200) ex_2 = Params(300, 400) print(ex_2.__dict__) print(ex_2.w) print(ex_1.f) del ex_2.h
__setattr__ h = 100 __setattr__ w = 200 __setattr__ h = 300 __setattr__ w = 400 {'h': 300, 'w': 400} 400 __getattr__ <__main__.Params object at 0x7f2b51b77040> - f __delattr__ h Process finished with exit code 0
Для корректной работы этого метода нужно обращение к классу object, от которого наследуются все классы python. Теперь __setattr__ работает корректно и атрибуты теперь действительно присвоены экземпляру. И, следовательно, метод __getattr__ теперь тоже работает корректно. С классом object и его назначением мы подробнее познакомимся, когда будем говорить о наследовании.
__getitem__, __setitem__, __delitem__
В качестве атрибута мы можем передать коллекцию, к элементам которой можно обращаться по индексу и также через индекс изменять их.
class Languages: def __init__(self, *args): self.l_list = list(args) ex_l = Languages('Python', 'Java', 'C++', "JavaScript", 'PHP') print(ex_l[2])
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_14.py", line 7, in <module> print(ex_l[2]) TypeError: 'Languages' object is not subscriptable Process finished with exit code 1
Пусть класс Languages принимает список языков программирования и если мы захотим обратиться по индексу к элементу этого списка, то получим ошибку.
class Languages: def __init__(self, *args): self.l_list = list(args) def __getitem__(self, item): return self.l_list[item] def __setitem__(self, key, value): self.l_list[key] = value def __delitem__(self, key): del self.l_list[key] ex_l = Languages('Python', 'Java', 'C++', 'JavaScript', 'PHP') print(ex_l[2]) ex_l[2] = 'C#' print(ex_l[2]) del ex_l[2] print(ex_l[2])
C++ C# JavaScript Process finished with exit code 0
Методы __getitem__, __setitem__ и __delitem__ добавят это поведение в наш класс, добавят обращение по индексу, изменение по индексу и удаление по индексу соответственно.
__iter__, __next__
Говоря о списках возникает вопрос, можно ли по этому списку пройтись в цикле for. Ну раз существуют методы __iter__ и __next__, то очевидно, что без реализации этих методов попытка применить к списку цикл for вернет ошибку.
class Languages: def __init__(self, *args): self.l_list = list(args) ex_l = Languages('Python', 'Java', 'C++', 'JavaScript', 'PHP') for item in ex_l: print(item)
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_15.py", line 26, in <module> for item in ex_l: TypeError: 'Languages' object is not iterable Process finished with exit code 1
Добавим методы __iter__ и __next__.
class Languages: def __init__(self, *args): self.l_list = list(args) def __iter__(self): return iter(self.l_list) ex_l = Languages('Python', 'Java', 'C++', 'JavaScript', 'PHP') for item in ex_l: print(item)
Python Java C++ JavaScript PHP Process finished with exit code 0
Добавим в наш класс метод __iter__, внутри которого сделаем список итератором. Теперь применение к нему цикла for вернет поочередно каждый элемент списка.
class Languages: def __init__(self, *args): self.l_list = list(args) self.index = 0 def __next__(self): item = self.l_list[self.index] self.index += 1 return item ex_l = Languages('Python', 'Java', 'C++', 'JavaScript', 'PHP') print(ex_l.__next__()) print(ex_l.__next__()) print(ex_l.__next__()) print(ex_l.__next__()) print(ex_l.__next__())
Python Java C++ JavaScript PHP Process finished with exit code 0
А вот так можно реализовать то же самое поведение, но уже через метод __next__, правда при такой реализации мы упираемся в длину списка, и если в данном примере еще раз вызвать print(ex_l.__next__()), то мы получим ошибку, говорящую, что итератор закончился. И на самом деле эту ошибку можно достаточно просто обработать, но мы этого пока не умеем.
__pos__, __neg__, __invert__
Унарные, то есть применяемые к одному операнду. Так например унарный минус изменит положительно значение на отрицательное, т.е. подставит перед ним минус.
>>> a = 5 >>> b = 10 >>> b = -b >>> b -10 >>> a = -a >>> a -5 >>> a + b -15
Унарные операции реализуются просто подставлением необходимого знака перед числом.
class Numbers: def __init__(self, a, b): self.a = a self.b = b def __pos__(self): return +self.b def __neg__(self): return -self.b ex_1 = Numbers(5, -10) print(ex_1.__pos__()) print(ex_1.__neg__())
-10 10 Process finished with exit code 0
Метод __pos__ - унарный плюс.
Метод __neg__ - унарный минус.
Унарный плюс примененный к отрицательному числу все-равно вернет отрицательное число, потому что минус 'сильнее' плюса. Минус на плюс дает минус. А вот применение унарного минуса к отрицательному числу вернет положительное число, ведь минус на минус дает плюс.
class Numbers: def __init__(self, a, b): self.a = a self.b = b # def __pos__(self): # return +self.b # # def __neg__(self): # return -self.b def __invert__(self): self.a, self.b = self.b, self.a return self.a, self.b ex_1 = Numbers(5, -10) print(ex_1.__invert__())
(-10, 5) Process finished with exit code 0
Множественно присвоение в классах можно реализовать, например, с помощью метода __invert__.
__round__, __floor__, __ceil__, __trunc__
Округление в python необязательно реализовывать с помощью магических методов.
import math class Rounding: def __init__(self, a): self.a = a def __round__(self, n=None): # == def round(self): return round(self.a, n) # return round(self.a) def __floor__(self): # == def floor(self): == def floor(self): return math.floor(self.a) # return int(self.a) return math.float(self.a) def __ceil__(self): # == def ceil(self): == def ceil(self): return math.ceil(self.a) # return int(self.a) return math.ceil(self.a) def __trunc__(self): # == def trunc(self): == def trunc(self): return math.trunc(self.a) # return int(self.a) + 1 return math.trunc(self.a) ex_1 = Rounding(3.14159) print(ex_1.__round__(3)) print(ex_1.__floor__()) print(ex_1.__ceil__()) print(ex_1.__trunc__())
3.142 3 4 3 Process finished with exit code 0
Методы __round__, __floor__, __ceil__ и __trunc__ - это 'подкапотная' реализация тех методов для округления, которыми мы пользуемся.
Оставшиеся методы арифметических операций
Осталось еще несколько арифметических операций, которые мы не разобрали.
class Number: def __init__(self, x): self.x = x ex_1 = Number(20) ex_2 = Number(5) print(ex_1 // ex_2) print(ex_1 % ex_2) print(ex_1 ** ex_2)
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_18.py", line 8, in <module> print(ex_1 // ex_2) TypeError: unsupported operand type(s) for //: 'Number' and 'Number' Process finished with exit code 1
Речь идет о целочисленном делении, остатке от деления и возведении в степень.
class Number: def __init__(self, x): self.x = x def __floordiv__(self, other): if isinstance(other, Number): return self.x // other.x def __mod__(self, other): if isinstance(other, Number): return self.x % other.x def __pow__(self, power, modulo=None): if isinstance(power, Number): return self.x ** power.x ex_1 = Number(20) ex_2 = Number(5) print(ex_1 // ex_2) print(ex_1 % ex_2) print(ex_1 ** ex_2) print(Number.__pow__(ex_1, ex_1, ex_1))
4 0 3200000 104857600000000000000000000 Process finished with exit code 0
Метод __floordiv__ для целочисленного деления.
Метод __mod__ для остатка от деления.
Метод __pow__ для возведения в степень, по умолчанию имеет третий необязательный аргумент.
__lshift__, __rshift__
Функция shift() в python работает в сторону увеличения и уменьшения, используются для этого lshift() и rshift() соответственно. Что делает эта функция лучше увидеть на примере.
class Number: def __init__(self, x): self.x = x def __lshift__(self, other): if isinstance(other, int): return self.x << other def __rshift__(self, other): if isinstance(other, int): return self.x >> other ex_1 = Number(20) print(ex_1 << 1) print(ex_1 << 2) print(ex_1 << 3, '\n') print(ex_1 >> 1) print(ex_1 >> 2) print(ex_1 >> 3)
40 80 160 10 5 2 Process finished with exit code 0
Метод __lshift__ образует последовательность (x) - ((x) * 2) - (((x) * 2) * 2) - ((((x) * 2) * 2) * 2) и так далее.
Метод __rshift__ образует обратную последовательность, причем округление деления происходит в меньшую сторону.
Такие операции называют бинарными сдвигами. Поскольку сначала происходит преобразование цифры к ее бинарному эквиваленту, сдвигает этот бинарный эквивалент на указанное количество знаков, а затем преобразует получившееся значение обратно к числовому и возвращает его в виде ответа. Так, например, бинарный \эквивалент числа 20 это 10100 применяем к этому числу оператор << тем самым смещаем число на один знак влево и получаем 101000, переводим 101000 обратно к десятичной системе счисления и получаем 40.
Бинарное И, ИЛИ, ИЛИ НЕТ
Раз уж коснулись бинарных сдвигов, то следует упомянуть еще о трех бинарных операциях.
class Number: def __init__(self, x): self.x = x def __or__(self, other): return self.x | other.x def __xor__(self, other): return self.x ^ other.x def __and__(self, other): return self.x & other.x ex_1 = Number(13) ex_2 = Number(10) print(ex_1 | ex_2) print(ex_1 ^ ex_2) print(ex_2 & ex_1)
15 7 8 Process finished with exit code 0
Метод __and__ отвечает за бинарное И.
Метод __or__ отвечает за бинарное ИЛИ.
Метод __xor__ отвечает за бинарное ИЛИ НЕТ.
Принцип работы такой же, переводим число из десятичной системы в двоичную и проводим над ним соответствующую операцию.
Так, например, 13 в двоичной системе счисления это 1101, а 10 - 1010. Бинарное И сложит эти числа, получив тем самым 1111, что в переводе в двоичную систему равняется 15.
Отраженные операторы. Составное присваивание. Остальные магические методы
У перечисленным выше бинарных и арифметических операциях есть отраженные операторы. В начале каждого из них можно вначале написать букву r, так например в методе __add__(self, other) мы в качестве первого операнда выбираем self, а в качестве второго other, в случае __radd__ все происходит ровно наоборот. Применение этим методам найти можно редко.
Также ко всем этим операторам можно добавить вначале букву i. Эта буква отвечает за составное присваивание так часто используемое в python. Так, например, тот же метод __add__(self, other) складывает переменные классическим методом, то есть self + other, и результат в таком случае возвращается новым значением и никакая из переменных не изменяется, а в случае метода __iadd__ сложение происходит присваиванием, а именно self += other, что можно представить как self = self + other.
Мы разобрали еще не все магические методы, остались методы преобразования типов, например, метод __int__ для преобразования значения к целочисленному значению, думаю с ними совсем все очевидно, если будет нужда в этих методах в каком-нибудь примере в будущем мы обязательно прибегнем к ним и посмотрим на их работу.
Методы __enter__ и __exit__ - методы менеджера контекста, а именно with open() as, думаю еще будет момент для рассмотрения их работы, но если говорить об этих методах сейчас метод __enter__ срабатывает при запуске работы менеджера контекста with, а __exit__ отрабатывает когда все необходимые операции над файлом уже выполнены и он готов к закрытию, или выполнении функции close().
Методы __copy__ и __deepcopy__ используются для копирования классов, с той разницей, что __copy__ копирует класс, но изменения скопированного класса влияют на изменения оригинального, а в случае __deepcopy__ не влияют.
Встроенные функции isinstance() и issubclass(), с которой мы еще не знакомы, тоже имеют свои эквиваленты среди магических методов __instancecheck__ и __subclasscheck__ соответственно.
И последняя группа магических методов, это магические методы для сериализации, этой темы мы коснемся много позже.
Дескрипторы
В некоторых магических методах происходило дублирование кода, например, в тех местах, где мы делали проверку принадлежности переменной other к типу класса. Дублирование функционально одинакового кода, но для разных переменных, выглядит неправильно, такое написание противоречит правилу dry(don't repeat yourself). Дескрипторы помогают решить проблему этого нагромождения и описать универсальный интерфейс для функционально одинакового кода.
Проблему дублирования фрагмента кода с проверкой на принадлежность переменной other к классу можно решить и без дескрипторов. Например, вспомним пример из раздела __add__, __mul__, __sub__, __truediv__, одинаковая проверка дублируется 4 раза, посмотрим как можно это изменить.
class Group: def __init__(self, group_count): self.count = group_count @classmethod # @staticmethod def is_true(cls, other): # def is_true(other): if type(other) != Group: return f"не подходит" # raise TypeError("не подходит") def __add__(self, other): self.is_true(other) return self.count + other.count def __mul__(self, other): self.is_true(other) return self.count * other.count def __sub__(self, other): self.is_true(other) return self.count - other.count def __truediv__(self, other): self.is_true(other) return self.count / other.count group_1 = Group(50) group_2 = Group(40) print(group_1 + group_2) print(group_1 * group_2) print(group_1 - group_2) print(group_1 / group_2)
90 2000 10 1.25 Process finished with exit code 0
Например, можно эту проверку снести в обычный метод класса, где будем совершать проверку other на принадлежность ее к экземпляру класса. И перед каждой операцией просто совершать эту проверку. Правда работать такая проверка будет работать корректно, если применить инструкцию raise, а не return, но с raise мы по-прежнему не знакомы, такая реализация использована просто для наглядности. Можно и не использовать метод класса, но тогда pycharm возмутится и скажет, что нужен либо метод класса, либо статический метод, один из этих вариантов нужно использовать поскольку с помощью таких методов можно обращаться к такому методу не через экземпляр класса, а напрямую.
class Group: def __init__(self, group_count_1, group_count_2): self.count_1 = group_count_1 self.count_2 = group_count_2 @classmethod def is_true(cls, other): if type(other) != (int, float): return f"не подходит" @property def changes_1(self): return self.count_1 @changes_1.setter def changes_1(self, other): self.is_true(other) self.count_1 = other @property def changes_2(self): return self.count_2 @changes_2.setter def changes_2(self, other): self.is_true(other) self.count_2 = other group_1 = Group(10, 20) print(group_1.__dict__) group_1.count_1 = 30 print(group_1.__dict__)
{'count_1': 10, 'count_2': 20} {'count_1': 30, 'count_2': 20} Process finished with exit code 0
Оставим эту проверку, только проверять будем теперь не на принадлежность экземпляру класса, а на принадлежность к целочисленному или вещественному типу данных, но при этом изменим функциональность самого класса. Пусть класс Group принимает теперь два значения, и мы хотим просто обращаться к ним, и изменять их. Property в этом поможет, тут нет ничего нового мы уже это умеем. Но тут сразу в глаза бросается все то же повторение кода, а это класс, у которого всего два атрибута, а что если бы их было больше. Конечно, в python есть решение для данной ситуации. Сама идея дескрипторов заключается в создании стороннего класса, внутри которого весь этот функционально одинаковый код будет приведен к единому интерфейсу. Реализуем такой класс.
class Descriptor: @classmethod def is_true(cls, other): if type(other) != int: return 'Не подходит' def __set_name__(self, owner, name): self.name = name def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): self.is_true(value) instance.__dict__[self.name] = value class Group: count_1 = Descriptor() count_2 = Descriptor() def __init__(self, group_count_1, group_count_2): self.count_1 = group_count_1 self.count_2 = group_count_2 group_1 = Group(10, 20) print(group_1.__dict__) group_1.count_1 = 30 print(group_1.__dict__) print(group_1.count_1)
{'count_1': 10, 'count_2': 20} {'count_1': 30, 'count_2': 20} 30 Process finished with exit code 0
Начиная с версии python 3.6 у нас появился магический метод __set_name__, внутри которого мы присваиваем локальные имена атрибутов экземпляров класса и инициализируем локальную переменную self.name. self в этом случае это ссылка на создаваемый внутри класса Group экземпляр, owner ссылка на сам этот класс Group, а name это имя переменной, которой присвоен данный экземпляр, в нашем случае такими переменными являются count_1 и count_2. Метод __set_name__ класса Descriptor срабатывает сразу при создании экземпляров count_1 и count_2, следующим шагом происходит инициализация методом __init__ уже внутри класса Group и во время присвоения значений экземпляра класса Group вызывается метод __set__ непосредственно класса Descriptor. self в этом случае это также ссылка на соответствующий экземпляр класса Descriptor, instance это ссылка на соответствующий экземпляр класса Group, а value соответствующее значение экземпляра класса Group. Как мы знаем словарь __dict__ хранит информацию о локальных свойствах экземпляра класса, поэтому мы обратимся к этому словарю через переменную instance и для значения self.name, внутри которого у нас хранится имя переменной, присвоим новое значение value. Поскольку переменная name хранит имя, в нашем случае она хранит имена count_1 и count_2, то новое значение value будет присвоено не имени name, а имени присвоенному переменной name, именно поэтому вызов метода __dict__ возвращает не {'name': 10, 'name': 20}, а {'count_1': 10, 'count_2': 20}. Ну и если мы хотим считать значение какой-то конкретной переменной какого-то экземпляра, то будет использоваться метод __get__ класса Descriptor, где self это по-прежнему ссылка на экземпляр класса Descriptor, instance ссылка на экземпляр класса Group, а owner ссылка на сам класс Group. И обращаясь к необходимому имени через словарь __dict__ мы просто возвращаем значение этого имени.
class Descriptor: @classmethod def is_true(cls, other): if type(other) != int: raise TypeError('Неправильный тип данных') def __set_name__(self, owner, name): self.name = name def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): self.is_true(value) instance.__dict__[self.name] = value def __delete__(self, instance): del instance.__dict__[self.name] class Group: count_1 = Descriptor() count_2 = Descriptor() count_3 = Descriptor() count_4 = Descriptor() count_5 = Descriptor() def __init__(self, group_count_1, group_count_2, group_count_3, group_count_4, group_count_5): self.count_1 = group_count_1 self.count_2 = group_count_2 self.count_3 = group_count_3 self.count_4 = group_count_4 self.count_5 = group_count_5 group_1 = Group(10, 20, 30, 40, 50) print(group_1.__dict__) group_1.count_1 = 30 print(group_1.__dict__) print(group_1.count_1) del group_1.count_4 print(group_1.__dict__)
{'count_1': 10, 'count_2': 20, 'count_3': 30, 'count_4': 40, 'count_5': 50} {'count_1': 30, 'count_2': 20, 'count_3': 30, 'count_4': 40, 'count_5': 50} 30 {'count_1': 30, 'count_2': 20, 'count_3': 30, 'count_5': 50} Process finished with exit code 0
И теперь реализация такого поведения для класса принимающего 5 параметров получилась гораздо компактнее, чем получилась бы без использования дескрипторов. Также реализуем метод __delete__, теперь взаимодействие с экземплярами классов и их атрибутами выглядит полноценно. И заменим return на raise, для корректной отработки обработки неверно переданных данных.
Наследование
Концепция наследования подразумевает наследование атрибутов и методов родительского класса классом потомком. Сымитируем проблему.
class Computer: def __init__(self, oc, ram, color): self.oc = oc self.RAM = ram self.color = color def about(self): return f"Computer {self.oc}, {self.RAM}, {self.color}" class Laptop: def __init__(self, oc, ram, color): self.oc = oc self.RAM = ram self.color = color def about(self): return f"Laptop {self.oc}, {self.RAM}, {self.color}" ex_computer = Computer('linux', 8, 'black') ex_laptop = Laptop('linux', 8, 'white') print(ex_computer.about()) print(ex_laptop.about())
Computer linux, 8, black Laptop linux, 8, white Process finished with exit code 0
Допустим мы собираем информацию о стационарных компьютерах и ноутбуках, но информацию об этих устройствах мы собираем одинаковую. Интуитивно напрашивается вынесение дублирующегося кода за два этих класса и дальнейшее объединение этого кода.
class PC: def __init__(self, oc, ram, color): self.oc = oc self.RAM = ram self.color = color class Computer(PC): def about(self): return f"Computer {self.oc}, {self.RAM}, {self.color}" class Laptop(PC): def about(self): return f"Laptop {self.oc}, {self.RAM}, {self.color}" ex_computer = Computer('linux', 8, 'black') ex_laptop = Laptop('linux', 8, 'white') print(ex_computer.about()) print(ex_laptop.about())
Computer linux, 8, black Laptop linux, 8, white Process finished with exit code 0
Создадим родительский класс PC, для того, чтобы наследоваться от какого-нибудь класса, нужно у дочернего класса открыть скобки после названия и написать туда название родительского класса. Обратите внимание создание экземпляров по прежнему происходит через классы Computer и Laptop, а не через класс PC, конечно, это нужно для того, чтобы параметр self понимал на какой класс ссылаться и уникальными свойствами какого класса в последствии пользоваться.
super()
А теперь представим, что у ноутбуков и стационарных компьютеров будут какие-то уникальные свойства, например цена и время эксплуатации соответственно.
class PC: def __init__(self, oc, ram, color): self.oc = oc self.RAM = ram self.color = color class Computer(PC): def __init__(self, oc, ram, color, operating_time=3): super().__init__(oc, ram, color) self.ot = operating_time def about(self): return f"Computer {self.oc}, {self.RAM}, {self.color}, {self.ot}" class Laptop(PC): def __init__(self, oc, ram, color, price=70000): super().__init__(oc, ram, color) self.price = price def about(self): return f"Laptop {self.oc}, {self.RAM}, {self.color}, {self.price}" ex_computer = Computer('linux', 8, 'black') ex_laptop = Laptop('linux', 8, 'white') print(ex_computer.about()) print(ex_laptop.about())
Computer linux, 8, black, 3 Laptop linux, 8, white, 70000 Process finished with exit code 0
Для добавления новых атрибутов их конечно нужно снова проинициализировать, но тогда возникает проблема, нужно снова инициализировать все атрибуты родительского класса в дочерних, и в таком случае дублирование появляется снова. Для решения данной проблемы существует функция super(), которая как раз существует для наследования проинициализированных в родительском классе атрибутов. Внутрь функции super() не требуется передавать параметр self, функция super() сама находит и использует ссылку на родительский класс. Функцию super() всегда рекомендуется вызывать вначале.
class PC: def __init__(self, oc, ram, color): self.oc = oc self.RAM = ram self.color = color @staticmethod def some_def(): print('Вызов метода родительского класса') class Computer(PC): def __init__(self, oc, ram, color, operating_time=3): super().__init__(oc, ram, color) super().some_def() self.ot = operating_time def about(self): return f"Computer {self.oc}, {self.RAM}, {self.color}, {self.ot}" class Laptop(PC): def __init__(self, oc, ram, color, price=70000): super().__init__(oc, ram, color) self.price = price def about(self): return f"Laptop {self.oc}, {self.RAM}, {self.color}, {self.price}" ex_computer = Computer('linux', 8, 'black') ex_laptop = Laptop('linux', 8, 'white') print(ex_computer.about()) print(ex_laptop.about())
Вызов метода родительского класса Computer linux, 8, black, 3 Laptop linux, 8, white, 70000 Process finished with exit code 0
Через super() можно обращаться не только к методу __init__, но и к любому другому методу родительского класса.
Наследование от object. Использование метода __new__
Я упоминал, что в python по умолчанию происходит наследование от класса object. Это базовый класс, который содержит некоторый набор базовых методов. Обеспечивание классов базовым набором методов и является причиной такой реализации, и начиная с версии python 3 явное указание наследования от класса object не требуется. Теперь можно более подробно обсудить как работает метод __new__.
class Number: def __new__(cls, *args, **kwargs): print('Вызов __new__') def __init__(self, x): self.x = x ex_1 = Number(10) print(ex_1) print(issubclass(Number, object))
Вызов __new__ None True Process finished with exit code 0
Рассмотрим пример. Во-первых, то о чем я говорил, наследование от object явно прописывать не нужно, но это подразумевается. Функция issubclass() помогает в этом убедится, первым параметром в эту функцию передается дочерний класс, а вторым родительский, и если наследование действительно есть функция возвратит True, в противном случае False, работает эта функция только с классами, а не с их экземплярами.
Теперь что касается метода __new__. Как мы помним метод __new__ срабатывает до инициализации экземпляра в методе __init__, но как мы видим в данном случае метод __new__ вызвался, а вот инициализация не произошла, экземпляра класса создан не был. Почему так произошло? Потому что метод __new__ должен возвращать адрес созданного экземпляра.
class Number: # == class Number(object) def __new__(cls, *args, **kwargs): print('Вызов __new__') return super().__new__(cls) def __init__(self, x): self.x = x ex_1 = Number(10) ex_2 = Number(30) print(ex_1) print(ex_2) print(issubclass(Number, object))
Вызов __new__ Вызов __new__ <__main__.Number object at 0x7f176bbefeb0> <__main__.Number object at 0x7f176bbefdf0> True Process finished with exit code 0
Взять этот адрес мы можем из класса object, обратившись к нему функцией super() и поскольку метод __new__ относится к набору базовых методов, то и у базового класса object он тоже есть, и этот метод как раз будет хранить адрес каждого снова создаваемого экземпляра класса. Но зачем это вообще может пригодиться, ведь метод __new__ срабатывает автоматически при создании экземпляра, метода __init__ достаточно для корректной работы с экземплярами классов. Действительно этот так, но у метода __new__ есть одно применение, благодаря методу __new__ мы можем контролировать возможное максимальное количество создаваемых экземпляров, а также задавать условие по которому будет решаться создавать этот экземпляр или нет.
class Number: def __new__(cls, x): if x == 30: return super().__new__(cls) else: return None def __init__(self, x): self.x = x ex_1 = Number(10) ex_2 = Number(30) print(ex_1) print(ex_2) print(issubclass(Number, object))
None <__main__.Number object at 0x7f45c7d36f10> True Process finished with exit code 0
например, вот так можно проконтролировать должен ли создаваться класс или нет.
class Number: __instance = None def __new__(cls, x): if cls.__instance is None: cls.__instance = super().__new__(cls) return cls.__instance def __init__(self, x): self.x = x ex_1 = Number(10) ex_2 = Number(30) print(ex_1) print(ex_2)
<__main__.Number object at 0x7f78d2133f10> <__main__.Number object at 0x7f78d2133f10> Process finished with exit code 0
А вот так реализуется паттерн 'singleton', этот паттерн позволяет создавать только один экземпляр класса, каждый новый экземпляр создаваем после будет помещаться в ту же ячейку памяти, в которой находился прошлый созданный экземпляр этого класса. Как это работает? Мы создаем приватную переменную, которую принято называть instance, изначально эта переменная ровна None. Далее мы делаем проверку, если instance равен None, мы помещаем в нее адрес создаваемого экземпляра, а если instance уже содержит какой-то экземпляр мы возвращаем его.
class Number: __instance = None def __new__(cls, *args, **kwargs): if cls.__instance is None: cls.__instance = super().__new__(cls) return cls.__instance def __init__(self, x): self.x = x def into(self): return f"{self.x}" ex_1 = Number(10) ex_2 = Number(30) ex_3 = Number(50) print(ex_1, ex_1.into()) print(ex_2, ex_2.into()) print(ex_3, ex_3.into())
<__main__.Number object at 0x7fd7ff9a0100> 50 <__main__.Number object at 0x7fd7ff9a0100> 50 <__main__.Number object at 0x7fd7ff9a0100> 50 Process finished with exit code 0
Не стоит забывать и том, что атрибуты последнего созданного экземпляра будут являться единственными существующими атрибутами, это, конечно, логично, но эта наглядность помогает окончательно разобраться как этот паттерн работает.
Множественное наследование
Наследоваться можно не от единственного класса, допускается использование нескольких родительских методов.
class Employee: def __init__(self, name, age, post): super().__init__() self.name = name self.age = age self.post = post def info(self): return f"employee {self.name}, age {self.age}, on post {self.post}" class Control: sequence = 0 def __init__(self): Control.sequence += 1 self.seq = self.sequence class NewWorker(Employee, Control): def about(self): return f"{self.name} with id {self.seq}" employee_1 = NewWorker('Vanya', 25, 'junior_developer') employee_2 = NewWorker('Petya', 27, 'middle_developer') print(employee_1.info()) print(employee_2.info()) print(employee_1.about()) print(employee_2.about()) print(NewWorker.__mro__)
employee Vanya, age 25, on post junior_developer employee Petya, age 27, on post middle_developer Vanya with id 1 Petya with id 2 (<class '__main__.NewWorker'>, <class '__main__.Employee'>, <class '__main__.Control'>, <class 'object'>) Process finished with exit code 0
Допустим мы ведем учет людей нанимаемых на работу и нам бы хотелось в одной базе данных хранить имя, возраст и должность сотрудника, а в другой хранить порядок найма сотрудников в компанию, при этом учет хотелось бы вести автоматически. И при необходимости обращаться к атрибутам любой их этих баз. Для множественного наследования достаточно перечислить родительские классы через запятую, а для связи родительских классов между собой используем функцию super() в первом родительском классе, от которого мы наследуемся. Почему функция super() обращается к классу Control, а не к object, мы ведь явно этого нигде не указали. Дело в том, что множественное наследование использует специальный алгоритм обхода классов, который называется MRO (Method Resolution Order). И в python есть одноименный метод __mro__, который выводит последовательность обхода классов. Применение метода __mro__ к нашему классу NewWorker показывает, что к object мы обращаемся в последнюю очередь. Такие вспомогательные классы при множественном наследовании называются миксины, и миксинов может быть больше чем один.
Коллекция __slots__
Напоминаю, нам ничего не мешает добавить атрибут в коллекцию __dict__, имени которого там ранее не было.
class Employee: def __init__(self, name, age, post): self.name = name self.age = age self.post = post ex_1 = Employee('Vanya', 25, 'junior_developer') ex_1.lastname = 'Ivanov' print(ex_1.__dict__)
{'name': 'Vanya', 'age': 25, 'post': 'junior_developer', 'lastname': 'Ivanov'} Process finished with exit code 0
Делается это просто написанием имени несуществующего атрибута через точку со значением этого нового атрибута. Коллекция __slots__ ограничивает это поведение.
class Employee: __slots__ = ('name', 'age', 'post') def __init__(self, name, age, post): self.name = name self.age = age self.post = post ex_1 = Employee('Vanya', 25, 'junior_developer') ex_1.lastname = 'Ivanov' print(ex_1.__dict__)
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_25.py", line 11, in <module> ex_1.lastname = 'Ivanov' AttributeError: 'Employee' object has no attribute 'lastname' Process finished with exit code 1
В коллекции __slots__ перечисляются доступные имена атрибутов для экземпляров класса. И теперь попытка создания несуществующего атрибута приведет к ошибке.
class Employee: __slots__ = ('name', 'age', 'post') def __init__(self, name, age, post): self.name = name self.age = age self.post = post class NewWorker(Employee): def info(self): return f"{self.name}, {self.age}, {self.post}" ex_1 = NewWorker('Vanya', 25, 'junior_developer') ex_1.lastname = 'Ivanov' print(ex_1.info()) print(ex_1.__dict__)
Vanya, 25, junior_developer {'lastname': 'Ivanov'} Process finished with exit code 0
А вот когда дело касается наследование создание новых атрибутов допускается. При этом коллекция __dict__ по прежнему не будет содержать имена из коллекции __slots__.
class Employee: __slots__ = ('name', 'age', 'post') def __init__(self, name, age, post): self.name = name self.age = age self.post = post class NewWorker(Employee): __slots__ = () def info(self): return f"{self.name}, {self.age}, {self.post}" ex_1 = NewWorker('Vanya', 25, 'junior_developer') ex_1.lastname = 'Ivanov' print(ex_1.info()) print(ex_1.__dict__)
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_25.py", line 18, in <module> ex_1.lastname = 'Ivanov' AttributeError: 'NewWorker' object has no attribute 'lastname' Process finished with exit code 1
А если мы все-таки хотим унаследовать коллекцию __slots__ ее следует передать явно, при этом атрибуты заново прописывать не требуется.
Полиморфизм
Еще один важный паттерн ООП - Полиморфизм. Говоря грубо, идея полиморфизма сводится к одинаковому названию логически схожих методов разных классов.
class DollarInRuble: def __init__(self, count, course=83): self.count = count self.course = course def dollar_in_rouble(self): return self.count * self.course class EuroInRuble: def __init__(self, count, course=93): self.count = count self.course = course def euro_in_rouble(self): return self.count * self.course ex_d_1 = DollarInRuble(70) ex_d_2 = DollarInRuble(500) ex_e_1 = EuroInRuble(400) ex_e_2 = EuroInRuble(200) print(ex_d_1.dollar_in_rouble(), ex_d_2.dollar_in_rouble()) print(ex_e_1.euro_in_rouble(), ex_e_2.euro_in_rouble())
5810 41500 37200 18600 Process finished with exit code 0
Например, создадим два класса внутри которых будем переводить валюту в рубли. По сути в этой реализации мы видим один и тот же метод, а речь идет только о двух валютах, а если таких валют будут десятки, во всех этих разнообразных названиях для одинаковых методах можно запутаться, да и не нужно нам это разделение, ведь параметр self нам и нужен для того, чтобы понимать, о каком экземпляре идет речь.
class DollarInRuble: def __init__(self, count, course=83): self.count = count self.course = course def currency_in_rouble(self): return self.count * self.course class EuroInRuble: def __init__(self, count, course=93): self.count = count self.course = course def currency_in_rouble(self): return self.count * self.course ex_d_1 = DollarInRuble(70) ex_d_2 = DollarInRuble(500) ex_e_1 = EuroInRuble(400) ex_e_2 = EuroInRuble(200) print(ex_d_1.currency_in_rouble(), ex_d_2.currency_in_rouble()) print(ex_e_1.currency_in_rouble(), ex_e_2.currency_in_rouble(), '\n') investors = [ex_d_1, ex_d_2, ex_e_1, ex_e_2] for i in investors: print(i.currency_in_rouble())
5810 41500 37200 18600 5810 41500 37200 18600 Process finished with exit code 0
Дадим этим методам одно название это и будет реализацией полиморфизма. Теперь обращаться с программой удобнее, более того мы можем поместить все экземпляры в список, пройтись по нему в цикле for и поочередно применить одноименный метод к каждому из экземпляров, как вы понимаете при разных названиях этих методов такая реализация была бы невозможна.
Обработка исключений. try / except
Исключениями в python называют ошибки и их обработка является достаточно важным и полезным навыком в программировании.

На схеме я постарался собрать все существующие на данный момент исключения. Как видно все исключения наследуются от BaseException, а далее основная масса ошибок от Exception. Зачем эта информация нужна и в чем заключается идея обработки исключений?
print(1 + 'b')
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_27.py", line 1, in <module> print(1 + 'b') TypeError: unsupported operand type(s) for +: 'int' and 'str' Process finished with exit code 1
Вот один из самых очевидных примеров, складывать строки и целые числа нельзя. Мы получаем исключение типа TypeError, говорящую, что действие невозможно. А теперь на следующий пример.
print(10 + 30) print(1 + 'b') print('a' + 'b')
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_27.py", line 2, in <module> print(1 + 'b') TypeError: unsupported operand type(s) for +: 'int' and 'str' 40 Process finished with exit code 1
Первая и третья строка не содержат ошибок и их исполнение выдаст запрашиваемый результат, а вот по центру все еще код написанный с ошибкой. Отсюда вытекает причина почему обработка исключений так сильно необходима - ошибка прекращает исполнение кода в том месте где она встречается и все строки находящиеся ниже ошибки исполняться не будут. Конечно, такого поведения хочется избежать. И на первый взгляд может показаться, пиши код без ошибок и не нужно будет ничего обрабатывать, но во-первых, ошибки и программирование друг без друга существовать не могут, любая более менее сложная программа это результат сталкивания программиста с большим количеством ошибок, а во-вторых, даже полностью правильно написанный код не гарантирует, что ошибки в той или иной ситуации при использовании данного кода не возникнут, чуть позже посмотрим на такой пример. А пока обработаем нашу ошибку и познакомимся с конструкцией Try/Except.
print(10 + 30) try: print(1 + 'b') except TypeError: print('Числа и строки складывать нельзя') print('a' + 'b')
40 Числа и строки складывать нельзя ab Process finished with exit code 0
Синтаксис этой конструкции такой. В блок try помещается фрагмент кода, в котором предполагается возможность возникновения ошибки, а в блоке except происходит обработка предполагаемой ошибки и описывается действие, которое нужное сделать, если ошибка действительно возникнет. И программа как видно продолжает свое исполнение.
print(10 + 30) try: print(1 + 'b') except TypeError: print('Числа и строки складывать нельзя') try: lst = [1, 2, 3, 4] print(lst[5]) except IndexError: pass print('a' + 'b')
40 Числа и строки складывать нельзя ab Process finished with exit code 0
Возможна обработка сразу нескольких ошибок, например, попробуем взять индекс выходящий за границы индексов списка lst и в except просто пропустим это действие при возникновении ошибки.
В этих двух примерах мы использовали конкретные типы ошибок, поскольку они нам известны. А что если мы заранее не знаем какой тип ошибки может возникнуть в данном фрагменте кода? Для этого я нарисовал тут схему исключений, из которой явно видно, что почти все ошибки наследуются от типа Exception. И достаточно частый случай обработки исключений это использования в блоке except типа Exception. Это достаточно частая реализация обработки исключений.
print(10 + 30) | print(10 + 30) try: | try: print(1 + 'b') | lst = [1, 2, 3, 4] lst = [1, 2, 3, 4] | print(lst[5]) print(lst[5]) | print(1 + 'b') except TypeError: | except TypeError: print('Числа и строки складывать нельзя') | print('Числа и строки складывать нельзя') except Exception: | except Exception: print('Exception') | print('Exception') print('a' + 'b') | print('a' + 'b')
40 | 40 Числа и строки складывать нельзя | Exception ab | ab | Process finished with exit code 0 | Process finished with exit code 0
Правда в таком случае pycharm скажет, что использован слишком широкий диапазон исключений, но в данном случае pycharm можно не слушать. Рассмотрим повнимательней пример. Первый, конструкция, в которой несколько except идут после одного try допускаются. Но при этом инструкция сработает для первой найденной ошибки из блока try. Как видно из примера, поменяв ошибки местами внутри блока try мы видим разные выводы except действий. Можно выбирать исключения из еще более широко диапазона, и даже не заменой Exception на BaseException, а просто написав except с двоеточием, в таком случае абсолютно любая возможная ошибка будет отловлена и отработана.
Но все-таки иногда бывает полезно записывать конкретный тип исключений в except, полезно это может быть в тех ситуациях, когда фрагмент внутри блока try может при разном исполнении вызывать разные типы исключений и нам было бы полезно понимать какой конкретно тип ошибки случается в данном конкретном исполнении и для каждого такого конкретного типа использовать конкретную инструкцию обработки. Надеюсь эта идея ясна.
try / except / finally / else. вложенная обработка исключений. где не получится обойтись без обработки исключений
На самом деле конструкция try / except может включать в себя еще два необязательных блока - finally и else. Блок else отработает в том случае, если в блоке try не будет ни одной ошибки, а блок finally отработает всегда, независимо от того были ли ошибки в блоке tyr или нет.
print(10 + 30) | print(10 + 30) try: | try: print(1 + 'b') | print(1 * 'b') lst = [1, 2, 3, 4] | lst = [1, 2, 3, 4] print(lst[5]) | print(lst[3]) except Exception as e: | except Exception as e: print(e) | print(e) else: | else: print('Вызов блока else') | print('Вызов блока else')) finally: | finally: print('Вызов блока finally') | print('Вызов блока finally') print('a' + 'b') | print('a' + 'b')
40 | 40 unsupported operand type(s) for +: 'int' and 'str' | b Вызов блока finally | 4 ab | Вызов блока else | Вызов блока finally Process finished with exit code 0 | ab | | Process finished with exit code 0
Помимо этого можно давать собственное название для ошибки после ключевого слова as. Запись 'except Exception as e' возможно встретить достаточно часто. И если нам достаточно увидеть описание ошибки при ее возникновении достаточно распечатать это ключевое слово.
print(10 + 30) try: print(1 + 'b') try: lst = [1, 2, 3, 4] print(lst[5]) except Exception as e: print(e) except Exception as e: print(e) else: print('Вызов блока else') finally: print('Вызов блока finally') print('a' + 'b')
40 unsupported operand type(s) for +: 'int' and 'str' Вызов блока finally ab Process finished with exit code 0
Конструкция try/except может быть вложенной. При такой реализации будет обработана первая найденная ошибка. Таким образом, на примере мы видим, что была обработана ошибка в третьей строке, но если закомментировать эту строку или написать ее без ошибок то будет обработана следующая найденная ошибка, в нашем случае IndexError.
print(10 + 30) try: print(1 * 'b') try: lst = [1, 2, 3, 4] print(lst[5]) except Exception as e: print(e) except Exception as e: print(e) else: print('Вызов блока else') finally: print('Вызов блока finally') print('a' + 'b')
40 b list index out of range Вызов блока else Вызов блока finally ab Process finished with exit code 0
И хотелось бы еще обратить внимание на то, что блок else в данном случае будет исполнена, ведь блок else находится на одном уровне с той конструкцией try/except, в которой ошибки нет.
Обработка исключений может быть полезна в любой программе, но особенно это может быть полезно в программах где мы запрашиваем какую-то информацию от пользователя.
while True: try: a, b = map(int, input('Введите два числа: ').split()) action = input('Что сделать с числами, разделить или умножить? ') if action.lower() == 'разделить': print(a / b) elif action.lower() == 'умножить': print(a * b) else: print('Напишите разделить или умножить') except Exception as e: print(e)
Введите два числа: 10 15 Что сделать с числами, разделить или умножить? умножить 150 Введите два числа: 30 0 Что сделать с числами, разделить или умножить? разделить division by zero Введите два числа: 0 15 Что сделать с числами, разделить или умножить? Разделить 0.0 Введите два числа: 15 15 Что сделать с числами, разделить или умножить? сложить Напишите разделить или умножить Введите два числа:
Допустим напишем такой простенький калькулятор, который может только умножать и делить числа и делать это бесконечное количество раз. И без использования инструкции try/except при делении на ноль программа бы прекратила свое выполнение. Контролировать то, что введет пользователь мы, конечно, можем, но не во всех случаях. Вот один из элементарных, но достаточно наглядных примеров, когда без try/except программа не станет работать так как мы задумывали.
raise
Инструкция raise возбуждает исключение.
raise TypeError('Ошибка')
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_29.py", line 1, in <module> raise TypeError('Ошибка') TypeError: Ошибка Process finished with exit code 1
Вызывается raise с любым типом исключения, а после типа ошибки в скобках можно написать какой-нибудь текст, который будет выводиться вместе с этим исключением.
class Truediv: def __init__(self, number): self.number = number @classmethod def validate(cls, other): if type(other) != Truediv: raise TypeError('Действие доступно только с экземплярами класса') def __truediv__(self, other): self.validate(other) return self.number / other.number ex_1 = Truediv(50) ex_2 = Truediv(20) print(ex_1 / ex_2) print(ex_1 / 100)
2.5 Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_29.py", line 18, in <module> print(ex_1 / 100) File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_29.py", line 11, in __truediv__ self.validate(other) File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_29.py", line 8, in validate raise TypeError('Действие доступно только с экземплярами класса') TypeError: Действие доступно только с экземплярами класса Process finished with exit code 1
Например, вспомним метод деления и будем проверять является ли переменная other экземпляром класса, и если нет выводить исключение с соответствующим текстом.
class Truediv: def __init__(self, number): self.number = number @classmethod def validate(cls, other): if type(other) != Truediv: raise TypeError('Действие доступно только с экземплярами класса') def __truediv__(self, other): self.validate(other) return self.number / other.number raise ZeroDivisionError('Нельзя') ex_1 = Truediv(50) ex_2 = Truediv(20) ex_3 = Truediv(0) print(ex_1 / ex_2) print(ex_1 / ex_3) print(ex_1 / 100)
Traceback (most recent call last): File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_29.py", line 1, in <module> class Truediv: File "/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/lesson_29.py", line 14, in Truediv raise ZeroDivisionError('Нельзя') ZeroDivisionError: Нельзя Process finished with exit code 1
Можно использовать несколько инструкций raise, например на случай, если передать в какой-нибудь экземпляр-делитель ноль, ошибка в таком случае будет другого типа, не TypeError, а ZeroDivisionError. Допустим в такой ситуации тоже хочется увидеть какой-нибудь заранее заготовленный текст, и вот так нехитро это можно реализовать. И как видно из данного примера инструкция raise, как и обычное исключение, останавливает работу программы.
if name == '__main__'
Хоть эта тема можно сказать и не относится к ООП, но по мне это хорошая тема для завершения всего базового материала по python, под базовым материалом я понимаю базовый синтаксис и ООП.
Часто можно увидеть скрипты, в конце которых прописана конструкция if name == '__main__', зачем это нужно? Как мы знаем по умолчанию каждая программа читается сверху вниз, а также во время запуска программы создается словарь с некоторым набором служебных переменных.
dm = 10 km = 0.001 def how_many_cm(m, cm=100): return f"{m * cm} centimetres" def how_many_other(m, measure_of_length): if measure_of_length == 'dm': return f"{m * dm} decimetres" elif measure_of_length == 'km': return f"{m * km} kilometers" else: return f"Выберете дециметры или километры" print(how_many_cm(50)) print(how_many_other(100, 'dm')) print(how_many_other(100, 'km')) print(globals()) print(__name__)
5000 centimetres 1000 decimetres 0.1 kilometers {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7ffb42776c10>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/home/tsarkoilya/kavo/PycharmProjects/forsite/python_learn/oop/test_2.py', '__cached__': None, 'dm': 10, 'km': 0.001, 'how_many_cm': <function how_many_cm at 0x7ffb427460d0>, 'how_many_other': <function how_many_other at 0x7ffb4253fe50>} __main__ Process finished with exit code 0
Список этих служебных переменных можно увидеть через функцию global(). Напишем простенькую программу для преобразования метров в разные меры длины, среди служебных переменных помимо прочего мы видим наши функции и глобальные переменные, а также переменную '__name__' со значением __main__. Точкой входа в программу на данный момент является первая строка, то есть создание переменной dm. Конструкция if name == '__main__' нужна для изменения этой самой точки входа. Для чего это нужно.
dm = 10 | from lesson_30 import * km = 0.001 | | print(__name__) def how_many_cm(m, cm=100): return f"{m * cm} centimetres" def how_many_other(m, measure_of_length): if measure_of_length == 'dm': return f"{m * dm} decimetres" elif measure_of_length == 'km': return f"{m * km} kilometers" else: return f"Выберете дециметры или километры" print(how_many_cm(50)) print(how_many_other(100, 'dm')) print(how_many_other(100, 'km')) print(__name__)
5000 centimetres 1000 decimetres 0.1 kilometers lesson_30 __main__ Process finished with exit code 0
Нужно это тогда, когда мы хотим использовать наш скрипт не только как самостоятельную программу для самостоятельного пользования, а когда мы хотим предоставлять какие-то объекты из нашей программы для импорта в другие программы. В программу lesson_30_1 импортировано все содержимое программы lesson_30 и при простом запуске программы lesson_30_1 мы видим все функции print() программы lesson_30. Конечно, такого поведения нам бы хотелось избежать. Помимо этого, как видно из результата работы программы lesson_30_1 переменная __name__ импортированная из программы lesson_30 получает значение равное имени программы, а именно lesson_30, но при прямом вызове содержимого переменной __name__ мы видим имя __main__. Таким образом через имя переменной __name__ можно увидеть, какая программа является главной, а какая пользуется возможностями этой программы.
dm = 10 | from lesson_30 import * km = 0.001 | | print(__name__) def how_many_cm(m, cm=100): return f"{m * cm} centimetres" def how_many_other(m, measure_of_length): if measure_of_length == 'dm': return f"{m * dm} decimetres" elif measure_of_length == 'km': return f"{m * km} kilometers" else: return f"Выберете дециметры или километры" def main(): print(how_many_cm(50)) print(how_many_other(100, 'dm')) print(how_many_other(100, 'km')) print(__name__) if __name__ == '__main__': main()
__main__ Process finished with exit code 0
Часто исполнение программы помещают в функцию main(), а далее задают новую точку входа в программу конструкцией if name == '__main__' и первым делом выполняют функцию main(). Теперь импорт всего содержимого и исполнение этой программы не выведет исполнение функций print() главного скрипта.
dm = 10 | from lesson_30 import how_many_cm, main km = 0.001 | | print(how_many_cm(50)) | print(main()) def how_many_cm(m, cm=100): | return f"{m * cm} centimetres" | print(__name__) def how_many_other(m, measure_of_length): if measure_of_length == 'dm': return f"{m * dm} decimetres" elif measure_of_length == 'km': return f"{m * km} kilometers" else: return f"Выберете дециметры или километры" def main(): print(how_many_cm(50)) print(how_many_other(100, 'dm')) print(how_many_other(100, 'km')) print(__name__) if __name__ == '__main__': main()
5000 centimetres 5000 centimetres 1000 decimetres 0.1 kilometers lesson_30 None __main__ Process finished with exit code 0
Таким образом, мы получили грамотно написанный скрипт, необходимыми функциями которого можно пользоваться в сторонних программах без исполнения лишних функций.
dm = 10 | from lesson_30 import * km = 0.001 | | print(how_many_cm(50)) | def how_many_cm(m, cm=100): | print(__name__) return f"{m * cm} centimetres" def how_many_other(m, measure_of_length): if measure_of_length == 'dm': return f"{m * dm} decimetres" elif measure_of_length == 'km': return f"{m * km} kilometers" else: return f"Выберете дециметры или километры" def main(): print(how_many_cm(50)) print(how_many_other(100, 'dm')) print(how_many_other(100, 'km')) print(__name__) if __name__ == '__main__': main()
5000 centimetres __main__ Process finished with exit code 0
Так же как и при импорте через оператор звездочка. При исполнении программы мы видим исполнение только одной единственной необходимой функции, без выполнения всех функций print(). Пишите код грамотно, это гораздо приятнее и для вас и для тех кто будет читать ваш код.
Что дальше?
На этом знакомство с основами python закончено, теперь вы можете прочитать и понять, пожалуй, любой фрагмент кода python, который попадется вам на глаза. Python это не только базовый синтаксис и ООП, python это огромное количество библиотек, предназначенных для огромного количества задач, которые решает этот язык. И следующий этап вашего обучения это выбор направления, которое вам более интересно, Django со всеми его вытекающими для современного backend'а веб-приложений; pandas, matplotlib, scipy и прочие библиотеки для анализа данных и машинного обучения и прочие возможности языка и библиотеки используемые для их реализации. Есть библиотеки, которые поставляются сразу с установкой python, другие библиотеки нужно устанавливать отдельно. Но не забывайте, все эти библиотеки в основе хранят все те типы данных, все те методы, все те функции и их особенности, все те классы и принципы их взаимодействия, с которыми мы познакомились в первых двух блоках посвященных python.
Далее только понимание чего вы хотите от программирования и бесконечная практика
Комментарии
Разделы
- НАВЕРХ
- Классы
- Параметр self
- __init__, __new__, __del__
- Приватные и Публичные методы и атрибуты
- Property
- Паттерн 'Моносостояние'
- classmethod и staticmethod
- __str__, __repr__
- __len__, __abs__
- __add__, __mul__, __sub__, __truediv__
- Методы сравнения
- __hash__
- __bool__
- __call__
- __setattr__, __getattribute__
__getattr__, __delattr__ - __getitem__, __setitem__, __delitem__
- __iter__, __next__
- __pos__, __neg__, __invert__
- __round__, __floor__, __ceil__, __trunc__
- Оставшиеся методы арифметических операций
- __lshift__, __rshift__
- Бинарное И, ИЛИ, ИЛИ НЕТ
- Отраженные операторы.
Составное присваивание.
Остальные магические методы - Дескрипторы
- Наследование
- super()
- Наследование от object. Использование метода __new__
- Множественное наследование
- Коллекция __slots__
- Полиморфизм
- Обработка исключений. try / except
- try / except / finally / else.
вложенная обработка исключений - raise
- if name == '__main__'
- Что дальше?
Для отправки комментария необходимо авторизоваться
Комментарии
Здесь пока ничего нет...