Ответы на вопросы с PyObject. Часть 2

в 13:43, , рубрики: python, метки:

Всем привет.
Это продолжение ответов на вопросы и задания по Python с сайта pyobject.ru.

Disclaimer:

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

Классы

1. Написать базовый класс Observable, который бы позволял наследникам:
a. при передаче **kwargs заносить соответствующие значения как атрибуты
b. сделать так, чтобы при print отображались все публичные атрибуты

При обращении к атрибуту, происходит следующее (тут мог и соврать):
1. проверяется сам объект на наличие атрибута в нем. Пользовательские атрибуты хранятся в атрибуте __dict__ объекта.
2. проверяется атрибут __dict__ типа объекта через __class__.__dict__ объекта
3. проверяются родители типа
4. выполняется метод __getattribute__ для новых классов
5. выполняется метод __getattr__

Для того, чтобы значения были доступны как атрибуты, достаточно обновить атрибут __dict__ объекта, что и происходит в методе __init__.
Метод __str__ используется для отображения «инофрмативного представления» объекта. В данном случае мы выводим все публичные атрибуты объекта и их значения.

    class Observable(object):
        """
        Base class for attributes from dict.

        >>> class X(Observable):
        ...     pass
        >>> x = X(foo=1, bar="Test", _barr='hidden', baz=(5, 6))
        >>> print x
        X(bar=Test, foo=1, baz=(5, 6))
        >>> print x.foo
        1
        >>> print x.bar
        Test
        >>> print x._barr
        hidden
        >>> print x.baz
        (5, 6)
        """
        def __init__(self, **kwargs):
            self.__dict__.update(kwargs)

        def __str__(self):
            return '%s(%s)' % (self.__class__.__name__,
                (', '.join('%s=%s' % (key, val) for (key, val)
                    in self.__dict__.iteritems() if not key.startswith('_'))))

2. Написать класс, который бы по всем внешним признакам был бы словарем, но позволял обращаться к ключам как к атрибутам.
Для того, чтобы класс по всем внешним признакам был словарем, мы его от этого словаря и унаследуем. А чтобы к ключам можно было обращаться как к атрибутам, в классе определим метод __getattr__, который вызывается в тех случаях, когда аттрибут не найден в словаре объекта, его типа и родителях.

    class DictAttr(dict):
        """
        Base class for JS-style dict.

        >>> x = DictAttr([('one', 1), ('two', 2), ('three', 3)])
        >>> print x
        {'three': 3, 'two': 2, 'one': 1}
        >>> print x['three']
        3
        >>> print x.get('two')
        2
        >>> print x.one
        1
        >>> print x.test
        Traceback (most recent call last):
        ...
        AttributeError
        """
        def __getattr__(self, name):
            try:
                return self[name]
            except KeyError:
                raise AttributeError

3. Пункт 2 с усложнением: написать родительский класс XDictAttr так, чтобы у наследника динамически определялся ключ по наличию метода get_KEY.
Над этим заданием я просидел достаточно много времени, потому что у меня никак не получалось сделать его модно, стильно, молодежно и не костыльно. Что получилось — решать вам.

В кратце о том, как оно работает:
1. метод __getitem__ позволяет перехватывать обращения через []
Когда мы попадаем в метод то, в начале, пробуем получить значение с помощью простого обращения к словарю через вызов родительского __getitem__. Если же элемент в словаре не был найден, то пробуем получить значение с помощью метода __getattr__ в качестве аргумента которого используем get_KEY
2. метод get() переопределяет стандартное поведение get() типа dict. Мы вначале пробуем получить атрибут с помощью __getattr__ и аргумента get_KEY. И только в случае неудачи вызываем родительский метод get, который обратится к непереопределенному __getitem__ и проверит наличие аргумента в словаре.
3. метод __getattr__ позволяет обрабатывать все остальные ситуации. Для начала мы пробуем получить значение через вызов родительского __getitem__. И вот тут, в случае неудачи, в дело вступает грязный хак. Я так и не смог придумать, как исключить возможность рекурсии, потому что необходимо проверить наличие атрибута get_KEY, которое происходит через вызов __getattr__ объекта. Ну вы поняли. В итоге я имел строку вида get_get_get_get_get_foo.

Недостаток данной реализации — атрибуты, начинающиеся с get_, вызовут AttributeError. Это отображено в doctest.

    class XDictAttr(dict):
        """
        >>> class X(XDictAttr):
        ...     def get_foo(self):
        ...         return 5
        ...     def get_bar(self):
        ...         return 12
        ...     def get_get_z(self):
        ...         return 42



        >>> x = X({'one': 1, 'two': 2, 'three': 3})
        >>> x
        {'one': 1, 'three': 3, 'two': 2}
        >>> x['one']
        1
        >>> x.three
        3
        >>> x.bar
        12
        >>> x['foo']
        5
        >>> x.get('foo', 'missing')
        5
        >>> x.get('bzz', 'missing')
        'missing'
        >>> x.get_bar()
        12
        >>> x.get_foz()
        Traceback (most recent call last):
        ...
        AttributeError
        >>> x.get_get_z()
        42
        >>> x.get('get_z')
        42
        >>> x.get_z
        Traceback (most recent call last):
        ...
        AttributeError
        """
        def __getattr__(self, name):
            try:
                return super(XDictAttr, self).__getitem__(name)
            except KeyError:
                if not name.startswith('get_'):
                    return getattr(self, 'get_%s' % name)()
                else:
                    raise AttributeError

        def __getitem__(self, key):
            try:
                return super(XDictAttr, self).__getitem__(key)
            except KeyError:
                return getattr(self, 'get_%s' % key)()

        def get(self, key, default=None):
            try:
                return getattr(self, 'get_%s' % key)()
            except AttributeError:
                return super(XDictAttr, self).get(key, default)

4. Написать класс, который регистрирует свои экземпляры и предоставляет интерфейс итератора по ним
Я понимаю, что я не знаю многого в Python, но как по мне, то данное задание стоило отнести к метаклассам, по следующей причине:

    >>> for i in Reg:
    ...     print i
    <Reg instance at 0x98b6ecc>
    <Reg instance at 0x98b6fec>
    <Reg instance at 0x98ba02c>

Данный пример подразумевает, что итератор мы берем у типа, а не объекта класса. Да и интерпретатор питона ругался похожей ошибкой, когда я пробовал обернуть __iter__ в classmethod или staticmethod.
Поэтому моя реализация следующая:

    class RegBase(type):
        def __iter__(cls):
            return iter(cls._instances)


    class Reg(object):
        """
        >>> x = Reg()
        >>> x  # doctest: +ELLIPSIS
        <__main__.Reg object at 0x...>
        >>> y = Reg()
        >>> y # doctest: +ELLIPSIS
        <__main__.Reg object at 0x...>
        >>> z = Reg()
        >>> z # doctest: +ELLIPSIS
        <__main__.Reg object at 0x...>
        >>> for i in Reg: # doctest: +ELLIPSIS
        ...     print i
        <__main__.Reg object at 0x...>
        <__main__.Reg object at 0x...>
        <__main__.Reg object at 0x...>
        """
        __metaclass__ = RegBase
        _instances = []

        def __init__(self):
            self._instances.append(self)

P.S. да, и еще в примере Reg instance, у меня же Reg object. Может тут мой косяк?

Метаклассы и дескрипторы

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

Вопросы:
Я не буду цитировать документацию (Data model). В дополнение напишу: на хабре есть хорошие статьи об объектной структуре в общем и метаклассах в частности. Тут и тут. Спасибо их авторам.

Задания:
1. Реализовать дескрипторы, которые бы фиксировали тип атрибута.
Двумя словами — дескриптор это аттрибут класса нового вида с определенным поведением.
Статья о дескрипторах тут.

    class Property(object):
        """
        >>> class Image(object):
        ...     height = Property(0)
        ...     width = Property(0)
        ...     path = Property('/tmp/')
        ...     size = Property(0)



        >>> img = Image()
        >>> img.height = 340
        >>> img.height
        340
        >>> img.path = '/tmp/x00.jpeg'
        >>> img.path
        '/tmp/x00.jpeg'
        >>> img.path = 320
        Traceback (most recent call last):
          ...
        TypeError
        """
        def __init__(self, value):
            self.__value = value
            self.__value_type = type(value)

        def __get__(self, obj, objtype=None):
            return self.__value

        def __set__(self, obj, value):
            if type(value) == self.__value_type:
                self.__value = value
            else:
                raise TypeError

2. Реализовать базовый класс (используя метакласс), который бы фиксировал тип атрибута.
Вот тут у меня не совсем понял задание. По примеру автора атрибуты height, path являются классовыми. Подразумевалось ли, что метакласс должен фиксировать и переносить их в объект или только фиксировать — не понятно. Я реализовал второе.

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

    class ImageMeta(type):
        def __new__(mcs, name, bases, dct):
            for key, val in dct.iteritems():
                if not key.startswith('_') and not hasattr(val, '__call__'):
                    dct[key] = Property(val)
            return type.__new__(mcs, name, bases, dct)


    class ImageBase(object):
        """
        >>> class Image(ImageBase):
        ...    height = 0
        ...    path = 'tmp'
        ...
        ...    def foo(self):
        ...        return 'bar'
        >>> img = Image()
        >>> img.height = 340
        >>> img.height
        340
        >>> img.path = '/tmp/x00.jpeg'
        >>> img.path
        '/tmp/x00.jpeg'
        >>> img.path = 320
        Traceback (most recent call last):
          ...
        TypeError
        >>> hasattr(img.foo '__call__')
        True
        """
        __metaclass__ = ImageMeta

3. Реализовать базовый класс (используя метакласс) и дескрипторы, которые бы на основе класса создавали SQL-схему (ANSI SQL) для модели.
Просьба прокомментировать данный способ, потому что мне кажется, что можно было сделать красивей.
Для реализации были сделаны:
1. базовый дескриптор Property с классовым счетчиком, который позволяет отсортировать атрибуты в той последовательности, в который они были созданы.
2. дескрипторы Integer и Str с собственными __str__, которые используются при создании SQL представления модели.
3. метакласс TableMeta, который получает список полей модели и создает SQL представление модели.
4. базовый класс Table, который предоставляет классовый метод, который возвращает SQL представление модели.

    class Property(object):
        """
        >>> class Image(object):
        ...    size = Property(int)
        ...    name = Property(basestring)
        >>> img = Image()
        >>> img.size = 0
        >>> img.size
        0
        >>> img.name = '/tmp/img'
        >>> img.name
        '/tmp/img'
        >>> img.size = '~'
        Traceback (most recent call last):
          ...
        TypeError
        >>> img.name = ['/tmp/', 'img']
        Traceback (most recent call last):
          ...
        TypeError
        >>> img.__class__.__dict__['size'].counter
        0
        >>> img.__class__.__dict__['name'].counter
        1
        """
        counter = 0

        def __init__(self, value_type):
            self.__value = None
            self.__value_type = value_type
            self.counter = Property.counter
            Property.counter += 1

        def __get__(self, obj, objtype=None):
            return self.__value

        def __set__(self, obj, value):
            if isinstance(value, self.__value_type):
                self.__value = value
            else:
                raise TypeError


    class Integer(Property):
        def __init__(self):
            super(Integer, self).__init__(int)

        def __str__(self):
            return self.__class__.__name__.upper()


    class Str(Property):
        def __init__(self, size):
            super(Str, self).__init__(basestring)
            self.__size = size

        def __str__(self):
            return '{0}({1})'.format('varchar', self.__size).upper()


    class TableMeta(type):
        def __new__(mcs, name, bases, dct):
            fields = [(attr_name, val) for (attr_name, val) in dct.items()
                                  if isinstance(val, Property)]
            fields.sort(key=lambda x: x[1].counter)
            sql = ',n'.join('t{0} {1}'.format(attr_name, val)
                for (attr_name, val) in fields)
            dct['__sql'] = u'CREATE TABLE {0} (n{1}n)'.format(name, sql)
            return type.__new__(mcs, name, bases, dct)


    class Table(object):
        """
        >>> class Image(Table):
        ...     height = Integer()
        ...     width = Integer()
        ...     path = Str(128)

        >>> print Image.sql() # doctest: +NORMALIZE_WHITESPACE
        CREATE TABLE Image (
            height INTEGER,
            width INTEGER,
            path VARCHAR(128)
        )
        """
        __metaclass__ = TableMeta

        @classmethod
        def sql(cls):
            return cls.__dict__['__sql']

На этом все. За комментарии и критику — спасибо.

Автор: gagoman

* - обязательные к заполнению поля