python

Зачем мне гибкость Python, если мне запрещают ей пользоваться?

  • четверг, 5 октября 2017 г. в 03:12:48
https://habrahabr.ru/post/339272/
  • ООП
  • Ненормальное программирование
  • Python


Здравствуйте! Есть Была у меня следующая задача: надо было спарсить кучу данных и организовать их в классы, а позже загрузить в БД. Вроде бы, ничего сложного, но в этот день я даже забыл поесть, а почему — смотрите под кат, потому что я сделяль.

image

Данных, конечно же, было много, но задачу это никак не усложнило, усложнило то, что один и тот же элемент можно было найти в разных уголках сайта. Эти данные можно сравнить с аккаунтами в социальных сетях. Один и тот же аккаунт может оставить свой след везде — и лайки на разных страничках пооставлять, и комментарии везде написать, и на стенку разным людям что-нибудь повесить. И нужно, чтобы всё это был один и тот же объект в нашей программе и чтобы он никак не дублировался. Вроде бы, всё просто, проверяй себе, был ли найден этот элемент уже — и всё. Но это некрасиво, это не тру. Да и противоречит философии Python. Хотелось красивого решения, что-то, что просто запрещало бы создание элемента, который уже существует или просто не создавало бы его, всю инициализацию игнорировало бы, а внутренний конструктор возвращал уже существующий элемент.

Приведу пример. У меня есть, например, сущность.

class Animal:
	def __init__(self, id):
		self.id=id

И каждая такая сущность имеет свой уникальный id.

В итоге, находя две одинаковых сущности в разных местах, мы создаём 2 абсолютно одинаковых объекта. Первое, что нужно, это добавить какое-то хранилище объектов:

class Animal:
	__cache__=dict()

	def __init__(self, id):
		self.id=id

Новый объект в python создаётся в функции __new__ класса, эта функция должна вернуть новый созданный объект, и именно в ней нам и надо копаться для переопределения поведения создания элемента.

class Animal:
	__cache__=dict()

	def __new__(cls, id):
		if not id in Animal.__cache__:
			Animal.__cache__[id]=super().__new__(cls)
		return Animal.__cache__[id]

	def __init__(self, id):
		self.id=id

Вот, вроде бы, и всё, задача решена. Думал я первые 20 минут. При расширении программы и увеличении классов я стал получать ошибку наподобии: __init__() required N positional argument

Проблема заставила меня выйти в google с поиском того, что, может, я сделал совсем всё против правил. Оказалось, да. Они мне говорят, чтобы я не лез в метод __new__ без нужды, а альтернативу предложили Factory pattern.

Вкратце, Factory pattern состоит в том, что мы выделяем место, которое управляет созданием объектов. Для Python они предложили вот такой пример

class Factory:      
    def register(self, methodName, constructor, *args, **kargs):
        """register a constructor"""
        _args = [constructor]
        _args.extend(args)
        setattr(self, methodName,apply(Functor,_args, kargs))
        
    def unregister(self, methodName):
        """unregister a constructor"""
        delattr(self, methodName)

class Functor:
    def __init__(self, function, *args, **kargs):
        assert callable(function), "function should be a callable obj"
        self._function = function
        self._args = args
        self._kargs = kargs
        
    def __call__(self, *args, **kargs):
        """call function"""
        _args = list(self._args)
        _args.extend(args)
        _kargs = self._kargs.copy()
        _kargs.update(kargs)
        return apply(self._function,_args,_kargs)

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

Немного изучения процесса создания дало мне ответ. Создание объекта (вкратце) происходит следующим образом: сначала вызывается метод __new__, в который передаётся класс и все аргументы конструктора, этот метод создаёт объект и возвращает его. Позже вызывается метод __init__ класса, к которому принадлежит объект.

Абстрагированный код:

def __new__(cls, id, b, k, zz):
	return super().__new__(cls)
def __init__(self, id, b, k, zz):
	# anything
	self.id=id
obj=Animal.__new__(Animal, 1, 2, k=3, zz=4)
obj.__class__.__init__(obj, 1, 2, k=3, zz=4)

Проблема вылезла при следующем действии. Например, я добавляю класс Cat

class Cat(Animal):
	data="data"
	
	def __init__(self, id, b, k, zz, variable, one_more_variable):
		# anything
		pass

Как видите, конструкторы у классов разные. Представим, что мы уже создали объект Animal с id=1. Позже создаём элемент Cat с id=1.

Объект класса Animal с id=1 уже существует, так что по логике вещей объект класса Cat не должен создаться. В общем, он этого и не делает, а завершает ошибку с тем, что __init__ передано разное количество аргументов.

Как Вы поняли, он пытается создать элемент класса Cat, но позже вызывает конструктор класса Animal. Мало того, что он вызывает не тот конструктор, совсем плохим результатом является то, что даже если бы мы снова создавали Animal с id=1, конструктор для одного и того же объекта вызвался повторно. И, возможно, перезаписал бы все данные и сделал бы нежелательные действия.

Нехорошо. Ещё есть смысл отступить и создать фабрику по производству объектов.
Но ведь мы пишем на Python, самом гибком и красивом языке, почему мы должны идти на уступки.

Как оказалось, решение есть:

class Animal:
	__cache__=dict()
	__tmp__=None

	def __fake_init__(self, *args, **kwargs):
		self.__class__.__init__=Animal.__tmp__
		Animal.__tmp__=None

	def __new__(cls, id):
		if not id in Animal.__cache__:
			Animal.__cache__[id]=super().__new__(cls)
		else:
			Animal.__tmp__=Animal.__cache__[id].__class__.__init__
			Animal.__cache__[id].__class__.__init__=Animal.__fake_init__
		return Animal.__cache__[id]

	def __init__(self, id):
		self.id=id

Вызов конструктора отключить было невозможно, после выполнения __new__ беспрекословно шёл вызов функции __init__ из класса созданного (или нет, как в нашем случае) объекта. Выход был один — заменить __init__ в классе созданного объекта. Чтобы не потерять конструктор класса, я его сохранил в какую-нибудь переменную и позже вместо него подсунул фейковый конструктор, который потом вызывался при «создании» объекта. Но фейковый конструктор не пустой, он именно и занимается тем, что возвращает старый конструктор на своё место.

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

an1=Animal(1)
an2=Animal(1)
cat1=Cat(1)

и не беспокоиться о проблемах.

Спасибо за внимание!