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

Введение

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

Классы

Объектно-ориентированное программирование построено на классах и на концепциях, или принципах, взаимодействия этих классов.

class SomeClass:
    pass

Создание класса напоминает создание функции, прописываем ключевое слово class после него пишем название класса и ставим двоеточие. Класс создан.

Python Console
>>> class Computer:
...            operation_system = 'Linux'
...            type_of_computer = 'Laptop'
...            RAM = 4
...

>>> Computer.   
Special Variables
__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 с его атрибутами появился среди прочих специальных переменных.

Python Console
>>> class Computer:
...           operation_system = 'Linux'
...           type_of_computer = 'Laptop'
...           RAM = 4
...   
>>> Computer.operation_system
'Linux'
>>> Computer.type_of_computer = 'PC'
Special Variables
__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.

Python Console
>>> 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
ex_1={Computer}<__main__.Computer object at 0x7fd556d187f0>
RAM = {int}8

operation_system = {str}'Linux'

type_of_computer = {str}'PC'
ex_2={Computer}<__main__.Computer object at 0x7fd556d188e0>
RAM = {int}3

operation_system = {str}'Windows'

type_of_computer = {str}'PC'
Special Variables
__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)>

Мы можем создавать экземпляры классов. Каждый из экземпляров будет ссылаться на оригинальный класс и изменения атрибутов экземпляра класса никак не повлияют на атрибуты оригинального класса. При этом изменения совершаемые над атрибутами в оригинальном классе повлияют на атрибуты его экземпляров, если они не были явно изменены ранее.

Python Console
>>> 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() принимает имя объекта, вторым имя атрибута, а третьим значение по умолчанию, которое будет возвращено, если переданного имени атрибута в пространстве этого объекта не существует.

Python Console
>>> 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)
ex_1={Computer}<__main__.Computer object at 0x7f1ed29319a0>
RAM = {int}4

color = {str}'black'

operation_system = {str}'Windows'

price = {int}15000

type_of_computer = {str}'PC'
Special Variables
__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(). Первый аргумент - имя объекта, второй - имя атрибута, третий - значение, устанавливаемое в качестве нового значения для уже существующего атрибута и в качестве первого значения для несуществующего.

Python Console
>>> 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
Special Variables
__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
CODE
class Computer:
    operation_system = 'Linux'
    type_of_computer = 'Laptop'
    RAM = 4

    def info():
        print('Информация о моем компьютере')


print(Computer.info())

ex = Computer()

print(ex.info())
RESULT
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
Problems: Current File 1
! Method must have a first parametr, usually called 'self' 6

Внутри классов мы можем объявлять методы, в данном случае функция info(). При такой записи pycharm показывает нам одну критическую ошибку. Pycharm подсказывает, что метод класса должен иметь обязательный параметра, который обычно называется self. Мы можем обратиться к методу класса, но не можем обратиться к этому же методу у экземпляра этого класса. Ошибка подсказывает, что передано ноль аргументов, а должен быть передан один. Тут нам и нужен параметр self. Этот параметр нужен для взаимодействия с экземплярами класса.

CODE
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()
RESULT
Информация о моем компьютере<__main__.Computer object at 0x7fec71592c10>
Информация о моем компьютере<__main__.Computer object at 0x7fec71592c40>

Process finished with exit code 0

Параметр self, как видно из результата, ссылается на уникальную ячейку памяти выделенную для каждого экземпляра класса.

CODE
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')
RESULT
5000 black
10000 white

Process finished with exit code 0
Problems: Current File 2
! Instance attribute price defined outside __init__ 7
! Instance attribute color defined outside __init__ 8

Теперь через этот параметр мы можем создавать или менять локальные свойства экземпляров класса. Благодаря параметру self интерпретатор понимает к какому экземпляру мы обращаемся. Но pycharm опять подсказывает, что мы сделали что-то неправильно. А именно говорит, что не хватает некого инициализатора __init__.

__init__, __new__, __del__

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

CODE
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())
RESULT
Мой ПК - desktop, linux, 8
Мой ПК - notebook, windows, 4

Process finished with exit code 0

Перепишем наши переменные внутри инициализатора __init__. Метод __init__ выполняет роль конструктора класса. Для атрибутов метода __init__ можно прописать значения по умолчанию и тогда можно не прописывать их при создании экземпляра класса, а вот значения атрибутов без значения по умолчанию прописывать придется обязательно. Обратится к этим атрибутам можно внутри любого метода данного класса. А благодаря параметру self перед названием атрибута интерпретатор поймет к какому экземпляру класса относится данный атрибут.

CODE
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())
RESULT
Мой ПК - 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 достаточно для конструирования экземпляров классов. Но вернемся к этому вопросу позже.

CODE
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())
RESULT
Мой ПК - 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 уже поднадоел, давайте создадим новый.

CODE
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)
RESULT
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, который будет хранить информацию, по которой можно идентифицировать любую банковскую карту. Сейчас все атрибуты и методы этого класса являются публичными. Это значит, что мы можем обратиться к методу и получить информацию о карте, а также мы можем обратиться к любому атрибуту экземпляра класса напрямую, даже не используя метод. Конечно, для данных, доступ к которым не хотелось бы предоставлять для любого человека, такое поведение класса не совсем подходящее. Для таких данных и существует возможность создания приватных методов и атрибутов.

CODE
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)
RESULT
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

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

CODE
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)
RESULT
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

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

CODE
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())
RESULT
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, внутри которого мы обратимся к нашему приватному методу без проблем выведет его содержимое.

CODE
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)
RESULT
id 4200 5800 3000 2000, cvv 250
4200 3800 5000 4000
250

Process finished with exit code 0

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

Property

Мы ранее обращались к функциям getattr(), setattr(), delattr(). Теперь немного улучшим эти функции.

CODE
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())
RESULT
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().

CODE
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
RESULT
300000
400000

Process finished with exit code 0

Передадим наши методы в соответствующие атрибуты функции property(). Теперь ко всем трем свойствам мы можем обращаться через одно ключевое слово, в нашем случае слово price. Согласитесь это выглядит удобнее. Но можно представить эту запись еще компактнее.

CODE
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
RESULT
300000
400000

Process finished with exit code 0

В первую очередь посмотрим на закомментированные строки. Свойство property можно представить таким образом, а далее для избежания конфликта имен и превращения этого свойства в декоратор используем для каждого метода одно имя, price в нашем случае. Метод get будет являться главным, опишем его как декоратор @property, остальные два метода декорируем относительно главного декоратора. Таким образом мы получили достаточно гибкий и элегантный способ работы с приватными атрибутами.

CODE
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)
RESULT
23100
38500

Process finished with exit code 0

Посмотрим еще один пример использования property. Создадим класс, который будет принимать курс доллара и количество долларов, а возвращать в своем единственном методе эквивалент этого значения в рублях. Начнем с метода my_rub_balance, он будет производить вычисление каждый раз при его выводе, даже в том случае, если количество и курс не изменился, это не самое хорошее поведение, ресурсы расходуются на вычисление результата, который уже известен. Для контролирования этого момента создадим проверку. Определим атрибут my_balance и присвоим ей значение None. Таким образом получается, если баланс ранее не был вычислен мы его вычислим, если был, то просто вернем его. Но отсюда вытекает новая проблема, изменение свойства не сбрасывает значение атрибута my_balance. Для решения этой проблемы обернем атрибут dol в декораторы @property (или @getter) и @setter, при чем внутри сеттера будем каждый раз сбрасывать значение атрибута my_balance. Таким образом мы реализовали решение данной задачи таким образом, чтобы текущий баланс сохранялся в случае неизменности данных и благодаря этому каждый раз не тратились бы ресурсы устройства на вычисление одного и того же действия.

Паттерн 'Моносостояние'
Python Console
>>> 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})
table_1={Table}<__main__.Table object at 0x7fb505080f70>
color = {str}'black'

height = {int}500

width = {int}1200
Special Variables
__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__, с которым мы ранее не знакомились, выводит список всех атрибутов класса, а также его атрибутов. Воспользуемся этой информацией для реализации данного паттерна.

Python Console
>>> 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
table_1={Table}<__main__.Table object at 0x7fbbd52c5880>
color = {str}'white'

height = {int}500

width = {int}1500
table_2={Table}<__main__.Table object at 0x7fbbd52c5850>
color = {str}'white'

height = {int}500

width = {int}1500
Special Variables
__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, который является ссылкой на экземпляр класса, но это не единственный вариант определения методов класса.

CODE
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))
RESULT
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 - двойное нижнее подчеркивание.

CODE
class Language:
    def __init__(self, name):
        self.name = name

ex_1 = Language('Python')
print(ex_1)
RESULT
<__main__.Language object at 0x7f5e3539ec10>

Process finished with exit code 0

Начнем знакомство с методов для отображения экземпляров класса. Посмотрим на класс Language, если распечатать атрибут функцией print(), то мы увидим не очень дружелюбное для понимания имя.

Python Console
>>> 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() к экземплярам класса не выйдет, нужно сначала определить эти методы.

CODE
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__())
RESULT
5
40

Process finished with exit code 0

Метод __len__ соответственно вернет длину объекта.
А метод __abs__ вернет модуль числа, или говоря по другому, его абсолютное значение.

__add__, __mul__, __sub__, __truediv__

Основным арифметическим операциям тоже соответствуют магические методы.

CODE
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
RESULT
90
140
240
2000
10
-10
0.8

Process finished with exit code 0

Метод __add__ для сложения.
Метод __mul__ для умножения.
Метод __sub__ для вычитания.
Метод __truediv__ для деления.
В каждом методе будем проверять является ли переменная 'other' целым или вещественным числом для арифметических операций с числами. А вторя проверка нужна для совершения арифметических операций над экземплярами. Реализовав эти четыре метода мы теперь можем совершать все четыре операции над экземплярами класса Group. При чем порядок записи экземпляров не важен.

Методы сравнения

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

CODE
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)
RESULT
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() вычисляет хэш объекта, но только неизменяемого объекта. И как мы помним в качестве ключа словаря мы можем использовать только неизменяемые объекты.

CODE
print(hash('slovo'))
print(hash((1, 2, 'abc')))
print(hash((1, 2, 'abc')))
RESULT
859905052852229039
5049973204321871429
5049973204321871429

Process finished with exit code 0

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

CODE
class Group:
    def __init__(self, group_count):
        self.count = group_count


group_1 = Group(50)
group_2 = Group(50)

print(group_1 == group_2)
RESULT
False

Process finished with exit code 0

Создадим два одинаковых экземпляра одного класса и сравним их. Получим достаточно ожидаемый результат - False. Дело в том, что таким сравнением мы ссылки на ячейку памяти выделенную под конкретный экземпляр и они конечно разные. Для сравнения содержимого атрибута требуется определить уже знакомый метод __eq__. Но сначала посмотрим на хэш этих экземпляров.

CODE
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))
RESULT
8765914553793
8765914553784

Process finished with exit code 0

Хэши вычисляются и хэши разные. Это говорит нам еще и том, что экземпляр класса считается неизменяемым объектом.

CODE
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))
RESULT
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__.

CODE
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))
RESULT
True
50
50
True
7622224536684080658
7622224536684080658

Process finished with exit code 0

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

__bool__

Как мы помним любой тип данных в python является объектом, а любой объект можно отнести к одному из логических типов - True либо False. Любой значимый объект, будь то не пустой список, не пустой словарь или, например, не число ноль относятся к логическому типу True, остальные к типу False.

CODE
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))
RESULT
True
True

Process finished with exit code 0

Применение функции bool() к любому экземпляру вернет True. Даже если передать в качестве аргументов нули. Дело в том, что пока в классе явно не определено поведение метода __bool__, то для отнесения объекта к логическому типу используется метод __len__, этот метод возвращает True, если длина объекта больше нуля.

CODE
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))
RESULT
True
False

Process finished with exit code 0

Определим метод __bool__, в котором будем проверять является ли значение нулем и в случае хотя бы одного нуля возвращать False.

__call__

Метод __call__ вызывается при вызове класса. Например, когда мы создаем экземпляр класса таким образом: название_экземпляра_класса = название_класса(). Именно круглые скобки в конце названия класса говорят о его выводе и именно в этот момент срабатывает метод __call__. И грубо говоря внутри метода __call__ вызывается метод __new__ и метод __init__. Но вот вызвать экземпляр класса через круглые скобки не выйдет. Тут нам и пригодится метод __call__.

CODE
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)
RESULT
Вызов экземпляра first
1
Вызов экземпляра first
Вызов экземпляра second
Вызов экземпляра second
Вызов экземпляра second
Вызов экземпляра second
4

Process finished with exit code 0

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

CODE
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())
RESULT
Вызов экземпляра first
First
Вызов экземпляра second
Second

Process finished with exit code 0

Зачем это может пригодиться? Раз метод __call__ вызывается сразу при вызове экземпляра класса, то мы можем описать внутри него какое-то поведение применимое к каждому вызову экземпляра. Например, пусть каждый раз к имени экземпляра применяется метод title().

__setattr__, __getattribute__, __getattr__, __delattr__
CODE
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
RESULT
__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__ вызывается, когда мы удаляем какой-нибудь атрибут.

CODE
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
RESULT
__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__.

CODE
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
RESULT
__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__ в данном примере описано не совсем правильно.

CODE
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
RESULT
__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__

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

CODE
class Languages:
    def __init__(self, *args):
        self.l_list = list(args)


ex_l = Languages('Python', 'Java', 'C++', "JavaScript", 'PHP')
print(ex_l[2])
RESULT
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 принимает список языков программирования и если мы захотим обратиться по индексу к элементу этого списка, то получим ошибку.

CODE
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])
RESULT
C++
C#
JavaScript

Process finished with exit code 0

Методы __getitem__, __setitem__ и __delitem__ добавят это поведение в наш класс, добавят обращение по индексу, изменение по индексу и удаление по индексу соответственно.

__iter__, __next__

Говоря о списках возникает вопрос, можно ли по этому списку пройтись в цикле for. Ну раз существуют методы __iter__ и __next__, то очевидно, что без реализации этих методов попытка применить к списку цикл for вернет ошибку.

CODE
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)
RESULT
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__.

CODE
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)
RESULT
Python
Java
C++
JavaScript
PHP

Process finished with exit code 0

Добавим в наш класс метод __iter__, внутри которого сделаем список итератором. Теперь применение к нему цикла for вернет поочередно каждый элемент списка.

CODE
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__())
RESULT
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

Унарные операции реализуются просто подставлением необходимого знака перед числом.

CODE
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__())
RESULT
-10
10

Process finished with exit code 0

Метод __pos__ - унарный плюс.
Метод __neg__ - унарный минус.
Унарный плюс примененный к отрицательному числу все-равно вернет отрицательное число, потому что минус 'сильнее' плюса. Минус на плюс дает минус. А вот применение унарного минуса к отрицательному числу вернет положительное число, ведь минус на минус дает плюс.

CODE
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__())
RESULT
(-10, 5)

Process finished with exit code 0

Множественно присвоение в классах можно реализовать, например, с помощью метода __invert__.

__round__, __floor__, __ceil__, __trunc__

Округление в python необязательно реализовывать с помощью магических методов.

CODE
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__())
RESULT
3.142
3
4
3

Process finished with exit code 0

Методы __round__, __floor__, __ceil__ и __trunc__ - это 'подкапотная' реализация тех методов для округления, которыми мы пользуемся.

Оставшиеся методы арифметических операций

Осталось еще несколько арифметических операций, которые мы не разобрали.

CODE
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)
RESULT
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

Речь идет о целочисленном делении, остатке от деления и возведении в степень.

CODE
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))
RESULT
4
0
3200000
104857600000000000000000000

Process finished with exit code 0

Метод __floordiv__ для целочисленного деления.
Метод __mod__ для остатка от деления.
Метод __pow__ для возведения в степень, по умолчанию имеет третий необязательный аргумент.

__lshift__, __rshift__

Функция shift() в python работает в сторону увеличения и уменьшения, используются для этого lshift() и rshift() соответственно. Что делает эта функция лучше увидеть на примере.

CODE
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)
RESULT
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.

Бинарное И, ИЛИ, ИЛИ НЕТ

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

CODE
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)
RESULT
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 раза, посмотрим как можно это изменить.

CODE
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)
RESULT
90
2000
10
1.25

Process finished with exit code 0

Например, можно эту проверку снести в обычный метод класса, где будем совершать проверку other на принадлежность ее к экземпляру класса. И перед каждой операцией просто совершать эту проверку. Правда работать такая проверка будет работать корректно, если применить инструкцию raise, а не return, но с raise мы по-прежнему не знакомы, такая реализация использована просто для наглядности. Можно и не использовать метод класса, но тогда pycharm возмутится и скажет, что нужен либо метод класса, либо статический метод, один из этих вариантов нужно использовать поскольку с помощью таких методов можно обращаться к такому методу не через экземпляр класса, а напрямую.

CODE
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__)
RESULT
{'count_1': 10, 'count_2': 20}
{'count_1': 30, 'count_2': 20}

Process finished with exit code 0

Оставим эту проверку, только проверять будем теперь не на принадлежность экземпляру класса, а на принадлежность к целочисленному или вещественному типу данных, но при этом изменим функциональность самого класса. Пусть класс Group принимает теперь два значения, и мы хотим просто обращаться к ним, и изменять их. Property в этом поможет, тут нет ничего нового мы уже это умеем. Но тут сразу в глаза бросается все то же повторение кода, а это класс, у которого всего два атрибута, а что если бы их было больше. Конечно, в python есть решение для данной ситуации. Сама идея дескрипторов заключается в создании стороннего класса, внутри которого весь этот функционально одинаковый код будет приведен к единому интерфейсу. Реализуем такой класс.

CODE
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)
RESULT
{'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__ мы просто возвращаем значение этого имени.

CODE
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__)
RESULT
{'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, для корректной отработки обработки неверно переданных данных.

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

Концепция наследования подразумевает наследование атрибутов и методов родительского класса классом потомком. Сымитируем проблему.

CODE
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())
RESULT
Computer linux, 8, black
Laptop linux, 8, white

Process finished with exit code 0

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

CODE
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())
RESULT
Computer linux, 8, black
Laptop linux, 8, white

Process finished with exit code 0

Создадим родительский класс PC, для того, чтобы наследоваться от какого-нибудь класса, нужно у дочернего класса открыть скобки после названия и написать туда название родительского класса. Обратите внимание создание экземпляров по прежнему происходит через классы Computer и Laptop, а не через класс PC, конечно, это нужно для того, чтобы параметр self понимал на какой класс ссылаться и уникальными свойствами какого класса в последствии пользоваться.

super()

А теперь представим, что у ноутбуков и стационарных компьютеров будут какие-то уникальные свойства, например цена и время эксплуатации соответственно.

CODE
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())
RESULT
Computer linux, 8, black, 3
Laptop linux, 8, white, 70000

Process finished with exit code 0

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

CODE
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())
RESULT
Вызов метода родительского класса
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__.

CODE
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))
RESULT
Вызов __new__
None
True

Process finished with exit code 0

Рассмотрим пример. Во-первых, то о чем я говорил, наследование от object явно прописывать не нужно, но это подразумевается. Функция issubclass() помогает в этом убедится, первым параметром в эту функцию передается дочерний класс, а вторым родительский, и если наследование действительно есть функция возвратит True, в противном случае False, работает эта функция только с классами, а не с их экземплярами.
Теперь что касается метода __new__. Как мы помним метод __new__ срабатывает до инициализации экземпляра в методе __init__, но как мы видим в данном случае метод __new__ вызвался, а вот инициализация не произошла, экземпляра класса создан не был. Почему так произошло? Потому что метод __new__ должен возвращать адрес созданного экземпляра.

CODE
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))
RESULT
Вызов __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__ мы можем контролировать возможное максимальное количество создаваемых экземпляров, а также задавать условие по которому будет решаться создавать этот экземпляр или нет.

CODE
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))
RESULT
None
<__main__.Number object at 0x7f45c7d36f10>
True

Process finished with exit code 0

например, вот так можно проконтролировать должен ли создаваться класс или нет.

CODE
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)
RESULT
<__main__.Number object at 0x7f78d2133f10>
<__main__.Number object at 0x7f78d2133f10>

Process finished with exit code 0

А вот так реализуется паттерн 'singleton', этот паттерн позволяет создавать только один экземпляр класса, каждый новый экземпляр создаваем после будет помещаться в ту же ячейку памяти, в которой находился прошлый созданный экземпляр этого класса. Как это работает? Мы создаем приватную переменную, которую принято называть instance, изначально эта переменная ровна None. Далее мы делаем проверку, если instance равен None, мы помещаем в нее адрес создаваемого экземпляра, а если instance уже содержит какой-то экземпляр мы возвращаем его.

CODE
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())
RESULT
<__main__.Number object at 0x7fd7ff9a0100> 50
<__main__.Number object at 0x7fd7ff9a0100> 50
<__main__.Number object at 0x7fd7ff9a0100> 50

Process finished with exit code 0

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

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

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

CODE
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__)
RESULT
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__, имени которого там ранее не было.

CODE
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__)
RESULT
{'name': 'Vanya', 'age': 25, 'post': 'junior_developer', 'lastname': 'Ivanov'}

Process finished with exit code 0

Делается это просто написанием имени несуществующего атрибута через точку со значением этого нового атрибута. Коллекция __slots__ ограничивает это поведение.

CODE
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__)
RESULT
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__ перечисляются доступные имена атрибутов для экземпляров класса. И теперь попытка создания несуществующего атрибута приведет к ошибке.

CODE
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__)
RESULT
Vanya, 25, junior_developer
{'lastname': 'Ivanov'}

Process finished with exit code 0

А вот когда дело касается наследование создание новых атрибутов допускается. При этом коллекция __dict__ по прежнему не будет содержать имена из коллекции __slots__.

CODE
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__)
RESULT
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__ ее следует передать явно, при этом атрибуты заново прописывать не требуется.

Полиморфизм

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

CODE
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())
RESULT
5810 41500
37200 18600

Process finished with exit code 0

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

CODE
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())
RESULT
5810 41500
37200 18600

5810
41500
37200
18600

Process finished with exit code 0

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

Обработка исключений. try / except

Исключениями в python называют ошибки и их обработка является достаточно важным и полезным навыком в программировании.

На схеме я постарался собрать все существующие на данный момент исключения. Как видно все исключения наследуются от BaseException, а далее основная масса ошибок от Exception. Зачем эта информация нужна и в чем заключается идея обработки исключений?

CODE
print(1 + 'b')
RESULT
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, говорящую, что действие невозможно. А теперь на следующий пример.

CODE
print(10 + 30)
print(1 + 'b')
print('a' + 'b')
RESULT
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.

CODE
print(10 + 30)
try:
    print(1 + 'b')
except TypeError:
    print('Числа и строки складывать нельзя')
print('a' + 'b')
RESULT
40
Числа и строки складывать нельзя
ab

Process finished with exit code 0

Синтаксис этой конструкции такой. В блок try помещается фрагмент кода, в котором предполагается возможность возникновения ошибки, а в блоке except происходит обработка предполагаемой ошибки и описывается действие, которое нужное сделать, если ошибка действительно возникнет. И программа как видно продолжает свое исполнение.

CODE
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')
RESULT
40
Числа и строки складывать нельзя
ab

Process finished with exit code 0

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

В этих двух примерах мы использовали конкретные типы ошибок, поскольку они нам известны. А что если мы заранее не знаем какой тип ошибки может возникнуть в данном фрагменте кода? Для этого я нарисовал тут схему исключений, из которой явно видно, что почти все ошибки наследуются от типа Exception. И достаточно частый случай обработки исключений это использования в блоке except типа Exception. Это достаточно частая реализация обработки исключений.

CODE                                                                                      CODE
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')
RESULT                                                                                   RESULT
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 или нет.

CODE                                                                                          CODE
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')
RESULT                                                                                       RESULT
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' возможно встретить достаточно часто. И если нам достаточно увидеть описание ошибки при ее возникновении достаточно распечатать это ключевое слово.

CODE  
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')
RESULT 
40
unsupported operand type(s) for +: 'int' and 'str'
Вызов блока finally
ab

Process finished with exit code 0

Конструкция try/except может быть вложенной. При такой реализации будет обработана первая найденная ошибка. Таким образом, на примере мы видим, что была обработана ошибка в третьей строке, но если закомментировать эту строку или написать ее без ошибок то будет обработана следующая найденная ошибка, в нашем случае IndexError.

CODE  
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')
RESULT 
40
b
list index out of range
Вызов блока else
Вызов блока finally
ab

Process finished with exit code 0

И хотелось бы еще обратить внимание на то, что блок else в данном случае будет исполнена, ведь блок else находится на одном уровне с той конструкцией try/except, в которой ошибки нет.

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

CODE  
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)
RESULT 
Введите два числа: 10 15
Что сделать с числами, разделить или умножить? умножить
150
Введите два числа: 30 0
Что сделать с числами, разделить или умножить? разделить
division by zero
Введите два числа: 0 15
Что сделать с числами, разделить или умножить? Разделить
0.0
Введите два числа: 15 15
Что сделать с числами, разделить или умножить? сложить
Напишите разделить или умножить
Введите два числа: 

Допустим напишем такой простенький калькулятор, который может только умножать и делить числа и делать это бесконечное количество раз. И без использования инструкции try/except при делении на ноль программа бы прекратила свое выполнение. Контролировать то, что введет пользователь мы, конечно, можем, но не во всех случаях. Вот один из элементарных, но достаточно наглядных примеров, когда без try/except программа не станет работать так как мы задумывали.

raise

Инструкция raise возбуждает исключение.

CODE  
raise TypeError('Ошибка')
RESULT 
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 с любым типом исключения, а после типа ошибки в скобках можно написать какой-нибудь текст, который будет выводиться вместе с этим исключением.

CODE  
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)
RESULT 
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 экземпляром класса, и если нет выводить исключение с соответствующим текстом.

CODE  
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)
RESULT 
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__', зачем это нужно? Как мы знаем по умолчанию каждая программа читается сверху вниз, а также во время запуска программы создается словарь с некоторым набором служебных переменных.

CODE  
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__)
RESULT 
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__' нужна для изменения этой самой точки входа. Для чего это нужно.

CODE  (lesson_30.py)                                                                     CODE (lesson_30_1.py) 
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__)
RESULT (lesson_30_1.py)
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__ можно увидеть, какая программа является главной, а какая пользуется возможностями этой программы.

CODE  (lesson_30.py)                                                                     CODE (lesson_30_1.py) 
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()
RESULT (lesson_30_1.py)
__main__

Process finished with exit code 0

Часто исполнение программы помещают в функцию main(), а далее задают новую точку входа в программу конструкцией if name == '__main__' и первым делом выполняют функцию main(). Теперь импорт всего содержимого и исполнение этой программы не выведет исполнение функций print() главного скрипта.

CODE  (lesson_30.py)                                                                     CODE (lesson_30_1.py) 
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()
RESULT (lesson_30_1.py)
5000 centimetres
5000 centimetres
1000 decimetres
0.1 kilometers
lesson_30
None
__main__

Process finished with exit code 0

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

CODE  (lesson_30.py)                                                                     CODE (lesson_30_1.py) 
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()
RESULT (lesson_30_1.py)
5000 centimetres
__main__

Process finished with exit code 0

Так же как и при импорте через оператор звездочка. При исполнении программы мы видим исполнение только одной единственной необходимой функции, без выполнения всех функций print(). Пишите код грамотно, это гораздо приятнее и для вас и для тех кто будет читать ваш код.

Что дальше?

На этом знакомство с основами python закончено, теперь вы можете прочитать и понять, пожалуй, любой фрагмент кода python, который попадется вам на глаза. Python это не только базовый синтаксис и ООП, python это огромное количество библиотек, предназначенных для огромного количества задач, которые решает этот язык. И следующий этап вашего обучения это выбор направления, которое вам более интересно, Django со всеми его вытекающими для современного backend'а веб-приложений; pandas, matplotlib, scipy и прочие библиотеки для анализа данных и машинного обучения и прочие возможности языка и библиотеки используемые для их реализации. Есть библиотеки, которые поставляются сразу с установкой python, другие библиотеки нужно устанавливать отдельно. Но не забывайте, все эти библиотеки в основе хранят все те типы данных, все те методы, все те функции и их особенности, все те классы и принципы их взаимодействия, с которыми мы познакомились в первых двух блоках посвященных python.
Далее только понимание чего вы хотите от программирования и бесконечная практика

Для отправки комментария необходимо авторизоваться



Комментарии

Здесь пока ничего нет...