Начнем издалека - создание объекта инстанцированием класса плохо совместимо с идеями ООП. Они гласят, что код должен зависеть от интерфейсов, а не от реализаций. До тех пор пока на вход нашему коду приходят готовые объекты - все хорошо. Он будет с готовностью принимать любые типы, реализующие требуемый интерфейс, но как только мы начинаем создавать новые объекты ситуация меняется. Теперь код зависит от конкретного класса, что усложняет следующие задачи:
- Изменение класса на другой, хоть и реализующий тот же интерфейс. Приходится вручную менять все точки инстанцирования, и, возможно, перекомпилировать код;
- Выбор конкретного класса на основе внешних условий или точки инстанцирования;
- Использование уже готового объекта - взятого из пула или какого то конкретного (синглетон);
- Построение объекта с большим количеством зависимостей - приходиться передавать в точку конструирования все данные для построения множества взаимосвязанных объектов;
- Не классическая проблема для ICC, но из той-же области:
class A(object):
def __init__(self, val):
self.val = val
def __add__(self, val):
return A(self.val + val)
class B(A):
pass
print B(1) + 1 # <__main__.A object at 0x18877d0>
А хотелось бы получить экземпляр В.
Все эти проблемы связаны общей причиной - код делает работу, которую он делать не должен - инстанцирование конкретных классов. На самом деле чаще всего нам не нужен фиксированный класс. Нам нужен класс, предоставляющий определенный интерфейс. Нужно отвязаться от явного указания класса и передать его создание стороннему коду. Фактически мы хотим "виртуализовать" инстанцирование.
В самом простом случае можно воспользоваться фабричной функцией(ФФ). Если же мы хотим конфигурировать поведение ФФ, или сохранять состояние между вызовами (синглетон, пул объектов, etc), то логично сделать ФФ методом класса, в экземпляре которого будут храниться настройки. Такой класс может быть синглетоном(если конфигурация глобальная), или передаваться образом по цепочке вызовов во все точки, где нужно инстанцирование. Этот класс как раз и называется Inversion of Control Container (ICC дальше).
Для его использования нужно заменить прямое инстанцирование классов на вызов метода ICC. Параметрами метода будут требуемый интерфейс, и, возможно, контекст вызова и часть параметров для конструктора (последнее применяется редко). ICC возвращает готовый экземпляр. Конкретный класс для инстанцирования и параметры конструктора настраиваются програмно или берутся из конфигурационного файла.
Типичный пример - создание виртуальной машины в libvirt. Основная функция API принимает xml строку, описывающую виртуальную машину. Эта строка чаще всего берется вызывающим кодом из внешнего источника, потому как в большинстве случаев ему не важны подробности конфигурации для работы с VM соответственно и код создания можно унифицировать, а строку с конфигурацией использовать как черный ящик.
ICC также можно рассматривать как шаблон проектирования, объединяющий и унифицирующий другие порождающие шаблоны - ФФ, синглетон, и прочее.
Java и C# имеет различные реализации ICC (java spring, dagger) которые используются очень широко. Для питона же они практически не применяются. Сначала я покажу как написать pythonic ICC, а потом рассмотрю почему он не нужен. Написание своего связанно с тем, что по уже готовые пишутся людьми только что пришедшими с Java/C# и не отличаются питонистичностью.
Итак что можно хотеть от идеального ICC? Во-первых оставить пользовательский код почти без изменений. Во-вторых поддерживать возможность возвращать при инстанцировании целевого класса экземпляры другого класса, или определенный объект или результат вызова некоторой функции.
Итак был такой код:
Без подсветки синтаксисаclass Bee(object):
def __init__(self, x):
pass
class Cee(object):
def __init__(self, x):
pass
assert isinstance(Bee(1), Bee)
Мы хотим иметь возможность не меняя код инстанцирования Bee выбирать что именно будет получаться - экземпляр Bee или Cee. С позиции duck typing классы Bee и Cee реализуют один и тот-же интерфейс и взаимозаменяемы, хоть мы это и не декларируем явным наследованием.
В принципе инстанцирование можно и не менять, но тогда его поведение будет не совсем очевидным. С первого взгляда кажется, что мы инстанцируем обычный класс Bee, а в итоге получаем экземпляр другого класса, который к классу Bee никакого отношения не имеет. Т.е. isinstance(Bee(), Bee) == False. Поэтому немного изменим пример. Bee и Cee будут наследовать общий интерфейс IBee и именно этот интерфейс мы и будем инстанцировать.
Без подсветки синтаксисаclass IBee(IOCInterface):
def __init__(self, x):
pass
class Bee(IBee):
def __init__(self, x):
print "Bee.__init__ called"
class Cee(IBee):
def __init__(self, x):
print "Cee.__init__ called"
IBee.register(Bee)
assert isinstance(IBee(1), Bee)
IBee.register(Cee)
assert isinstance(IBee(1), Cee)
Что бы это работало нужно перехватить конструирование объекта типа IBee и вернуть что-мы-там-хотим. Для этого вспоминаем, что конструирование объекта в python выражается следующим псевдокодом:
Без подсветки синтаксиса# obj = Cls(x, y) ==>
obj = Cls.__new__(Cls, x, y)
if isinstance(obj, Cls):
Cls.__init__(obj, x, y)
Т.е. Cls.__new__ возвращает пустой экземпляр типа Cls, Cls.__init__ наполняет его реальными данными. Очень похоже на operator new + конструктор в С++. Итак нам нужно перегрузить IBee.__new__ и возвращать из него наш объект.
Без подсветки синтаксисаioc = {}
class IOCInterface(object):
def __new__(cls, *args, **kwargs):
return ioc[cls](cls, *args, **kwargs)
@classmethod
def register(cls, impl):
factory = lambda ccls, *args, **kwargs: \
super(IOCInterface, ccls).__new__(impl, *args, **kwargs)
cls.register_factory(factory)
@classmethod
def register_instance(cls, obj):
cls.register_factory(lambda *args, **kwargs: obj)
@classmethod
def register_factory(cls, func):
ioc[cls] = func
Немного пояснений. Класс IOCInterface будет базовым для всех интерфейсов. Переменная ioc будет хранить текущую конфигурацию - отображение интерфейса на фабричную функцию для этого интерфейса. Для простоты примера мы будем хранить конфигурацию в глобальной переменной. Перегруженный метод __new__ получает инстанцируемый класс первым параметром, а дальше идут параметры конструктора. Он берет зарегистрированную для этого класса фабричную функцию и создает новый объект с ее помощью. IOCInterface.register позволяет зарегистрировать класс для данного интерфейса. IOCInterface.register_instance - зарегистрировать синглетон. Для унификации они создают специальные фабричные функции.
Замечания:
- Нельзя использовать cls.__new__ как фабричную функцию в IOCInterface.register, так как мы получим вечный цикл. Нужно "проскочить" IOCInterface в иерархии сcls;
- Для классов с перегруженным __new__ нужно смотреть по ситуации;
- Есть соблазн просто сохранять класс/синглетон в словарь и потом в __new__
делать что-то вида;
Без подсветки синтаксисаdef __new__(cls, *args, **kwargs):
obj = ioc[cls]
if isinstance(obj, type):
return obj(cls, *args, **kwargs)
elif type(obj, (types.FunctionType, types.LambdaType)):
return obj(cls, *args, **kwargs)
else:
return obj
Делать этого не стоит, хотя бы потому что так мы не сможем зарегистрировать экземпляр класса с перегруженным __call__ или функцию как синглетон для типа
- Теперь мы не может инстанцировать классы, которые прямо или опосредованно наследуют IOCInterface, без регистрации для них реализации. Т.е. Bee(1) вывалится с KeyError. В принципе это соответствует духу происходящего - мы запрещаем прямое инстанцирование классов. Разве что стоит перехватывать KeyError и генерировать понятное сообщение. Или изменить __new__ вот так
def __new__(cls, *args, **kwargs):
return ioc.get(cls, super(IOCInterface, cls).__new__)(cls, *args, **kwargs)
Тогда по-умолчанию Bee(1) будет создавать экземпляр Bee
- В продолжение к предыдущему пункту - можно зарегистрировать фабричные функции и для IBee и для Bee;
- У этого кода есть проблема с синглетонами. Если мы регистрируем для IBee синглетоном экземпляр IBee или любого производного от него класса, то при каждом вызове IBee() для этого класса будет заново вызываться конструктор. Нужно или отслеживать и игнорировать повторные вызовы конструктора, или сделать его пустым и вынести инициализацию в отдельный метод.
Но в питоне есть еще один способ перехватить конструирование нового экземпляра. Рассмотрим запись b = Bee(1). Что тут написано? Вызывается объект/функция Bee с параметром 1. Компилятор питона не имеет никакой семантической информации о программе (в отличии от компилятора, например, С++) - он владеет только синтаксической информацией. Для него len(a) и IBee(a) это просто вызов объекта. Т.е. питон превратит IBee(a) в IBee.__class__.__call__(IBee, 1). IBee.__class__ - это метакласс IBee. Где же происходит вызов IBee.__new__ и IBee.__init__? В type.__call__. Вызов __new__, а затем конструктора - это то, что записано в стандартном метаклассе Т.е. все выглядит примерно так:
Без подсветки синтаксисаclass type:
# ...
def __call__(self, *args, **kwargs):
obj = Cls.__new__(Cls, x, y)
if isinstance(obj, Cls):
Cls.__init__(obj, x, y)
return obj
Соответственно написав свой метакласс и перегрузив __call__ мы избавимся от проблемы с синглетоном.
Без подсветки синтаксисаclass IOCMeta(type):
def __call__(self, *args, **kwargs):
return ioc[self](self, *args, **kwargs)
class IOCInterface2(object):
__metaclass__ = IOCMeta
@classmethod
def register(cls, impl):
def factory(ccls, *args, **kwargs):
return type.__call__(impl, *args, **kwargs)
cls.register_factory(factory)
@classmethod
def register_instance(cls, obj):
cls.register_factory(lambda *args, **kwargs: obj)
@classmethod
def register_factory(cls, func):
ioc[cls] = func
Класс IOCInterface2 лишен проблемы с повторным вызовом __init__ для синглетона. Посмотрим как все получается, заодно прорекламирую всем замечательную замену для assert, assertEquals и др. - oktest:
Без подсветки синтаксисаfrom oktest import ok
from python_ioc import IOCInterface2
# ...
IBee.register(Bee)
ok(IBee(1)).is_a(Bee)
IBee.register(Cee)
ok(IBee(1)).is_a(Cee)
IBee.register_instance(1)
ok(IBee(1)) == 1
print "All tests passed"
print "'Bee.__init__ called' message should appears once and only once"
IBee.register(Bee)
IBee.register_instance(IBee(1))
IBee(1)
IBee(1)
Наконец допишем поддержку контекста с помощью конструкции with (этот вариант поддерживает только регистрацию классов):
Без подсветки синтаксиса@contextlib.contextmanager
def ioc_set(new_context):
global ioc
old_ioc = ioc.copy()
for k, v in new_context.items():
k.register(v)
try:
yield
finally:
ioc = old_ioc
Использование
Без подсветки синтаксисаIBee.register(Bee)
ok(IBee(1)).is_a(Bee)
with ioc_set({IBee: Cee}):
# until we exits with block IBee(xx) will gives an instance of Cee
ok(IBee(1)).is_a(Cee)
ok(IBee(1)).is_a(Bee)
Использование контекста можно немного упростить, избежав {...} и вместо этого записав подменяемые классы в виде ioc_set(IBee=Cee, ....). Cделать так не совсем просто, поскольку питон передает IBee как строку "IBee", а не как объект IBee.Но можно подняться из ioc_set на один кадр стека вверх и посмотреть, что там лежит по имени "IBee":
Без подсветки синтаксисаdef ioc_set_hack(**kwargs):
fr = sys._getframe(1)
result_map = {}
for name, obj in kwargs.items():
if name in fr.f_locals:
interface = fr.f_locals[name]
else:
interface = fr.f_globals[name]
result_map[interface] = obj
return ioc_context(result_map)
with ioc_set_hack(IBee=Cee):
ok(IBee(1)).is_a(Cee)
Я бы не советовал использовать эту функцию, поскольку ее поведение совсем не очевидно. Вообще я против превращения питона в DSL, а то есть шанс получить на выходе читаемость, как местами у scala.
К полученным классам довольно легко прикрутить все возможности "серьезных" ICC: загрузку конфигураций из внешних файлов, поддержку многопоточности и прочее.
Итак ICC пишется в питоне очень просто - 1, 2, 3 и используется даже проще, чем в Java/C#, так почему же их нигде нет? Почему авторы этих фреймворков пишут, что они от них отрекаются?
А дело в том, что ICC, как шаблон проектирования, незаметно встроен в питон повсюду и встроенные возможности покрывают бОльшую часть случаев его применения. Ради оставшихся единичных примеров не стоит городить весь огород. YAGNI и KISS в чистом виде.
Принципиальная разница между Java и python состоит в том когда в конструкции инстанцирования происходит связывание объекта с именем. В Java - процессе компиляции. Если вы пишете obj = new SomeClass(), то компилятор совершенно точно знает, что это инстанцирование объекта и совершенно точно знает - какого. Без перекомпиляции эта строка никогда не будет значить ничего другого. В python, как я уже писал выше, запись obj = SomeClass() означает следующее - найди в пространстве имен ХХХ (globals() или locals()) объект по имени 'SomeClass' и вызови его. globals() выступает ICC, который мы конфигурируем во время исполнения программы:
Без подсветки синтаксиса# same as SomeClass = type('SomeClass', (object,), {})
class SomeClass(object):pass
SomeOtherClass = SomeClass
Конструкция class в питоне всего лишь синтаксический сахар для объявления переменной, хранящей объект типа. Разница с приведенным выше ICC состоит в том, что в данном случае мы запрашиваем класс по имени внутри контейнера, а не по интерфейсу.
Точно так же каждый модуль в питоне является ICC контейнером, обращение к которому происходит взятием атрибута. mod.name это и есть разрешение реализации, привязанной к имени.
Вместо ручного присваивания можно использовать подменяющие функции, например patch. Которая работает подобно ioc_set (но умеет гораздо больше):
Без подсветки синтаксисаclass Class(object):
pass
with patch('__main__.Class', list):
ok(Class()).is_a(list)
Однако модель подмены по имени имеет одну слабость:
Без подсветки синтаксиса# some_module.py
class Class(object):
pass
# main.py
import some_module
from some_module import Class
def print_tp():
print some_module.Class()
print Class()
# unit_test.py
import main
with patch('some_module.Class', list):
print_tp() # only some_module.Class() patched
with patch('some_module.Class', list):
with patch('main.Class', list):
print_tp() # both some_module.Class() and Class() patched
Проблема в том, что один объект может иметь множество имен и для корректной работы нужно или сделать подмену раньше, чем другие модули создадут новый имена или заменить их все. Впрочем это не большая расплата за возможность оставить код чистым от ICC и за возможность работать с классами и модулями, которые про ICC не знают. Так же проблема смягчается тем, что в основном использование ICC - это юниттесты и в них простительно некоторое количество таких ручных хаков.
Проблема с инстанцированием корректного класса решается совсем просто:
Без подсветки синтаксисаclass A(object):
def __init__(self, val):
self.val = val
def __add__(self, val):
#return A(self.val + val)
return self.__class__(self.val + val)
class B(A):
pass
print B(1) + 1 # <__main__.В object at 0x18877d0>
Относительно других причин использовать ICC: В питоне практически никогда не нужно создавать сложные иерархии объектов. Такое API не питонистично и врядли будет пользоваться популярностью. Три модуля, которые приходят на ум - urllib2, logging и unittest. Два последние "слизаны" с явовских модулей - log4j и junit соответственно. Для logging фактически есть встроенный ICC - построение дерева логеров по конфигурационному файлу, вместо unittest все использовали nosetests (в 2.7 его починили и теперь для юниттеста не нужно инстанцировать половину вселенной). А с urllib2 почти все пересели на requests.
Без подсветки синтаксисаclass A(object):
def __init__(self, val):
self.val = val
def __add__(self, val):
return A(self.val + val)
class B(A):
pass
print B(1) + 1 # <__main__.A object at 0x18877d0>
Синглетон почти не нужен, поскольку нет проблем с порядком инициализации глобальных переменных, даже в многопсточной программе.
Наконец в питоне вызов функции не отличим от инстанцирования класса. Что-бы получить эту возможность в скале даже нагородили уродливый (и нарушающий DRY) огород из companion object. Так что если что-то у вас сегодня - класс, а завтра - фабричная функция, это нормально. Почти никто не заметит.
Ссылки:en.wikipedia.org/wiki/Don%27t_repeat_yourself
gbracha.blogspot.com/2007/06/constructors-considered-harmful.html
daily-scala.blogspot.com/2009/09/companion-object.html
logging.apache.org/log4j/1.2/manual.html
ru.wikipedia.org/wiki/JUnit
www.python-requests.org/en/latest
www.voidspace.org.uk/python/mock/patch.html
lostechies.com/ryansvihla/2009/11/16/i-recant-my-ioc-ioc-containers-in-dynamic-languages-are-silly
ru.wikipedia.org/wiki/YAGNI
ru.wikipedia.org/wiki/KISS_%28%D0%BF%D1%80%D0%B8%D0%BD%D1%86%D0%B8%D0%BF%29
springpython.webfactional.com
code.google.com/p/pinsor
github.com/rande/python-simple-ioc
www.kuwata-lab.com/oktest/oktest-py_users-guide.html
en.wikipedia.org/wiki/Factory_method_pattern
koder-ua.blogspot.com/2011/12/libvirt-co-1.html
ru.wikipedia.org/wiki/Spring_Framework#Inversion_of_Control
github.com/square/dagger
Исходники этого и других постов со скриптами лежат тут - github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.
No comments:
Post a Comment