Я открываю новый цикл статей, “Эффективное программирование”.
Давно уже стало банальностью утверждение о том, что программисты могут отличаться в производительности в десять и более раз. Среди важнейших причин, наряду с талантом и правильными условиями работы (сюда входит все от процессов разработки и тестирования до зарплаты, атмосферы в коллективе и удобства кресел), фигурирует и использование правильных практик программирования. Кент Бек (Kent Beck), создатель методологии Extreme Programming, говорил о себе “Я не считаю себя замечательным программистом, я лишь неплохой программист с замечательными привычками”. В этой и последующих статьях цикла будут рассмотрены некоторые практики, входящие в мой собственный набор “замечательных привычек”, а также реально полезные инструменты и библиотеки.
В качестве основы взяты практические эпизоды наших текущих проектов. Материал нового цикла будет в первую очередь полезен для разработчиков и, в особенности, для тех-лидов и руководителей, осуществляющих “тренерскую” работу.
Предположим, что вы хотите повысить свою эффективность как программиста. Или, что более интересно, повысить эффективность разработчиков в своей команде. Какое занятие отнимает больше всего времени после непроизводительной активности в составе: кофе, перекуры, bash.org? Наверняка это отладка. Отладка обычно занимает вдвое-втрое больше времени, чем написание первой (не работающей) версии кода.
Сегодня я напишу о способе, который позволит сделать отладку более приятной и эффективной.
Все, за исключением использования графических дебаггеров, способы отладки базируются на текстовом выводе значения переменных. Логи, Exception’ы, консоль — все это требует представления данных программы в текстовом формате. Каждый раз, когда это представление выбрано неудачно, вам придется менять его и проходить еще одну итерацию отладки. Некоторые ошибки будет сложно или совсем невозможно воспроизвести. Следовательно, очень желательно выбрать правильное представление с первого раза.
Сразу предупрежу, что стандартная реализация далеко не идеальна.
Что произойдет при попытке напечатать значение объекта в python?
>>> print 7
7
>>> print range(5)
[0, 1, 2, 3, 4]
Пока все хорошо. Попробуем отойти от базовых типов данных:
class Run(object):
pass
>>> r = Run()
>>> r.status = 'new'
>>> print r
<__main__.Run object at 0xd3f944c>
>>> print 'Shit happens...'
'Shit happens...'
Отвратительно. Придется обращаться отдельно к каждому атрибуту, что не очень удобно.
Надо сказать, что в Ruby вывод подробного состояния объекта реализован очень правильно:
>> Run.find :first
=> #<Run:0xb725f884 @attributes={"status"=>"done",
"finished_at"=>"1900-01-01 00:00:00", "id"=>"1",
"started_at"=>"1900-01-01 00:00:00"}>
Python здесь явно проигрывает. Впрочем, как я покажу дальше, не все так плохо.
Дело в том, что поведение объекта при вызове print можно настраивать. Для этого служат два специальных метода: __str__ и __repr__ — первый для описания объекта в свободной форме, второй — для максимально строго и полного описания соответственно (желательно, чтобы результат __repr__ являлся валидным выражением на языке python, описывающем текущий объект). Для отладки актуален, преимущественно, метод __repr__.
Классическое исполнение выглядит следующим образом:
class Run(object):
def __init__(self, status, started_at):
self.status, self.started_at = status, started_at
def get_name(self):
return self.user_name
def __repr__(self):
return "Run: status=%s started_at=%s" % (
self.status, self.started_at)
>>> from datetime import datetime
>>> r = Run('new', datetime.today())
>>> r
Run: status=new started_at=2007-08-12 03:16:26.468553
В этом коде нарушается принцип DRY (don’t repeat yourself): при добавлении в класс нового атрибута вам придется менять код текстового представления объекта.
В один прекрасный день обнаруживаешь, что у тебя накопилось с десяток классов с 5-15 значимыми атрибутами каждый, а редактирование методов __repr__ достало до печенок. Как быстро выяснилось, есть способ реализовать __repr__ раз и навсегда. Для этого достаточно немного покопаться во внутренней механике объектов, а именно — использовать свойство __dict__, дающее доступ ко всем атрибутам данного объекта в виде dictionary формата “имя: значение”. Такая реализация даже более компактна, чем явная печать каждого атрибута:
class Run(object):
# ...
def __repr__(self):
return 'Run: %s' % dict([
(k,v) for k,v in self.__dict__.items() if not k.startswith('_')])
>>> session = create_session()
>>> Run.get_latest(session)
Run: {'status': 'done', 'finished_at': datetime.datetime(2007, 8, 8, 20, 26, 48),
'resume_after': None, 'started_at': datetime.datetime(2007, 8, 8, 20, 26, 42),
'id': 4L, 'errors_in_a_row': 0}
Примечание: здесь и далее в примерах работа с БД организована с помощью библиотеки SQLAlchemy. Это, пожалуй, лучшее что есть сейчас в python для работы с базами данных.
Обратите внимание на выражение dict([(k,v) for k,v in self.__dict__ if not k.startswith('_')]). В pyhton принято начинать имена private-атрибутов с одного подчеркивания (”_”), следовательно, мы отфильтровываем все private-атрибуты и не показываем их. По опыту могу сказать, что это очень удобно при работе с SQLAlchemy, которая добавляет к вашим объектам кучу служебных свойств.
Уже гораздо лучше: написанный один раз код можно просто копировать в каждый новый класс. Для тех, кто работает под девизом “зачем объектное программирование, когда есть блочное копирование” этого вполне достаточно. Мы же пойдем чуть дальше и создадим действительно универсальную реализацию:
class Inspectable(object):
"""Provide an ability to print class instance and its attributes"""
def __repr__(self):
return '<%s: %s>' % (
self.__class__.__name__,
dict([(x,y) for (x,y) in self.__dict__.items() if not x.startswith('_')])
)
class Run(Inspectable, object):
#...
>>> session = create_session()
>>> Run.get_latest(session)
<Run: {'status': 'done', 'finished_at': datetime.datetime(2007, 8, 8, 20, 26, 48),
'resume_after': None, 'started_at': datetime.datetime(2007, 8, 8, 20, 26, 42),
'id': 4L, 'errors_in_a_row': 0}>
Обратите внимание на порядок наследование в Run: Inspectable, object. В python наследование от списка классов резолвится слева направо, поэтому первыми должны быть указаны классы, располагающиеся ниже в иерархии наследования. В противном случае получите ошибку наследования:
>>> class Run(object, Inspectable):
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Error when calling the metaclass bases
Cannot create a consistent method resolution
order (MRO) for bases object, Inspectable
Кстати, явно наследовать от object совсем необязательно — ведь Inspectable тоже является потомком object. Я, однако, предпочитаю указывать object если перед этим наследую только от абстрактных классов — по идеологии это близко концепции mixin’ов в Ruby.