Sun 12 Aug 2007
Эффективное программирование: текстовое представление переменных в python
Posted by Alex Lebedev under python, программирование, эффективное программирование
[13] Comments
Я открываю новый цикл статей, “Эффективное программирование”.
Давно уже стало банальностью утверждение о том, что программисты могут отличаться в производительности в десять и более раз. Среди важнейших причин, наряду с талантом и правильными условиями работы (сюда входит все от процессов разработки и тестирования до зарплаты, атмосферы в коллективе и удобства кресел), фигурирует и использование правильных практик программирования. Кент Бек (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.

August 12th, 2007 at 08:04
А pprint() вам не подходит?
August 12th, 2007 at 09:52
pprint не печатает внутреннее содержимое объектов:
August 13th, 2007 at 04:48
Хорошая статья, мне понравилось.
Замечание 1: проверяйте код. В коде до примечания про SA цикл будет не по (k,v), а только по k (забыт .items())
Замечание 2: если использовать словарь для показа пар атрибут-значение и брать эти пары из dict, то порядок полей будет не фиксированным. Это не очень здорово, на мой взгляд. В алфавитном порядке мне нравится больше, а словарь или кортеж – не принципиально:
August 13th, 2007 at 12:07
А для юникода вроде бы надо использовать unicode, я прав?
August 13th, 2007 at 01:49
Спасибо, исправил.
August 13th, 2007 at 01:59
Если хотите, чтобы результат был отсортирован по ключам, то логичнее использовать
pprint.pfromat()из стандартной библиотеки:К тому же,
pformat()позволяет легко использовать разные фокусы с форматированием, если вам это интересно:August 13th, 2007 at 02:02
Еще один момент:
dir(object)иobject.__dict__— это не одно и тоже!dir()вернет вам список доступных атрибутов и методов объекта, а__dict__— только атрибуты. В данном случае вам совершенно незачем выводить список из десятка-другого методов, правда?August 18th, 2007 at 08:21
Да, конечно. Честнее будет, конечно, проверять атрибут на исполняемость (
iscallable), чем полагаться наdict.Пример — ниже:
Так что наверное оптимальным вариантом будет следующий показывающий все атрибуты – как класса, так и экземпляра):
Теперь, с Вашей подачи, использую его. Спасибо.
August 18th, 2007 at 08:23
Забыл отэкранировать угловые скобки, WP “съел” часть кода.
September 11th, 2007 at 05:23
Спасибо за статью! Жду продолжения.
December 6th, 2007 at 02:31
Я предпочитаю юнит-тесты. Не люблю загромождать код “лишними” классами, которые:
а) не нужны большую часть времени
б) скорее всего непонятны постороннему читателю кода, особенно учитывая пункт а)
Юнит-тесты грандиозно сокращают время отладки. Но их нужно не лениться писать.
December 19th, 2007 at 10:39
Добавил pformat к варианту Юрия:
from pprint import pformatclass Inspectable(object):
"""
Provide an ability to
represent class/instance
attributes itself
"""
def repr(self):
attrs = self.class.dict.copy()
attrs.update(self.dict)
public_attrs = dict((a,v) for a,v in attrs.items() if not (a.startswith('_') or callable(v)))
return '<%s: %s>' % (self.class.name, pformat(public_attrs))
.. хотя мне ближе подход Евгения Морозова.
June 29th, 2008 at 10:11
да, забавненько получается