Всем привет.
Это продолжение ответов на вопросы и задания по 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