Python - как я могу заставить использование фабричного метода для создания объекта?

У меня есть набор связанных классов, которые все наследуются от одного базового класса. Я хотел бы использовать фабричный метод для создания экземпляров объектов для этих классов. Я хочу сделать это, потому что тогда я могу сохранить объекты в словаре, снабженном ключом по имени класса, прежде чем возвращать объект вызывающей стороне. Затем, если есть запрос на объект определенного класса, я могу проверить, существует ли он уже в моем словаре. Если нет, я его создаю и добавляю в словарь. Если так, то я верну существующий объект из словаря. Это по существу превратит все классы в моем модуле в синглтоны.

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

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

Это хорошая идея?

Изменить: Вот исходный код для моего базового класса. Мне сказали, что мне нужно выяснить метаклассы, чтобы сделать это более элегантно, но это то, что у меня есть сейчас. Все объекты Page используют один и тот же экземпляр Selenium Webdriver, который находится в модуле драйвера, импортированном вверху. Этот драйвер очень дорог для инициализации - он инициализируется при первом создании LoginPage. После инициализации initialize() Метод вернет существующий драйвер вместо создания нового. Идея состоит в том, что пользователь должен начать с создания LoginPage. В конечном итоге будут определены десятки классов Page, и они будут использоваться кодом модульного тестирования для проверки правильности поведения веб-сайта.

from driver import get_driver, urlpath, initialize
from settings import urlpaths

class DriverPageMismatchException(Exception):
    pass

class URLVerifyingPage(object):
    # we add logic in __init__() to check the expected urlpath for the page
    # against the urlpath that the driver is showing - we only want the page's
    # methods to be invokable if the driver is actualy at the appropriate page.
    # If the driver shows a different urlpath than the page is supposed to
    # have, the method should throw a DriverPageMismatchException

    def __init__(self):
        self.driver = get_driver()
        self._adjust_methods(self.__class__)

    def _adjust_methods(self, cls):
        for attr, val in cls.__dict__.iteritems():
            if callable(val) and not attr.startswith("_"):
                print "adjusting:"+str(attr)+" - "+str(val)
                setattr(
                    cls,
                    attr,
                    self._add_wrapper_to_confirm_page_matches_driver(val)
                )
        for base in cls.__bases__:
            if base.__name__ == 'URLVerifyingPage': break
            self._adjust_methods(base)

    def _add_wrapper_to_confirm_page_matches_driver(self, page_method):
        def _wrapper(self, *args, **kwargs):
            if urlpath() != urlpaths[self.__class__.__name__]:
                raise DriverPageMismatchException(
                    "path is '"+urlpath()+
                    "' but '"+urlpaths[self.__class.__name__]+"' expected "+
                    "for "+self.__class.__name__
                )
            return page_method(self, *args, **kwargs)
        return _wrapper


class LoginPage(URLVerifyingPage):
    def __init__(self, username=username, password=password, baseurl="http://example.com/"):
        self.username = username
        self.password = password
        self.driver = initialize(baseurl)
        super(LoginPage, self).__init__()

    def login(self):
        driver.find_element_by_id("username").clear()
        driver.find_element_by_id("username").send_keys(self.username)
        driver.find_element_by_id("password").clear()
        driver.find_element_by_id("password").send_keys(self.password)
        driver.find_element_by_id("login_button").click()
        return HomePage()

class HomePage(URLVerifyingPage):
    def some_method(self):
        ...
        return SomePage()

    def many_more_methods(self):
        ...
        return ManyMorePages()

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

4 ответа

В Python почти никогда не стоит пытаться "форсировать" что-либо. Что бы вы ни придумали, кто-то может обойти это, обезличив ваш класс, скопировав и отредактировав исходный код, пошарив с байт-кодом и т. Д.

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

Если вы просто пытаетесь предотвратить несчастные случаи, а не преднамеренное "неправильное использование", это другая история. На самом деле это просто стандартный дизайн по контракту: проверьте инвариант. Конечно, на данный момент, SillyBaseClass уже облажался, и уже слишком поздно, чтобы восстановить его, и все, что вы можете сделать, это assert, raise, logили что еще уместно. Но это то, что вы хотите: это логическая ошибка в приложении, и единственное, что нужно сделать, это заставить программиста это исправить, так assert вероятно, именно то, что вы хотите.

Так:

class SillyBaseClass:
    singletons = {}

class Foo(SillyBaseClass):
    def __init__(self):
        assert self.__class__ not in SillyBaseClass.singletons

def get_foo():
    if Foo not in SillyBaseClass.singletons:
        SillyBaseClass.singletons[Foo] = Foo()
    return SillyBaseClass.singletons[Foo]

Если вы действительно хотите помешать этому, вы можете проверить инвариант ранее, в __new__ метод, но разве чтоSillyBaseClass облажался "эквивалентно" запустить ядерное оружие ", зачем?

Похоже, вы хотите предоставить __new__ реализация: что-то вроде:

class MySingledtonBase(object):
    instance_cache = {}
    def __new__(cls, arg1, arg2):
        if cls in MySingletonBase.instance_cache:
            return MySingletonBase.instance_cache[cls]
        self = super(MySingletonBase, cls).__new__(arg1, arg2)
        MySingletonBase.instance_cache[cls] = self
        return self

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

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

То есть как то так:

class _InternalSubClassOne(_BaseClass):
    ...

class _InternalSubClassTwo(_BaseClass):
    ...

# An example factory function.
def new_object(arg):
    return _InternalSubClassOne() if arg == 'one' else _InternalSubClassTwo()

Я бы также добавил строки документации или комментарии к каждому классу, например: "Не создавайте экземпляр этого класса вручную, используйте метод factory new_object".

Вы также можете просто вкладывать классы в фабричный метод, как описано здесь:https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Factory.html#preventing-direct-creation

Рабочий пример из указанного источника:

      # Factory/shapefact1/NestedShapeFactory.py
import random

class Shape(object):
    types = []

def factory(type):
    class Circle(Shape):
        def draw(self): print("Circle.draw")
        def erase(self): print("Circle.erase")

    class Square(Shape):
        def draw(self): print("Square.draw")
        def erase(self): print("Square.erase")

    if type == "Circle": return Circle()
    if type == "Square": return Square()
    assert 0, "Bad shape creation: " + type

def shapeNameGen(n):
    for i in range(n):
        yield factory(random.choice(["Circle", "Square"]))

# Circle() # Not defined

for shape in shapeNameGen(7):
    shape.draw()
    shape.erase()

Я не фанат этого решения, просто хочу добавить это как еще один вариант.

Другие вопросы по тегам