Пишем красивый, идиоматический Python

Пишем красивый, идиоматический PythonИногда сложно найти в Сети правильные, а главное актуальные «best practices» для языка. Документация, конечно же, содержит всю необходимую информацию, но отсеять нужную вещь в абзацах подробного (на то она и документация) описания, довольно сложно. Но недавно мне улыбнулся поиск Google, и я наткнулся на очень полезные «паттерны» языка Python от одного из core разработчиков — Raymond Hettinger.

Примечание: Все рекомендации даны в нескольких вариантах: сначала идут самые «плохие» варианты, а дальше предлагается лучшая альтернатива. Актуально для версии языка 2.7, отличия для версии 3.3 читайте в примечаниях к конкретному «паттерну».

Цикл по массиву из чисел

Плохо: иногда пишут так.

for i in [0, 1, 2, 3, 4, 5]:
    print i**2

 

Хорошо: лучший, с генератором. Но в 32 битной системе список из миллиона чисел будет занимать ~ 32 mb памяти.

for i in range(6):
    print i**2

 

Отлично: само лучший вариант. В отличии от второго xrange возвращает только одно значение за раз, и не нужно лишнюю память для хранения всего массива.

for i in xrange(6):
    print i**2

 

Примечание: В версии Python 3.3 xrange уже в ядре и называеться просто range.

Цикл по списку

Плохо: часто бывшие С программисты пишут так.

colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)):
    print colors[i]

 

Хорошо: лучший вариант.

colors = ['red', 'green', 'blue', 'yellow']
for color in colors:
    print color

 

Но если нужно пройти по списку задом на перед?

Плохо: опять, прошло из C дает о себе знать:

colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)):
    print colors[i]

 

Хорошо: но в Python пишут вот так:

colors = ['red', 'green', 'blue', 'yellow']
for color in reversed(colors):
    print color

 

Цикл по списку с индексами

Плохо тоже что и выше.

colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)):
    print i, '-->', colors[i]

 

Хорошо: более элегантный вариант:

colors = ['red', 'green', 'blue', 'yellow']
for i, color in enumerate(colors):
    print i, '-->', color

 

Цикл по двум спискам

Плохо тоже что и выше.

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']
n = min(len(names), len(colors))
for i in range(n):
    print names[i], '-->', colors[i]

 

Хорошо: делаем с двух списков один список кортежей. Проблема в том что zip использует больше памяти чем первый вариант.

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']
for name, color in zip(names, colors):
    print name, '-->', color

 

Отлично: в отличии от zipizip использует кэширование, что помогает существенно сэкономить память.

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']
for name, color in izip(names, colors):
    print name, '-->', color

 

Примечание: В версии Python 3.3 izip вписан в ядро и называеться просто zip.

Сортировка списка по алгоритму

Плохо: используя функцию для сравнения.

colors = ['red', 'green', 'blue', 'yellow']

def compare_length(c1, c2):
    if len(c1) < len(c2): return -1
    if len(c1) > len(c2): return 1
    return 0

print sorted(colors, cmp=compare_length)

 

Хорошо: используя сортировку по ключу. Использует намного меньше памяти.

colors = ['red', 'green', 'blue', 'yellow']

print sorted(colors, key=len)

Примечание: Метод cmp убран с ядра Python 3.x.

Цикл по ключам словаря

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

for k in d:
    print k

Для изменения словаря в цикле используйте цикл по ключам (Пример: удаление всех ключей начинающихся с R):

for k in d.keys():
    if k.startswith('R'):
        del d[k]

В этом случае d.keys() делает копию ключей словаря, что позволяет нам свободно работать с оригинальной структурой.

Цикл по ключам и значением словаря

Плохо: цикл по ключам и возвращение значение по последним. Медленный способ:

for k in d:
    print k, '-->', d[k]

 

Хорошо: быстрее делать цикл по значениях:

for k, v in d.items():
    print k, '-->', v

 

Отлично: Но само лучший и быстрый способ это использовать итератор:

for k, v in d.iteritems():
    print k, '-->', v

 

Соединение двух списков в один словарь

Очень быстрый метод, используется только один кортеж для генерации словаря.

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue']

d = dict(izip(names, colors))
# d будет иметь следующее значение: 
# {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

 

Подсчет элементов в словаре

Плохо: обычный способ:

colors = ['red', 'green', 'red', 'blue', 'green', 'red']
d = {}

for color in colors:
    if color not in d:
        d[color] = 0
    d[color] += 1

#{'blue': 1, 'green': 2, 'red': 3}

 

Хорошо: использует функцию get():

colors = ['red', 'green', 'red', 'blue', 'green', 'red']
d = {}

for color in colors:
    d[color] = d.get(color, 0) + 1

 

Отлично: само продвинутый способ это использовать defaultdict(). Но вы должны знать как он работает.

d = defaultdict(int)
for color in colors:
    d[color] += 1

 

Группирование элементов списка

Плохо: если нужно сгруппировать элементы списка по некоторому признаку (в примере — длинна строки) часто используют такой метод:

names = ['raymond', 'rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie']
d = {}

for name in names:
    key = len(name)
    if key not in d:
        d[key] = []
    d[key].append(name)
{5: ['roger', 'betty'], 6: ['rachel', 'judith'], 7: ['raymond', 'matthew', 'melissa', 'charlie']}

 

Хорошо: но есть способ гораздо элегантней и быстрее:

d = defaultdict(list)
for name in names:
    key = len(name)
    d[key].append(name)

 

Итог

На сегодня все. Надеюсь эти тривиальные, но полезные примеры помогут кому-то улучшить свой код, как они помогли это сделать мне. Их автором является Raymond Hettinger (@raymondh), Python Core Developer.