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.

Где-то с месяц назад наткнулся на пост Why learning Haskell/Python makes you a worse programmer. Люк Плант (Luke Plant) поднимает крайне актуальную для многих из нас проблему — изучение продвинутых языков не делает вас лучшим программистом на том, что приходится использовать, а, напротив, деморализует и снижает качество работы.

Точка зрения сотрудника

Очень рекомендую прочитать как статью, так и комментарии. Если кратко, приводятся два основных аргумента:

  1. После того, как ты понял и полюбил Python, писать на C# неприятно. Код громоздок и некрасив. Процедурный подход вызывает кучу потенциальных глюков. Кроме того, начав думать на Python, приходится работать “живым компилятором”, переводя эти мысли в C#.

  2. Начав использовать функциональное программирование, пытаешься использовать его везде где только можно. C# не поддерживает многие концепции функционального программирования, в результате чего попытки применять функциональный стиль выглядят уродливо и непонятно. Многие задачи проще решать процедурно.

Люк делает вывод, что нет смысла заниматься самосовершенствованием, если потом не сможешь использовать обретенные навыки.

С изменением среды, на самом деле, все плохо. Шансы рядового программиста или проджект-менеджера изменить политику и стандарты компании, как правило, асимптотически приближаются к нулю. Исключения, когда проектная команда обладает высокой степенью автономности и может использовать какие угодно инструменты, редки как девственность.

В свое время это было одним из факторов, побудивших меня покинуть моего предыдущего работодателя и начать работать на себя.

Добавлю еще, что все относительно. Можно заменить языки в обсуждаемом примере на другие, но проблема останется той же. Есть языки лучше, чем Python и Haskell, есть и множество языков, куда хуже C#. В реальной жизни все еще сложнее, потому что важно не столько качество языка, сколько пригодность его и связанной с ним платформы для решения конкретной задачи.

Точка зрения компании

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

Ряд задач требуют использования неоптимальных с точки зрения эстетики и уровня абстракции языков. Драйвера и embedded-приложения пишутся на C. Windows-приложения — на том, что поставляет в данный момент Microsoft (сейчас это C#). SAP кастомизируется с помощью ABAP, недалеко ушедшего в своем развитии от COBOL. Можете продолжать до бесконечности.

Кроме того, есть ниши, где выбор языка может повлиять на маркетинговую привлекательность продукта. Java продается. C# продается. Hashkell… — а что это такое? Думаю, логика выбора языка в таком случае будет очевидна.

Что думаете по этому поводу, коллеги?