Использование метаклассов в Python

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



    Введение в метаклассы



    Итак, классический ООП подразумевает наличие только классов и объектов.
    Класс -шаблон для объекта; при объявлении класса указывается вся механика
    работы каждого конкретного «воплощения»: задаются данные, инкапсулируемые
    в объекте, и методы для работы этими данными.


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

    От метакласса можно наследоваться, получая новый метакласс, который, в свою
    очередь, можно использовать при определении новых классов. Таким образом,
    появляется новое «измерение» наследования, добавляющееся к иерархии наследования
    классов: метакласс -> класс -> объект.

    Простой пример



    Предположим, нас утомило задание атрибутов в контрукторе __init__(self, *args,
    **kwargs). Хотелось бы ускорить этот процесс таким образом, чтобы была
    возможность задавать атрибуты прямо при создании объекта класса. С обычным
    классом такое не пройдет:

      >>>class Man(object):
      >>>    pass
      >>>me = Man(height = 180, weight = 80)
      Traceback (most recent call last):
      File "<stdin>", line 20, in <module>
          TypeError: object.__new__() takes no parameters
    


    Объект конструируется вызовом класса оператором "()". Создадим наследованием от
    type метакласс, переопределяющий этот оператор:


      >>>class AttributeInitType(type):
      >>>    def __call__(self, *args, **kwargs):
      >>>        """ Вызов класса создает новый объект. """
      >>>        # Перво-наперво создадим сам объект...
      >>>        obj = type.__call__(self, *args)
      >>>        # ...и добавим ему переданные в вызове аргументы в качестве атрибутов.
      >>>        for name in kwargs:
      >>>            setattr(obj, name, kwargs[name])
      >>>        # вернем готовый объект
      >>>        return obj
    

    Теперь создадим класс, использующий новый метакласс:

      >>>class Man(object):
      >>>    __metaclass__ = AttributeInitType
    

    Вуаля:

      >>>me = Man(height = 180, weigth = 80)
      >>>print me.height
      180
    


    Расширение языка (абстрактные классы)


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


    Вместе с тем, программистам, занимающимся созданием, например, фреймворков и
    сопутствующих специальных подъязыков (Domain Specific Languages), предоставляются
    достаточно гибкие инструменты.

    Абстрактные классы (или их несколько иная форма — интерфейсы) — распространенный
    и популярный среди программистов метод определения интерфейсной части
    класса. Обычно такие понятия закладываются в ядро языка (как в Java или C++),
    Питон же позволяет изящно и легко реализовать их собственными средствами, в
    частности — при помощи метаклассов и декораторов.

    Рассмотрим работу библиотеки abc из предложения по реализации для стандартной библиотеки.

    abc


    Использовать асбтрактные классы очень легко. Создадим абстрактный базовый класс
    с виртуальным методом и попробуем создать класс-наследник без определения этого метода:

    
    >>> from abc import ABCMeta, abstractmethod
    >>> class A(object):
    >>> 	__metaclass__=ABCMeta
    >>> 	@abstractmethod
    >>> 	def foo(self): pass
    >>> 
    >>> A()  
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: Can't instantiate abstract class A with abstract methods foo
    

    Не вышло. Теперь определим нужный метод:

      
    >>>class C(A):
    >>>   def foo(self): print(42)
    >>>C
    <class '__main__.C'>
    >>>a=C()
    >>>a.foo()
    42
    

    Узнаем, как это реализуется в метаклассе (опустив некоторые другие возможности
    модуля abc) ABCMeta:

    >>>class ABCMeta(type):
    >>>    def __new__(mcls, name, bases, namespace):
    >>>        bases = _fix_bases(bases)
    >>>        cls = super(ABCMeta, mcls).__new__(mcls, name, bases, namespace)
    >>>        # Найдем множество(set) имен абстрактных методов среди собственных
    >>>        # методов и методов предков
    >>>        abstracts = set(name
    >>>                     for name, value in namespace.items()
    >>>                     if getattr(value, "__isabstractmethod__", False))
    >>>        for base in bases:
    >>>            for name in getattr(base, "__abstractmethods__", set()):
    >>>                value = getattr(cls, name, None)
    >>>                if getattr(value, "__isabstractmethod__", False):
    >>>                    abstracts.add(name)
    >>>        cls.__abstractmethods__ = frozenset(abstracts)
    >>>        return cls
    

    Метод _fix_bases добавляет скрытый класс _Abstract в число предков
    абстрактного класса. Сам _Abstract проверяет, осталось ли что-нибудь во
    множестве(set) __abstractmethods__; если осталось — выкидывает исключение.

    >>>class _Abstract(object):
    >>>    def __new__(cls, *args, **kwds):
    >>>        am = cls.__dict__.get("__abstractmethods__")
    >>>        if am:
    >>>            raise TypeError("can't instantiate abstract class %s "
    >>>                            "with abstract methods %s" %
    >>>                            (cls.__name__, ", ".join(sorted(am))))
    >>>        return super(_Abstract, cls).__new__(cls, *args, **kwds)
    >>>
    >>>def _fix_bases(bases):
    >>>    for base in bases:
    >>>        if issubclass(base, _Abstract):
    >>>            # _Abstract уже среди предков
    >>>            return bases
    >>>    if object in bases:
    >>>        # Заменяем object на _Abstract, если класс прямо наследуется от object
    >>>        # и не перечислен среди прочих предков
    >>>        return tuple([_Abstract if base is object else base
    >>>                      for base in bases])
    >>>    # Добавляем _Abstract в конец в противном случае
    >>>    return bases + (_Abstract,)
    

    В каждом абстрактном классе хранится по «замороженному» множеству(frozenset)
    абстрактных методов; то есть тех методов (функций-объектов), у которых есть
    атрибут __isabstractmethod__, выставляемый соответствующим декоратором:

    >>>def abstractmethod(funcobj):
    >>>    funcobj.__isabstractmethod__ = True
    >>>    return funcobj
    

    Итак, абстрактный метод получает атрибут __isabstractmethod__ при назначении ему
    декоратора. Атрибуты после наследования от абстрактного класса собираются во
    множестве "__abstractmethods__" класса-наследника. Если множество не пустое, и
    программист пытается создать объект класса, то будет вызвано исключение
    TypeError со списком неопределенных методов.

    Вывод

    Просто? Просто. Язык расширен? Расширен. Комментарии, как говорится, излишни.

    DSL в Django


    Один из продвинутых примеров DSL — механизм ORM Django на примере класса Model и
    метакласса ModelBase. Конкретно связь с базой данный здесь не интересны, имеет
    смысл сконцентрироваться на создании экземпляра класса-наследника класса Model.

    Большая часть следующего подраздела — подробный разбор кода
    ModelBase. Читателям, не нуждающимся в подробностях, достаточно прочитать вывод
    в конце раздела «Django».

    Разбор метакласса ModelBase

    Вся механика работы метакласса ModelBase сконцентрирована в месте
    переопределения метода __new__, вызываемого непосредственно перед созданием
    экземпляра класса модели:

      >>>class ModelBase(type):
      >>>    """
      >>>    Metaclass for all models.
      >>>    """
      >>>    def __new__(cls, name, bases, attrs):
      >>>        super_new = super(ModelBase, cls).__new__
      >>>        parents = [b for b in bases if isinstance(b, ModelBase)]
      >>>        if not parents:
      >>>            # If this isn't a subclass of Model, don't do anything special.
      >>>            return super_new(cls, name, bases, attrs)
    

    В самом начале метода просто создается экземпляр класса и, если этот класс не
    наследует от Model, просто возращается.

    Все конкретные опции класса модели собираются в атрибуте класса _meta, который
    может быть создан с нуля, унаследоваться от предка или быть подкорректирован в
    локальном классе Meta:

      >>>        # Создание клаcса
      >>>        module = attrs.pop('__module__')
      >>>        new_class = super_new(cls, name, bases, {'__module__': module})
      >>>        attr_meta = attrs.pop('Meta', None)
      >>>        abstract = getattr(attr_meta, 'abstract', False)
      >>>        if not attr_meta:
      >>>            meta = getattr(new_class, 'Meta', None)
      >>>        else:
      >>>            meta = attr_meta
      >>>        base_meta = getattr(new_class, '_meta', None)
    

    Кроме того, видим, что класс может быть абстрактным, не соответствующим
    какой-либо таблице в базе данных.

    Момент истины в процессе создания класса модели наступает при внесении в него
    параметров по умолчанию:

      >>>        new_class.add_to_class('_meta', Options(meta, **kwargs))
    

    add_to_class либо вызывает метод contribute_to_class аргумента, либо, если
    такового нет, просто добавляет именованный атрибут классу.

    Класс же Options в своем contribute_to_class делает атрибут _meta ссылкой на
    самого себя и собирает в нем различные параметры, вроде названия таблицы базы
    данных, списка полей модели, списка виртуальных полей модели, прав доступа и
    других. Он также проводит проверки связей с другими моделями на уникальность
    названий полей в БД.

    Далее в методе __new__ неабстрактному классу добавляются именованные
    исключения:

      >>>        if not abstract:
      >>>            new_class.add_to_class('DoesNotExist',
      >>>                subclass_exception('DoesNotExist', ObjectDoesNotExist, module))
      >>>            new_class.add_to_class('MultipleObjectsReturned',
      >>>                subclass_exception('MultipleObjectsReturned',
      >>>                    MultipleObjectsReturned, module))
    

    Если класс-родитель — не абстрактный, и параметры не установлены явно в локальном
    классе Meta, то наследуем параметры ordering и get_latest_by:

      >>>        if base_meta and not base_meta.abstract:
      >>>            if not hasattr(meta, 'ordering'):
      >>>                new_class._meta.ordering = base_meta.ordering
      >>>            if not hasattr(meta, 'get_latest_by'):
      >>>                new_class._meta.get_latest_by = base_meta.get_latest_by
    

    Менеджер по умолчанию должен быть нулевым. Если такая модель уже существует — завершаем обработку, возвращая эту модель:

      >>>        if getattr(new_class, '_default_manager', None):
      >>>            new_class._default_manager = None
      >>>        
      >>>        m = get_model(new_class._meta.app_label, name, False)
      >>>        if m is not None:
      >>>            return m
    


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

      >>>        for obj_name, obj in attrs.items():
      >>>            new_class.add_to_class(obj_name, obj)
    

    Теперь требуется пройтись по полям модели и найти связи типа «один к одному»,
    которые будут использовать чуть ниже:

      >>>        # Do the appropriate setup for any model parents.
      >>>        o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields
      >>>                if isinstance(f, OneToOneField)])
    

    Проход по предкам модели для наследования различных полей, с отбрасыванием тех,
    что не являются наследниками Model. Далее переведены комментарии, которых
    достаточно для понимания происходящего:

      >>>        for base in parents:
      >>>            if not hasattr(base, '_meta'):
      >>>                # Модели без _meta не являются действующими и интереса не представляют
      >>>                continue
      >>>
      >>>            # Все поля произвольного типа для данной модели
      >>>            new_fields = new_class._meta.local_fields + \
      >>>                         new_class._meta.local_many_to_many + \
      >>>                         new_class._meta.virtual_fields
      >>>            field_names = set([f.name for f in new_fields])
      >>>
      >>>            if not base._meta.abstract:
      >>>                # Обрабатываем "конкретные" классы...
      >>>                if base in o2o_map:
      >>>                    field = o2o_map[base]
      >>>                    field.primary_key = True
      >>>                    new_class._meta.setup_pk(field)
      >>>                else:
      >>>                    attr_name = '%s_ptr' % base._meta.module_name
      >>>                    field = OneToOneField(base, name=attr_name,
      >>>                            auto_created=True, parent_link=True)
      >>>                    new_class.add_to_class(attr_name, field)
      >>>                new_class._meta.parents[base] = field
      >>>
      >>>            else:
      >>>                # .. и абстрактные.
      >>>
      >>>                # Проверка на столкновения имен между классами,
      >>>                # объявленными в данном классе и в абстрактном предке
      >>>                parent_fields = base._meta.local_fields + base._meta.local_many_to_many
      >>>                for field in parent_fields:
      >>>                    if field.name in field_names:
      >>>                        raise FieldError('Local field %r in class %r clashes '\
      >>>                                         'with field of similar name from '\
      >>>                                         'abstract base class %r' % \
      >>>                                            (field.name, name, base.__name__))
      >>>                    new_class.add_to_class(field.name, copy.deepcopy(field))
      >>>
      >>>                # Все неабстрактные родители передаются наследнику
      >>>                new_class._meta.parents.update(base._meta.parents)
      >>>
      >>>            # Базовые Менеджеры наследуются от абстрактных классов
      >>>            base_managers = base._meta.abstract_managers
      >>>            base_managers.sort()
      >>>            for _, mgr_name, manager in base_managers:
      >>>                val = getattr(new_class, mgr_name, None)
      >>>                if not val or val is manager:
      >>>                    new_manager = manager._copy_to_model(new_class)
      >>>                    new_class.add_to_class(mgr_name, new_manager)
      >>>
      >>>            # Виртуальные поля (вроде GenericForeignKey) берем от родителя
      >>>            for field in base._meta.virtual_fields:
      >>>                if base._meta.abstract and field.name in field_names:
      >>>                    raise FieldError('Local field %r in class %r clashes '\
      >>>                                     'with field of similar name from '\
      >>>                                     'abstract base class %r' % \
      >>>                                        (field.name, name, base.__name__))
      >>>                new_class.add_to_class(field.name, copy.deepcopy(field))
      >>>
    

    Абстрактные классы моделей нигде не регистрируются:

      >>>        if abstract:
      >>>            # Абстрактные модели не могут инстанцироваться и не появляются
      >>>            #  в списке моделей для приложения, поэтому обратываются немного иначе, нежели
      >>>            #  нормальные модели
      >>>            attr_meta.abstract = False
      >>>            new_class.Meta = attr_meta
      >>>            return new_class
    

    Нормальные же регистрируются и возращаются уже из списка зарегистрированных
    классов моделей:

      >>>        new_class._prepare()
      >>>        register_models(new_class._meta.app_label, new_class)
      >>>        return get_model(new_class._meta.app_label, name, False)
    


    Вывод

    Итак, подведем итоги. Зачем понадобились метаклассы?

    1) Класс-модель должен иметь набор обязательных параметров (имя таблицы, имя
    джанго-приложения, список полей, связи с другими моделями и многие другие) в
    атрибуте _meta, которые и определяются при создании каждого класса, наследующего
    от Model.

    2) Эти параметры сложным образом наследуются от обычных и абстрактных
    классов-предков, что некрасиво закладывать в сам класс.

    3) Появляется возможность спрятать происходящее от программиста, использующего
    фреймворк.

    Замечаньица


    1) Если явно не указывать наследование класса от object, то класс использует
    метакласс, указанный в глобальной переменной __metaclass__, что иногда может
    быть удобно при многократном использовании собственного метакласса в пределах
    одного модуля. Простой пример, приведенный в начале заметки, можно переделать
    следующим образом:

      class AttributeInitType(type):
          def __call__(self, *args, **kwargs):
          obj = type.__call__(self, *args)
          for name in kwargs:
          setattr(obj, name, kwargs[name])
          return obj
    
      __metaclass__ = AttributeInitType
    
      class Man:
          pass
    
      me = Man(height = 180, weigth = 80)
      print me.height
    
      В стандартный поток выведется:
      180
    

    2) Есть такой супергуру питоновский, Тим Питерс. Он очень удачно сказал про
    применение метаклассов и аналогичных средств из разряда черной магии Питона:

        Metaclasses are deeper magic than 99% of users should ever worry
        about. If you wonder whether you need them, you don't (the
        people who actually need them know with certainty that they need
        them, and don't need an explanation about why).
    

    На русском это примерно так звучит:

        Метаклассы - лишнее для большинства пользователей. Если вообще вы задаетесь
        вопросом, нужны ли они, то они точно не нужны. Их используют только люди,
        которые точно знают, что делают и не нуждаются в объяснениях.
    

    Мораль тут простая: не мудрите. Метаклассы в большинстве случаев — лишнее. Питонист должен руководствоваться принципом наименьшего удивления;
    менять классическую схему работы ООП не стоит просто ради самолюбования.

    Ссылочки по мотивам



    Английская Википедия — отсюда позаимствован простой примерчик
    PEP-3119 — здесь
    описываются абстрактные классы в полном своем варианте.
    Ролик
    на английском
    , подробный разговор про метаклассы в Питоне с примерами
    использования. Там по ссылкам можно найти и саму статью с примерами, очень
    поучительно.
    Метки:
    Поделиться публикацией
    Комментарии 29
    • 0
      Отличная статья, спасибо.
    • +1
      Очень интересно, спасибо; а можно добавить свои 5 копеек? )

      >>… классический ООП подразумевает наличие только классов и объектов
      О каком классическом ООП тут идет речь? :) Дело в том, что классический ООП — это, скорее Smaltalk, а там наличествуют и классы классов, и MetaObject Protocol, аналоги которого есть в Python и Ruby. Тот ООП, в котором этого нет — скорее, mainstream-ООП: c++, java, c#.

      • +1
        Ну… Smalltalk он, конечно, стар, крут и прапапа всего сущего, но все же ООП вошел в мэйнстрим благодаря прежде всего C++/Java, не так ли?

        Поэтому на них и захотелось сослаться.
        • 0
          Не вопрос :) Хотел уточнить, чтобы убедиться, что я все правильно понял ) Спасибо за ответ.
        • 0
          В С++ целый океан говна помимо классов и объектов.
    • +1
      Я пайтон почти не знаю но разве что немного синтаксис но блин все больше удивляюсь его возможностям, прям взрыв мозгов %) Жду еще подобных статей
    • НЛО прилетело и опубликовало эту надпись здесь
      • +2
        про метаклассы есть большая куча статей в гугле только они все старые… и специфические очень.
        а тут наконец-то что-то собирающее всё в одной куче. респект автору.
    • +2
      >Питон расширяет классическую парадигму, и сами классы в нем тоже становятся
      равноправными объектами

      Т.е. с метаклассами классы становятся первоклассным объектом в языке?
      • +4
        Бинго.

        Более того, и сами метаклассы тоже являются первоклассным объектом Питона, их можно передавать куда угодно, добавлять атрибуты и методы и проводить любые действия, возможные для объектов.
        • +1
          >Более того, и сами метаклассы тоже являются первоклассным объектом Питона, их можно передавать куда угодно, добавлять атрибуты и методы и проводить любые действия, возможные для объектов.

          Собсно, это и подразумевается под «первоклассностью».
    • 0
      «Объект конструируется вызовом класса оператором „()“. Создадим наследованием от
      type метакласс, переопределяющий этот оператор:»

      А зачем такое извращение? вызов на классе () это банальный вызов конструктора у type. Поэтому если уж вы наследуетесь от type переопределять нужно имхо __init__, а никак не __call__.

      Другое дело что в качестве __metaclass__ совершенно необязательно присваивать наследника type, а можно использовать любую callable штуку. Так что если бы у вас был например class AttributeInitType(object): ваша конструкция имела бы смысл.

      Или я ошибаюсь?
      • 0
        Ответ вам — чуть ниже. По ошибке не туда залепил.
    • +1
      Ошибаетесь. Не проверял на Python 3.0, но на моем Python 2.6.2 получится вот так:

          class AttributeInitType(object):
              def __call__(self, *args, **kwargs):
                  """ Вызов класса создает новый объект. """
                  # Перво-наперво создадим сам объект...
                  obj = type.__call__(self, *args)
                  # ...и добавим ему переданные в вызове аргументы в качестве атрибутов.
                  for name in kwargs:
                      setattr(obj, name, kwargs[name])
                  # вернем готовый объект
                  return obj
          
          class Man(object):
              __metaclass__ = AttributeInitType
          
          me = Man(height = 180, weigth = 80)
          
          print me.height
      
      >>> Traceback (most recent call last):
        File "/tmp/py7637jbF", line 13, in <module>
          class Man(object):
      TypeError: Error when calling the metaclass bases
          object.__new__() takes no parameters
      


      Почему же переопределялся __call__? потому что __init__ вызывается при определении класса, а не появлении объекта. Только что все проверил лично.
    • 0
      Не знаю, насколько в тему. Вот хорошая статья о системе типов в Python (англ.): www.cafepy.com/article/python_types_and_objects/
      • 0
        очень в тему, знаком со статьей. Одно из лучших пояснений к системе типов Питонаю
    • 0
      Очень круто! Спасибо за статью. И раз уж Вы здесь и знакомы с Django, как достигается цепочка параметров при запросе — Model.objects.filter().order_by().values() и т.д.?
      • +1
        Имеем, например:

          class Apple(Model):
              pass
        


        Можем, например, сделать так:

          Apple.objects.all()  
        


        Так вот. objects — ссылка на экземпляр класса Manager. В этом классе собрана вся
        логика работы с моделью, стандартная либо расширенная для данной конкретной модели. По
        любому из его методов вроде all() или get() возращается объект QuerySet,
        инкапсулирующий в себе информацию об отложенных запросах.

        Методы all() и get_query_set() из класса Manager:

          def get_query_set(self):
              """Returns a new QuerySet object.  Subclasses can override this method
              to easily customize the behavior of the Manager.
              """
              return QuerySet(self.model)
        
          def all(self):
              return self.get_query_set()
        


        Теперь заглянем в определение класса QuerySet():

          def all(self):
              """
              Returns a new QuerySet that is a copy of the current one. This allows a
              QuerySet to proxy for a model manager in some cases.
              """
              return self._clone()
        
          def filter(self, *args, **kwargs):
              """
              Returns a new QuerySet instance with the args ANDed to the existing
              set.
              """
              return self._filter_or_exclude(False, *args, **kwargs)
        
          def _filter_or_exclude(self, negate, *args, **kwargs):
              if args or kwargs:
                  assert self.query.can_filter(), \
                          "Cannot filter a query once a slice has been taken."
        
              clone = self._clone()
              if negate:
                  clone.query.add_q(~Q(*args, **kwargs))
              else:
                  clone.query.add_q(Q(*args, **kwargs))
              return clone
        


        Что делают эти методы? Они создают копию последнего QuerySet, дописывают в атрибут
        self.query новую, условного говоря, строчку, и возращают после этого копию.

        Легко получается Apple.object.all().filter(color=«green»).get(). На каждом шаге,
        в каждой точке, происходит копирование «наброска» запроса с расширением и
        возвратом копии.

        Так, кстати, и получаются эти самые «ленивые», «отложенные» вычисления. Запросы
        к базе, скажем так, записываются в строчку, а выполняются уже одним пакетом либо
        в уже шаблоне, либо по желанию программиста — преобразованием к обычному списку
        объектов.
        • 0
          Туман рассеялся — все стало на свои места. Спасибо!
    • 0
      >Но если класс — объект, то какому классу он соответствует?
      >По умолчанию этот класс (метакласс) называется type.
      Задам дурацкий вопрос, а какой метакласс у type?
      • +2
        <a href«www.cafepy.com/article/python_types_and_objects/»> ссылочка по теме, где очень внятно нарисовано и объяснено. Тут выше ее, кажется, уже приводили.

        Оттуда или прямо из интерпретатора видим, что,
        >>>type(type)
        <type 'type'>
        


        И еще экспериментрируем:
        >>> isinstance(object, type)
        True
        >>> isinstance(type, object)
        True
        >>> 
        


        Ну так вот. type является «конкретизацией», или экземпляром, класса type(как бы рекурсивно) и наследует от object.

        В то же время класс object является экземпляром класса type, но не наследует ни от чего.

        Главное здесь:
        1) Все, и классы, и метаклассы — наследуют от object. Значит, они являются первоклассными объектами.

        2) Поведение класса в моменты объявления, вызова и наследования редактируется в метаклассе.

        3) Поведение объектов редактируется в классе.
        • 0
          Бляха… Не пойму, что тут с тегом творится. Или ошибся сам где-то?
          • 0
            Итак, классический ООП подразумевает наличие только классов и объектов.

            В ООП достаточно объектов и типов. Оффтопичный Javascript обходится лишь этим. Я не спорю. В этой области с терминологией творится полный кошмар.

            Как показано на картинке по ссылке выше, пунктирчато-стрелочное отношение instanceOf транзитивно. Но, тем не менее, у каждого объекта есть один единственный, самый близкий ему тип. Тот, который его породил.

            В Javascript отношение между объектом и «самым близким типом» реализуется через неявную ссылку obj.[[Prototype]], который можно выдернуть, сказав obj.constructor. В python — через obj.__class__ (который, не смотря на название, содержит-таки ссылку на этот «самый близкий тип»). Это я к тому, что термины: constructor и __class__ это синонимы. Поэтому, когда речь заходит об объекте, создающим объекты, то его стоит называть конструктором. Правда это не то же самое, что __init__(self), которая всего лишь процедура инициализации, связанная с объектом (она ни чего не создает, ей объект прилетает уже созданным, в самом первом аргументе self :) ).

            В принципе, ссылки от объекта к своему конструктору (который тоже объект и у которого есть свой конструктор, котор…) уже достаточно для того, чтобы порождать магию instanceOf (то бишь typeof), и говорить о наследовании. Т.е. если представить, что у питоньего объекта есть лишь __class__ и __dict__, то станет понятно как живется несчастному жаваскрипту, без классов. :)

            Но, в самом деле, как же «класс-ическое» ООП без классов? :) Class это обычный конструктор, характерной чертой которого является поддержка пусто-стрельчатого отношения IsA (т.е. наследования как показано на картинке) между объектами. Для этого в питоньих классах и появляется атрибут __bases__, где хранится кортеж супер-классов (список непосредственных родителей для данного класса, в смысле), суровое шаманство для иерархического поиска атрибутов в этой куче (в дополнение к поиску по иерархии типов), логика instanceOf начинает использовать isA, и определенные правила для манипуляции с object.__dict__. Мрак.

            Для того, чтобы создавать классы «на лету» не обязательно придумывать новые конструкторы (мета-классы). Python предоставляет несколько встроенных и шикарную библиотеку (import types) на все оставшиеся 90% случаев жизни. Например:
            >>> ClassOfPythons = type('ThePython', (object,), {'voice': 'sh-sh-sshhhhhh....'})
            >>> squasher = ClassOfPythons()
            >>> squasher.voice
            sh-sh-sshhhhhh....

            ну или так:
            ClassOfPythons = object.__class__('ThePython', (object,), {'voice': 'sh-sh-sshhhhhh....'})

            что абсолютно тоже самое, поскольку id(object.__class__) == id(type)

            Мета-класс это узко-специальная магия для того, чтобы класс мог выбрать себе конструктора. Фактически это ситуация равносильна тому, когда хвост станет махать собакой — бессмыслица, но иногда очень нужно. :) В метакласс в django-модели используется сугубо как синтаксический сахар. Пол-статьи ни о чем. А вот пример с абстрактным классом, просто красавчик. За это даже плюсик в карму, если получится. :)
            • 0
              Очень люблю Javascript за его подход к ООП. Очень. Но не понимаю, вообще не понимаю, при чем здесь язык :)

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

              Но все же людям привычно классифицировать понятия, которыми оперирует система; оттуда и страсть, ИМХО, к этому стилю (есть класс и его объект) во множестве популярнейших языков. С такой аргментации начинается среднее введение в ООП.

              зачем вы привели сейчас либу types? Цитирую справку: «This module defines names for some object types that are used by the standard Python interpreter...» То есть можно будет проверять, правильные ли типы подсунуты на вход функции. Пример с того же дока:

              from types import *
              def delete(mylist, item):
                  if type(item) is IntType:
                     del mylist[item]
                  else:
                     mylist.remove(item)
              


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

              Очень хорошо представляю, как вы начнете всю эту логику наследования моделей, передачи атрибута _meta, учет абстрактных базовых классов решать в функциональном стиле (ModelClass = type (...))! Будет и больше, и мутнее, и некрасиво.

              Дело, конечно, хозяйское, Питон дает несколько путей решения этих проблем. Насколько я понимаю слово pythonic — лучшее решение то, которое легче понять и прочитать.
              • 0
                Приятно, конечно, что прототипы позволяют избавиться от лишнего слоя абстракций в виде классов и, тем более, метаклассов. Более того, эти сущности при желании можно моделировать средствами JS.

                Извиняюсь за сумбурность вышесказанного. Ведь хотелось подвести к прямо противоположному выводу. :) Объекты и типы — вот базовая штука в ООП. А классы — это такой жутко полезный тип, — которого в javascript, как раз, серьезно не хватает. Ну и они там не реализуется по-человечески, поскольку язык не позволяет абстрагировать основную оопшную точкозапись «obj.property».
                • 0
                  Поясните фразу «не позволяет абстрагировать». Через точку можно обращаться к свойствам объекта-словаря? Можно. В чем проблема?

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

                  Я пока не видел никаких особых руководств на эту тему… Надо поискать да подумать.
                  • 0
                    Вы пишете obj.property, явно желая получить о объекта «obj», нечто, связанное с именем «property». Но где объект возьмет это «нечто»? В конструкторе? В __dict__? В каком-то из супер-классов? Для этого, в new-классах python используются умопомрачительные правила: www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html#id402018 (следующая статья по вашей ссылке :) ). Другие типы (не-new-классы) имеют другие правила. А благодаря «магическим» методам (__getattr__, __setattr__ и т.п.) и вы можете придумать что-то свое. Но, что бы вы там не напридумывали, «снаружи» это все равно будет выглядить как «obj.property». Вот это и есть абстракция доступа.

                    В Javascript можно реализовать подобные правила, и описать их внутри методов. Но тогда и «снаружи» они будут выглядить как вызовы методов. Мне попадалось, например, такое: value = obj.findName('property').get(). Внутри findName() реализован механизм ресолвинга свойства по иерархии классов, а при помощи get() абстрагирован доступ к value. Т.е. логически все сделано. Но без абстракции доступа (синтаксического сахара, со стороны языка, если хотите).
                    • 0
                      Ну понятно…

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

                      В любом случае, некоторые особенности языка не могут не вызвать симпатии. Другие же — недоумение от ограниченности. :)
                  • 0
                    Логика диспетчеризации запроса для получения свойства с использованием прототипа, примитивна до безобразия:
                    — Эй, Объект, гони сюда свойство property!
                    — Хм… а нету… (если есть, был обязан выдать)
                    — Тогда пусть вернет тот, кто тебя породил, такого урода! (объект бежит просить в словаре у своего конструктора)

                    Если и у конструктора такого свойства нет, то обычно, на этом все и заканчивается, ибо конструктор конструктора уже не при делах: «вассал моего вассала не мой вассал». Это все, что доступно в JS.

                    Диспетчеризация запроса по иерархии классов отражает совсем другое отношение: конкретное-общее (класс -> супер-класс) — и логика диспетчеризации запросов тут совершенно иная. Единственное что их роднит с «объект-конструктор», это транзитивное отношение «instanceOf», которое может означать и «объект типа», и «объект класса» (см. диаграмму www.cafepy.com/article/python_types_and_objects/images/relationships_transitivity.png). Но из-за этого «двойного» смысла и возникает путаница, будто одно можно выразить через другое. Это не так (не даром, на диаграмме стрелки выглядят по разному).

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