Python — Наследование: совместное использование объектов между экземплярами

Пример использования — мотивация и вызов

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

Чтобы начать мое обучение, Я решил автоматизировать повторяющуюся еженедельную задачу по заполнению некоторых табелей учета рабочего времени. расположен в Microsoft Teams, используя бота, который выполняет за меня тяжелую работу. Бот должен выполнить следующие действия:

  • Перейдите на страницу входа
  • Введите логин и пароль
  • Войти
  • Перейдите на страницу Excel с расписанием
  • Заполните мои недельные часы

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

Разбор кода

Код довольно простой. Я сильно полагаюсь на селен для выполнения всех действий, поэтому я хочу создать экземпляр chrome, в котором агент будет выполнять свои действия.

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

import os
import time
import random

from selenium import webdriver
from dataclasses import dataclass
from abc import ABC, abstractmethod
from webdriver_manager.chrome import ChromeDriverManager

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

@dataclass(frozen=True)
class XPathsContainer:
    teams_login_button: str="//*[@id="mectrl_main_trigger"]/div/div[1]"
    teams_login_user_button: str="//*[@id="i0116"]"
    teams_login_next_button: str="//*[@id="idSIButton9"]"
    teams_login_pwd_button: str="//*[@id="i0118"]"
    teams_sign_in_button: str="//*[@id="idSIButton9"]"
    teams_sign_in_keep_logged_in: str="//*[@id="KmsiCheckboxField"]"


@dataclass(frozen=True)
class UrlsContainer:
    teams_login_page: str="https://www.microsoft.com/en-in/microsoft-365/microsoft-teams/group-chat-software"

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

class Driver(ABC):
    def __init__(self, action, instruction, driver=None):
        if driver:
            self.driver = driver
        else:
            self.driver = webdriver.Chrome(ChromeDriverManager().install())

        self.actions = {
            'navigate': self.navigate,
            'click': self.click,
            'write': self.write
        }

        self.parameters = {
            'action': None,
            'instruction': None
        }

    @abstractmethod
    def sleep(self, current_tick=1):
        pass

    @abstractmethod
    def navigate(self, *args):
        pass

    @abstractmethod
    def click(self, *args):
        pass

    @abstractmethod
    def write(self, **kwargs):
        pass

    @abstractmethod
    def main(self, **kwargs):
        pass

Сейчас реализую базовый Agent дочерний класс, реализующий логику требуемых функций базового класса Driver.

class Agent(Driver):
    def __init__(self, action, instruction, driver):
        super().__init__(action, instruction, driver)
        self.action = action
        self.instruction = instruction

    def sleep(self, current_tick=1):
        seconds = random.randint(3, 7)
        timeout = time.time() + seconds
        while time.time() <= timeout:
            time.sleep(1)
            print(f"Sleeping to replicate user.... tick {current_tick}/{seconds}")
            current_tick += 1

    def navigate(self, url):
        print(f"Agent navigating to {url}...")
        return self.driver.get(url)

    def click(self, xpath):
        print(f"Agent clicking in '{xpath}'...")
        return self.driver.find_element_by_xpath(xpath).click()

    def write(self, args):
        xpath = args[0]
        phrase = args[1]
        print(f"Agent writing in '{xpath}' the phrase '{phrase}'...")
        return self.driver.find_element_by_xpath(xpath).send_keys(phrase)

    def main(self, **kwargs):
        self.action = kwargs.get('action', self.action)
        self.instruction = kwargs.get('instruction', self.instruction)
        self.actions[self.action](self.instruction)
        self.sleep()

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

def update_driver_parameters(driver, values):
    params = driver.parameters
    params['action'] = values[0]
    params['instruction'] = values[1]
    return params


def run_script(script):
    for script_line, script_values in SCRIPT.items():
        chrome = Agent(None, None, None)

        for instructions in script_values:
            params = update_driver_parameters(chrome, instructions)
            chrome.main(**params)
        chrome.sleep()


USER = os.environ["USERNAME"]
SECRET = os.environ["SECRET"]

SCRIPT = {
    'login': [
        ('navigate', UrlsContainer.teams_login_page),
        ('click', XPathsContainer.teams_login_button),
        ('write', (XPathsContainer.teams_login_user_button, USER)),
        ('click', XPathsContainer.teams_login_next_button),
        ('write', (XPathsContainer.teams_login_pwd_button, SECRET)),
        ('click', XPathsContainer.teams_sign_in_button),
        ('click', XPathsContainer.teams_sign_in_keep_logged_in),
        ('click', XPathsContainer.teams_sign_in_button),

    ]
}
run_script(SCRIPT)

Обеспокоенность

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

  • Я слишком полагаюсь на Xpaths, чтобы заставить бота делать что-то, что приведет к огромному классу данных, если нужно сделать много шагов;
  • Кроме того, полагаться на Xpaths может быть плохо, потому что, если страница будет обновлена, мне придется повторить шаги, но это, вероятно, неизбежное зло;
  • Я не уверен, правильна ли реализация неизменяемого класса. Я использовал dataclass за это;
  • У меня такое ощущение, что реализованное мной наследование довольно неуклюже. Я хочу иметь возможность использовать один и тот же драйвер вместе с несколькими классами. Я не хочу создавать новый драйвер для каждого действия, я всегда хочу получить последний контекст, который сделал драйвер, но если создается новый агент, то этому агенту должен быть назначен новый драйвер;
  • Может быть kwargs аргументы могут быть реализованы по-разному, я никогда не уверен, как правильно их проанализировать без использования kwargs.get;
  • Непоследовательное использование args и kwargs, можно ли это реализовать по-другому?

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *