python


Я открываю новый цикл статей, “Эффективное программирование”.

Давно уже стало банальностью утверждение о том, что программисты могут отличаться в производительности в десять и более раз. Среди важнейших причин, наряду с талантом и правильными условиями работы (сюда входит все от процессов разработки и тестирования до зарплаты, атмосферы в коллективе и удобства кресел), фигурирует и использование правильных практик программирования. Кент Бек (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.

« Previous Page