Пользователь
0,0
рейтинг
21 мая 2013 в 10:38

Разработка → Советы Google по кодированию на языке Python. Часть первая: советы по программированию recovery mode


Хай, Хабр!
Сегодня я хочу представить, дорогому хабрасообществу свой первый хабраперевод. Программировать на языке Python — подобно песне. Но еще лучше, когда Ваш код читаем и понятен, а значит чуть более поэтичен, чем обычно бывает производстве. У каждого свои правила и свои стереотипы относительно написания и оформления исходного кода, на каком бы языке он ни был написан. Множество копий сломано о щиты на форумах, но, как ни крути, нельзя не считаться с мнением авторитетных товарищей. Так что сейчас будет представлен перевод первой части стайл-гайда для языка Python от Google. Коснется он именно постулатов написания кода (вторая часть тоже скоро появится, а посвящена она будет форматированию исходного кода). Сразу предупреждаю: тут много (если не большая часть) прописных истин, которые все знают уже давно. Но я искренне надеюсь, что Вы сможете найти тут что-то новое или хотя бы вспомнить старое. Приступим под катом. И pdf тут как тут.

Google Python Style Guide


Версия: 2.48
Авторы: Amit Patel, Antoine Picard, Eugene Jhong, Jeremy Hylton, Matt Smart, Mike Shields.


Подготовка


Python является основным скриптовым языком, используемым в Google. Данное руководство — это список «хорошо» и «плохо» для программ написанных на языке Python. Чтобы помочь Вам форматировать свой код корректно, мы создали файл настроек для редактора Vim. В Emacs для наших целей должны подойти настройки по умолчанию.

Советы по программированию на Python



PyChecker

Используйте PyChecker для проверки своего кода
Определение

PyChecker — это инструмент для нахождения багов в исходниках Python-программ. Он находит проблемы, которые были бы выявлены компилятором менее динамичных языков, таких как С и С++. Это очень заманчиво. В силу динамической природы языка Python, некоторые предупреждения могут быть несправедливыми, однако ложные предупреждения не должны встречаться часто.

Плюсы

Отлавливает трудновыявляемые ошибки, такие как: ошибки типов, использование переменных перед их объявлением и т.д.

Минусы

PyChecker не идеален. Чтобы получить все его преимущества нам нужно:
  • Писать с оглядкой на него
  • Подавить его предупреждения
  • Исправлять ошибки
  • Либо не обращать внимания на них внимания

Решение

Убедитесь, что Вы запустили PyChecker с вашим кодом. Для того чтобы узнать как запустить PyChecker, загляните на его домашнюю страницу. Чтобы подавить предупреждения, вам нужно создать переменную __pychecker__ в данном модуле и указать какие ошибки должны подавляться. Например:

__pychecker__ = 'no-callinit no-classattr'

Подавление предупреждений таким путем имеет преимущество следующего плана — мы можем с легкостью производить поиск по подавлениям и возвращаться к ним. Вы можете познакомиться со списком предупреждений PyChecker, если наберете:

pychecker -- help

Неиспользуемые аргументы могут быть опущены при помощи '_', как наименования неиспользуемого аргумента, либо префикса аргумента «unused_». В ситуациях когда изменение имени аргумента невозможно, Вы можете упомянуть их в начале функции. Например:

def foo(a, unused_b, unused_c, d=None, e=None):
	_ = d, e
	return a

В идеале, PyChecker будет доработан, чтобы с уверенностью можно было утверждать, что «неиспользуемые объявления» в действительности таковые.

Импорты

Импортируйте только пакеты и модули
Определение

Механизм повторного использования открытого кода из одного модуля в другом.

Плюсы

Соглашение по пространствам имен очень простое. Местонахождение каждого объекта в коде указывается единым способом. x.Obj говорит о том, что объект Obj объявлен в модуле x.

Минусы

Иногда имена модулей могут конфликтовать. Некоторые имена модулей могут быть неудобно длинными.

Решение

Используйте import x для импортирования пакетов и модулей.
Используйте from x import y, когда x — префикс пакета, и y является именем модуля без префикса.
Используйте from x import y as z, если два модуля имеют имя y, либо если имя модуля неудобно длинное.
Например, модуль sound.effects.echo может быть импортирован так:

from sound.effects import echo
...
echo.EchoFilter(input, output, delay=0.7, atten=4)

Не используйте относительные имена в импортах, даже если модуль находится в том же самом пакете. Используйте только полное имя пакета. Это поможет предотвратить ситуацию, когда один и тот же пакет импортирован два раза.

Пакеты

Импортируйте каждый модуль, используя полный путь до него
Плюсы

Обходятся конфликты в именах модулей. Становится проще отыскать модуль на диске.

Минусы

Становится сложней переносить код, т.к. Вы должны полностью воссоздать полную иерархию модулей.

Решение

Весь Ваш новый код должен импортировать каждый модуль по его полному пути в пакете.
Импорту следует быть таким:

# Ссылка в коде с полным именем
import sound.effects.echo

# Ссылка в коде только с именем модуля (предпочтительно)
from sound.effects import echo


Исключения

Исключения разрешены, но должны использоваться осторожно
Определение

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

Плюсы

Поток управления обычного кода не перемешивается с кодом перехвата ошибок. Это так же позволяет потоку выполнения пропускать некоторое количество фреймов, когда выполняется определенное условие, например, возвращение из N-вложенных функций за один шаг вместо множественных обработок кодов ошибок.

Минусы

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

Решение

Обработка исключений должна следовать следующим позициям:
  • Возбуждайте исключения таким образом: raise MyException('Error message'), либо raise MyException. Не используйте форму записи с двумя аргументами: (raise MyException, 'Error message'), так же не используйте строковую форму записи (raise 'Error message').
  • Модули (или пакеты) должны определять свои собственные предметно-ориентированные классы исключений, которые должны наследоваться от встроенного класса Exception.
    Основное исключение модуля должно называться Error.

    class Error(Exception):
       pass
    

  • Никогда не используйте исключение, которое ловит все исключительные ситуации, только если Вы не собираетесь их перевозбудить позднее, либо Вы не находитесь во внешнем блоке кода в треде (и печатается сообщение об ошибке). Pytnon очень терпим в этом отношении, кроме того вы можете поймать всё что угодно: ошибки именования, вызов sys.exit(), прерывание Сtrl+С, провалы тестов и разного типа исключеня, которые Вы просто не хотите ловить.
  • Уменьшите количество кода находящегося в блоке try/except. Чем больше тело блока try, тем вероятней что исключение будет возбуждено в строке кода, в которой Вы не ожидаете возбуждения последнего. В этих случаях блок try/except скрывает реальную ошибку.
  • Искользуйте инструкцию finally, чтобы выполнить код независимо от того, было ли возбуждено исключение в блоке try или нет. Это часто бывает полезно для заключительных действий, например, закрытия файла.
  • При перехватывании исключения лучше использовать as, чем запятую:

    try:
       raise Error
    except Error as error:
       pass
    



Глобальные переменные

Избегайте использования глобальных переменных
Определение

Переменные, которые определены на уровне модуля.

Плюсы

Иногда полезны.

Минусы

Можно нечаянно изменить поведение модуля при импорте, т.к. присваивание переменных уровня модуля уже завершено когда модуль импортируется.

Решение

Избегайте использования глобальных переменных в пользу переменных класса. Несколько исключений ниже:
  • Стандартные настройки скриптов.
  • Константы уровня модуля. Например, PI = 3.14159. Константы должны быть именованы с использованием только заглавных букв и символа подчеркивания. Смотрите правила именования ниже.
  • Иногда полезно использовать глобальные переменные, чтобы кэшировать значения, необходимые для функции или возвращаемые функцией.
  • При необходимости, глобальные переменные должны быть созданы внутри модуля и доступны через общедоступные фукнции уровня модуля. Смотрите правила наименования ниже.


Вложенные/локальные/внутренние классы и функции

Вложенные/локальные/внутренние классы и функции - хороши
Определение

Класс может быть определен внутри метода, функции или другого класса. Функция может быть определена внутри метода или другой функции. Вложенные фукнции имеют доступ только на чтение к переменным, определенным в родительской области.

Плюсы

Делает возможным определение вспомогательных классов и функций, которые будут использованы только внутри очень ограниченного пространства. Соответствует принципу ADT.

Минусы

Экземпляры вложенных или локальных классов не могут быть сериализированы.

Решение

Они хороши.

Генераторы списков

Можно использовать в простых случаях
Определение

Генераторы списков и выражения-генераторы обеспечивают компактный и эффективный способ создавать списки и итераторы не прибегая к использованию функций map(), filter() или lambda-выражениям.

Плюсы

Простой генератор списков может быть чище и проще, чем другие способы создания списков. Выражение-генератор может быть очень эффективным, т.к. оно не создает весь список сразу.

Минусы

Сложные генераторы списков или выражения-генераторы могут быть трудночитаемы.

Решение

Можно использовать в простых случаях. Каждая часть должна быть расположена на одной строчке. Определение отображения, инструкция for, условное выражение. Несколько инструкций for или условий недопустимы. Используйте циклы, если Ваши выражения становятся слишком сложными.

Хорошо:

  result = []
  for x in range(10):
 	for y in range(5):
     	if x * y > 10:
         	result.append((x, y))

  for x in xrange(5):
 	for y in xrange(5):
     	if x != y:
         	for z in xrange(5):
             	if y != z:
                 	yield (x, y, z)

  return ((x, complicated_transform(x))
     	for x in long_generator_function(parameter)
     	if x is not None)

  squares = [x * x for x in range(10)]

  eat(jelly_bean for jelly_bean in jelly_beans
 	if jelly_bean.color == 'black')

Плохо:

  result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]

 return ((x, y, z)
     	for x in xrange(5)
     	for y in xrange(5)
     	if x != y
     	for z in xrange(5)
     	if y != z)


Стандартные итераторы и операторы

Используйте стандартные итераторы и операторы
Определение

Контейнерные типы, такие как словари и списки, определяют стандартные итераторы, набор тестовых операторов in и not in.

Плюсы

Стандартные итераторы и операторы — просты и эффективны. Они выражают операцию напрямую, без внутренних перевызовов. А функция, которая использует стандартные операторы, является простой. Она может быть использована с разными типами, которые поддерживают данную операцию.

Минусы

Вы не можете назвать типы объектов по названию метода (как, например, has_key() означает, что это словарь). Это так же является и преимуществом.

Решение

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

Хорошо:

 for key in adict: ...
  	if key not in adict: ...
  	if obj in alist: ...
  	for line in afile: ...
  	for k, v in dict.iteritems(): ...

Плохо:

 for key in adict.keys(): ...
  	if not adict.has_key(key): ...
  	for line in afile.readlines(): ...


Генераторы

Используйте генераторы по необходимости
Определение

Функция-генератор возвращает итератор, который производит значение каждый раз, когда он исполняет выражение yield. После генерации значения состояние выполнения функции приостанавливается до тех пор, пока не потребуется следующее значение.

Плюсы

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

Минусы

Минусов нет.

Решение

Прекрасно. Отдавайте предпочтение “Генерирует:", нежели «Возвращает:” в строках документации для функций-генераторов.

Лямбда-функции

Хороши для инлайновых выражений
Определение

Лямбда-функции определяют анонимные функции в выражении, как альтернатива конструкции обычной функции. Они часто используются, чтобы объявить callback-функции или операторы для функций высшего порядка, такие как map() и filter().

Плюсы

Удобно.

Минусы

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

Решение

Хорошо подходят для инлайновых выражений. Если код внутри лямбда-функции длиннее чем 60-80 символов, то, возможно, лучше определить данную функцию как обычную (или вложенную) функцию. Для простых операций, таких как умножение, используйте функции из модуля operator вместо лямбда-функций. Например, лучше использовать operator.mul вместо lambda x,y: x*y.

Условные выражения

Хороши для инлайновых выражений
Определение

Условные выражения — это механизм, который обеспечивает краткий синтаксис для конструкции if. Например, x = 1 if cond else 2.

Плюсы

Короче и более удобно, чем конструкция if.

Минусы

Может быть более трудночитаемым, чем выражение if. Условие может быть слишком сложным для написания, если выражение очень длинное.

Решение

Использовать только для инлайновых выражений. В иных случаях отдавать предпочтение использованию полноценной конструкции if.

Аргументы по умолчанию

Можно использовать в большинстве случаев
Определение

Вы можете назначить значения для переменных в конце списка аргументов функции, например, def foo(a, b=0):
Если функция foo будет вызвана только с одним аргументом, аргумент b будет равен 0. Если она будет вызвана с двумя аргументами, b будет иметь значение, переданное вторым аргументом.

Плюсы

Часто у вас есть функция, которая использует много значений по умолчанию. Но иногда Вам необходимо переопределить стандартные значения. Также Python не поддерживает перегрузку методов/функций и стандартные аргументы — это простой путь “имитировать” поведение перегрузки.

Минусы

Значения стандартных аргументов вычисляются один раз во время загрузки модуля. Это может являться причиной проблемы, если аргумент является изменяемым объектом, таким как список или словарь. Если функция изменяет объект (например, посредством добавления элемента в список), то стандартное значение будет изменено.

Решение

Можно использовать со следующими предостережениями:
Не используйте изменяемые объекты в качестве стандартных значений в функциях или определениях методов.

Хорошо:

def foo(a, b=None):
    	if b is None:
        	b = []

Плохо:

def foo(a, b=[]):

Вызов кода должен использовать именованные значения для аргументов со значением по умолчанию. Это позволяет документировать код и помогает предотвращать и выявлять дефекты, когда передано больше аргументов, чем нужно.
def foo(a, b=1):
   ...


Хорошо:

foo(1)
foo(1, b=2)

Плохо:

foo(1, 2)


Свойства

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

Определение

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

Плюсы

Удобочитаемость повышается за счет исключения явных вызовов get- и set-методов для простого доступа к атрибуту. Возможны ленивые вычисления. Считается, что поддерживать интерфейс класса — часть пути Python. С точки зрения производительности, позволяет свойству быть доступным через простейший метод-геттер (аксессор), когда нужен прямой доступ к переменной. Так же позволяет методам доступа быть добавленными позднее без изменения интерфейса.

Минусы

Свойства объявляются после того, как методы get и set были объявлены, требуя сообщить им, что они используются для свойств, находящихся ниже в коде (исключая свойства доступные только на чтение, созданные с помощью декоратора @property, о нем смотрите ниже). Они должны наследоваться от класса object. Могут скрывать побочные эффекты, такие как оператор перегрузки. Могут вводить в заблуждение классы-наследники.

Решение

Используйте свойства в только что написанном коде для доступа или изменения данных там, где Вы бы использовали простой get-метод или set-метод. Свойства „только на чтение“ должны создаваться с помощью декоратора @property. Наследование со свойствами может быть не очень прозрачным, если свойство родителя не будет переопределено. Таким образом, необходимо убедиться, что методы-геттеры вызываются косвенно, чтобы обеспечить переопределение методам в подклассах, вызываемых через свойство (используя паттерн Шаблонный метод)

Хорошо:

import math

class Square(object):
    	"""A square with two properties: a writable area and a read-only perimeter.

    	To use:
    	>>> sq = Square(3)
    	>>> sq.area
    	9
    	>>> sq.perimeter
    	12
    	>>> sq.area = 16
    	>>> sq.side
    	4
    	>>> sq.perimeter
    	16
    	"""

    	def __init__(self, side):
        	self.side = side

    	def __get_area(self):
        	"""Calculates the 'area' property."""
        	return self.side ** 2

    	def ___get_area(self):
        	"""Indirect accessor for 'area' property."""
        	return self.__get_area()

    	def __set_area(self, area):
        	"""Sets the 'area' property."""
        	self.side = math.sqrt(area)

    	def ___set_area(self, area):
        	"""Indirect setter for 'area' property."""
        	self.__set_area(area)

    	area = property(___get_area, ___set_area,
                   	doc="""Gets or sets the area of the square.""")

    	@property
    	def perimeter(self):
        	return self.side * 4


Вычисления True/False

Используйте False явно, если это возможно
Определение

Python вычисляет определенные значения в False, когда мы находимся в контексте булевых значений. Самый просто способ запомнить это — знать, что все “пустые” значения, такие как False, 0, None, [], {} — вычисляются в False в булевом контексте.

Плюсы

Такие условные конструкции, использующие булевы значения Python, удобочитаемы и меньше подвержены ошибкам. В большинстве случаев они работают быстрее.

Минусы

Могут выглядеть странно для разработчиков на С/С++.

Решение

Используйте False явно, если это возможно. Например, if foo: лучше, чем if foo != []:. Вы должны помнить несколько исключений:
  • Никогда не используйте == или != для сравнения объектов синглтона, таких как None. Используйте is или is not.
  • Остерегайтесь написания if x:, когда Вы подразумеваете if x is not None:, например, когда происходит тестирование переменной или аргумента, которые по умолчанию равны None, но их значение было изменено на другое. Другое значение может вычисляться в False при булевой проверке.
  • Никогда не сравнивайте булеву переменную с False, используя ==. Используйте if not x вместо этого. Если вам нужно отличить False от None, тогда используйте цепочечное выражение, такое как if not x and x is not None.
  • Для последовательностей (строк, списков, кортежей) используйте тот факт, что пустая последовательность вычисляется в False, таким образом if not seq или if seq лучше, чем if len(seq) или if not len(seq):.
  • Когда Вы работаете с целыми числами, явное сравнение с False несет больший риска, чем выгоды (случайная обработка None как 0). Вы можете сравнивать значение, которое является целым числом (и не является результатом выполнения функции len()) c нулем.


Хорошо:

if not users:
	print "no"

if foo == 0:
	self.handle_zero()

if i % 10 == 0:
 	self.handle_multiple_of_ten()


Плохо:

if len(users) == 0:
 	print 'no users'

if foo is not None and not foo:
 	self.handle_zero()

if not i % 10:
 	self.handle_multiple_of_ten()


Заметьте, что “0” (т.е. 0 как строка) вычисляется в True.

Устаревшие возможности языка

Используйте методы вместо модуля string там, где это возможно
Используйте методы вместо модуля string там, где это возможно. Используйте вызов функции посредством синтаксиса, а не функцию apply. Используйте генераторы списков и циклы for вместо функций filter и map там, где аргумент функции будет иметь инлайновое лямбда-выражение. Используйте циклы вместо reduce.

Определение

Настоящая версия языка Python предоставляет альтернативные конструкции которые люди находят определенно лучшими.

Решение

Мы не используем версии Python, которые не поддерживают эти возможности, так что нет причин использовать новые стили.

Хорошо:

words = foo.split(':')
[x[1] for x in my_list if x[2] == 5]
map(math.sqrt, data)	# Все хорошо - нет инлайновых лямбда выражений
fn(*args, **kwargs)


Плохо:

words = string.split(foo, ':')
map(lambda x: x[1], filter(lambda x: x[2] == 5, my_list))


Лексический контекст

Разрешается к использованию
Определение

Вложенные функции в питоне могут ссылаться на переменные, определенные в объемлющих функциях, но не могут выполнять присваивание им. Связи переменных разрешаются с помощью лексического контекста, который основан на статичном тексте программы. Любое присваивание имени в блоке кода заставляет Python обработать все ссылки на это имя, даже если использование предшествует объявлению. Если существует глобальное объявление, то имя будет обработано как глобальная переменная.
Пример использования этой возможности:

def get_adder(summand1):
   """Returns a function that adds numbers to a given number."""
   def adder(summand2):
   	return summand1 + summand2

   return adder

Плюсы

Часто приводит к более чистому и элегантному коду. Особенно комфортно с этим работать для Lisp и Scheme (и Haskell и ML и..) программистов.

Минусы

Может приводить к странным ошибкам, таким как в этом примере взятом из PEP-0227:
i = 4
def foo(x):
   def bar():
   	print i,
   # ...
   # Здесь куча кода
   # ...
   for i in x:  # Ох, переменная i локальна для Foo, так ее-то Bar и видит.
   	print i,
   bar()

таким образом foo([1,2,3]) выведет 1 2 3 3, а не 1 2 3 4.

Решение

Можно использовать.


От переводчика


Благодарности

Хочу выразить огромное спасибо SquaII, за вычитывание и помощь в пунктуации.

Соц. опрос
А Вы пользуетесь стайл-гайдами?

Проголосовало 1170 человек. Воздержалось 562 человека.

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

Code : super @Lovesuper
карма
–1,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (57)

  • +6
    Используйте циклы вместо reduce.

    Чем он их не устраивает? Если используются функции из operator или что-нибудь подобное, то код выглядит очень понятно и коротко, как и с map или filter.
    • 0
      Ну, читал, что reduce не стоит использовать т.к. он слишком запутанно работает. И схема его работы не всегда является прозрачной, особенно при использовании лямбда-функций.
      • +8
        При желании и генераторы списков можно писать трёхэтажные — потом в них чёрт ногу сломит. Аккуратный reduce — это красиво, коротко, ясно. Я за reduce :)
        • 0
          Да и я по большому счету тоже:)
        • 0
          Ключевое слово аккуратный, я тоже стараюсь использовать map, filter, reduce, zip аккуратно, и смотрится достаточно красиво (для меня по крайней мере). А как Вы, например, относитесь к строчке кода, в которой вместе используется reduce, zip, генератор списка и еще что-нибудь — такие участки кода порой встречаются. Мне они не всегда кажутся очевидными.
    • +9
      reduce целенаправленно выдавливают из языка. В 3 версии он уже выкинут в модуль functools. У меня сложилось впечатление, что Гвидо считает использование функциональных возможностей не «питонистическим» подходом.
      • 0
        Гвидо же вроде отошел от разработки к консультированию?
        • 0
          В одном из выпусков radiot говорили, что вроде как консультирует по вопросу: как правильно писать на Python в Dropbox.
    • +3
      Эту идею продвигает и Гвидо ван Россум, потому что считает, что использование reduce'а в целом сложнее для восприятия чем использование цикла. Кроме того он несколько проигрывает по производительности, поэтому в третьей версии Python он был вынесен из стандартной библиотеки в модуль functools.

      Почитать про это подробнее можно в этом посте.
      • +1
        Кроме того он несколько проигрывает по производительности
        from time import time
        from functools import reduce
        from operator import add
        
        lst = range(1,10000000)
        r1, r2 = 0, 0
        
        t1 = time()
        r1 = reduce(add, lst)
        t1 = time() - t1
        
        t2 = time()
        for i in lst: 
            r2 = add(i, r2)
        t2 = time() - t2 
        
        print(t1)
        print(t2)

        >>> 1.0490000248
        >>> 2.14700007439
        • 0
          Это если вы используете модуль operator. Если нужно создавать свою лямбду, то вариант с циклом быстрее.
          • 0
            Вы имеете ввиду такой вариант:

            from time import time
            from functools import reduce
            
            lst = xrange(1,10000000)
            r1, r2 = 0, 0
            
            t1 = time()
            r1 = reduce(lambda x, y: x + y, lst)
            t1 = time() - t1
            
            t2 = time()
            for i in lst: 
            	r2 = i + r2
            t2 = time() - t2 
            
            print(t1)
            print(t2)

            Но такой вариант очень часто не применим, т.к. нужно использовать уже готовую функцию, а не писать лямбду (или выносить логику в цикл). К тому же, получается, не сильно быстрее:

            >qqq.py
            1.83299994469
            1.74499988556

            >qqq.py
            1.79699993134
            1.79299998283

            >qqq.py
            1.75500011444
            1.81399989128


            Но в целом я согласен, код с reduce тяжелее читается.
            • 0
              Да, такой вариант. Ниже ответил habrahabr.ru/post/179271/#comment_6264423
              Вообще reduce замечательная вещь, но только в ФП языках. А Python, как ни крути, таковым не является.
            • +6
              Но в целом я согласен, код с reduce тяжелее читается

              Reduce — это, в первую очередь, идея: вы берёте список и аккумулируете его элементы, получая на выходе одно значение.

              For loop — это тоже идея, но другая. For позволяет итерировать по списку, т.е. последовательно получать доступ к каждому элементу.

              For можно заменить на reduce, равно как и reduce на for, но по сути это будет реализацией одной фичи через другую — концептуально они останутся разными и предназначенными для разных задач. Сравниете два сценария:

              1. В 3Б классе 25 учеников, нужно вывести на экран их имена и рост в сантиметрах.
              2. В 3Б классе 25 учеников, нужно найти самого высокого из них.

              В первом случае напрашивается цикл:

              for p in pupils: 
                  print p.name, p.height
              

              Давайте попробуем прочитать «вслух» этот кусок кода: «для каждого ученика в списке: распечатать его имя и рост». Как раз то, что мы хотели сделать, практически дословное соответсвие условию. Т.е. код читабелен, а readability, как мы помним, counts.

              Второй сценарий тоже можно записать в виде цикла:

              highest_pupil = None 
              max_height = 0
              for p in pupils: 
                  if p.height > max_height: 
                      highest_pupil = p
                      max_height = p.height
              return highest_pupil
              

              Но давайте и его попробуем прочитать: «присвоить самому высокому ученику значение None; установить максимальную высоту в 0; для каждого ученика в списке: если его высота больше максимальной: присвоить переменной самого выского ученика значение текущего ученика, присвоить максимальной высоте значение высоты текущего ученика». Чёрт, получилось как-то побольше и посложнее, чем было в условии. Но главное, что это описание не отражает идею, а лишь последовательность действий, которые нужно совершить.

              Теперь попробуем реализовать второй сценарий через reduce:

              reduce(lambda p1, p2: p1 if p1.height > p2.height else p2, pupils)
              

              Или, если лямбды воспринимаются плохо, то так:

              def higher_pupil(p1, p2): 
                  if p1.height > p2:
                      return p1
                  else:
                      return p2
              
              reduce(higher_pupil, pupils)
              


              Что можно прочитать как: «Свернуть список учеников, выбирая между двумя из них того, у которого рост выше». Не сказать, что это дословное выражение условия, но уже гораздо, гораздо ближе. И это далеко не предел читабельности. Вот ещё несколько примеров:

              1. Имея список инвесторов, найти общую сумму полученных инвестиций:
              reduce(lambda s, inv: s + inv.investment, investors)
              

              «Аккумулировать (в возвращаемую переменную s) инвестиции от каждого инвестора».

              2. Объединить (т.е., опять же, аккумулировать) строки (или списки):
              reduce(lambda result, s: result + s, strings)
              


              и т.д. Другое дело, что ни питоновские лямбды, ни стандартные коллекции, ни общая философия не могут похвастаться хорошей совместимостью с reduce-ом. Возьмём, например, простую задачу подсчёта слов в списке. С точки зрения reduce-а, задача элементарна: нужно всего лишь аккумулировать слова в списке в словарь, добавляя или инкрементируя счётчик для каждого слова. Что-то вроде:

              reduce(add_or_inc, words, {})
              

              Проблема возникает именно в этой функции add_or_inc. Однострочные лямбды не позволяют толком использовать if-ы, а defaultdict, почти идеально подходящий для этого случая, работает исключительно императивно. В итоге приходится делать add_or_inc отдельной функцией, что значительно увеличивает размер кода. С другой стороны, for loop, хотя и не выражает идею, но позволяет решить задачу в 3 строчки:

              d = defaultdict(int)
              for w in words: 
                  d[w] += 1
              


              Так что я бы сказал, что reduce малочитабелен не сам по себе, а именно в контексте существующей инфраструктуры языка. В других языках или в контексте отдельных библиотек он может помогать писать очень и очень читабельный код.
              • 0
                Думаю речь о нечитабельности reduce шла именно в контексте питона, для всех упомянутых вами задач есть решения более «питонистские».
                Имея список инвесторов, найти общую сумму полученных инвестиций

                sum([inv.investment for inv in investors])
                

                Объединить (т.е., опять же, аккумулировать) строки (или списки)

                ''.join(strings)
                

                В 3Б классе 25 учеников, нужно найти самого высокого из них.

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

                pupils.sort(key=lambda pupil: pupil.height)
                pupils[0]
                

                Либо можно выводить максимальный рост, при необходимости находя такого ученика (хотя конечно получается два прохода вместо одного):

                max_height = max([pupil.height for pupil in pupils])
                highest_pupil = [for pupil in pupils if pupil.height==max_height][0]
                

                И кстати, ваш цикл можно сократить на две строчки получив «читабельное» выражение:

                highest_pupil = pupils[0]
                for pupil in pupils: 
                    if pupil.height > highest_pupil.height: 
                        highest_pupil = pupil
                return highest_pupil
                

                Т.е. «берём первого попавшегося и сравниваем по высоте с остальными».

                Иначе говоря, сам язык и его инструментарий не очень располагает к reduce'ам и другим функциональным приёмам, посему мне кажется вполне логично что их убирают в закрома библиотек.
                • +2
                  Я ещё раз повторюсь: в reduce-е важна идея — взять все элементы и собрать по ним некую «статистику». Это как с for (сишным) и foreach — любой foreach можно элементарно развернуть в for, но если вам нужно обработать каждый элемент списка, то вы используете foreach. Так вы явно декларируете своё намерение, тем самым повышая читабельность.

                  Что касается примеров, то попробуйте чуть-чуть усложнить задачу и более «питоновские» решения сломаются. Например, что делать, если нам нужна не сумма инвестиций, а имя самого активного инвестора? Встроенные функции типа sum уже на сработают. Или вернуть не просто сумму, а объект InvestmentStats, который собирает сразу несколько статистик (учтите, что список инвесторов большой и читается с диска, так что делать несколько проходов — не comme il faut). Или нужно объединить списки/словари, а не строки. Или объединить список векторов в матрицу. Я могу приводить примеры бесконечно.

                  Что касается самого высокого ученика, то ваше решение некорректно: если список учеников пуст, то `highest_pupil = pupils[0]` выдаст ошибку. А значит надо проверять два кейса — список пусть или не пуст, а значит код ещё больше расширяется и становится ещё менее понятным (эй, мне нужно было просто найти самого высокого ученика! какие ещё дополнительные условия?). reduce прекрасно обрабатывает и этот случай (правда, в мой код нужно добавить начальное значение, но кейс так или иначе покрыт).

                  Ну и да, проходить по списку несколько раз или, тем более, сортировать его только для того, чтобы найти самый большой элемент — это ну совсем-совсем нехорошо.

                  Насчёт того, что стандартный инструментарий не располагает к использованию reduce-а — согласен, примерно это я и писал в предыдущем комментарии. Другой вопрос, стоило ли выкидывать reduce или следовало ещё раз подумать над инструментарием. Всё-таки reduce описывает довольно распространённый паттерн, а Гвидо вместо него предлагает ввести набор частных решений, таких как sum(), product(), any(), all() и другие.
                  • 0
                    Для меня лично, основное преимущество foreach относительно for — отсутствие необходимости в изменяемой «вручную» индексной переменной: меньше «менеджимых» програмимстом операторов — меньше потенциальных мест для ошибки.

                    Что касается самого высокого ученика, то ваше решение некорректно: если список учеников пуст, то `highest_pupil =pupils[0]` выдаст ошибку.
                    Тогда корректность самой задачи сомнительна. SQL, насколько я помню, max() в этом случае выдаст null — когда впервые увидел, был удивлён. Корректность применения фрагмента
                    max_height = 0
                    
                    перед циклом в этом случае тоже дискуссионна.
                  • 0
                    «Ручное» аккумулирование и «ручное» задание первоначального значения — тоже потенциальные места для ошибки, если совсем строго подходить.
                    • 0
                      Для меня лично, основное преимущество foreach относительно for — отсутствие необходимости в изменяемой «вручную» индексной переменной: меньше «менеджимых» програмимстом операторов — меньше потенциальных мест для ошибки.
                      

                      Дело не только в ошибках — они происходят на этапе написания кода. Дело в понимании кода на этапе перечитывания, и Python делает на этом очень большой упор. Из PEP8:

                      One of Guido's key insights is that code is read much more often than it is written. The guidelines provided here are intended to improve the readability of code and make it consistent across the wide spectrum of Python code.


                      Тогда корректность самой задачи сомнительна. SQL, насколько я помню, max() в этом случае выдаст null — когда впервые увидел, был удивлён.

                      А в чём сомнительность задачи? Вы сомневаетесь, следует ли обрабатывать списки нулевой длины? А что вы тогда предлагаете делать с такими списками, на примере того же SQL — что, по вашему мнению, должна возвращать функция max, если селект вернул пустой result set?
                      • 0
                        Вы сомневаетесь, следует ли обрабатывать списки нулевой длины?
                        Уже до и за меня решено, что придётся это как-то делать. Сомнительна не задача, а корректность её постановки: что делать в случае пустого списка, не ясно.
                        А что вы тогда предлагаете делать с такими списками, на примере того же SQL — что, по вашему мнению, должна возвращать функция max, если селект вернул пустой result set?
                        Тогда я ожидал ноль. Но потом понял, что null — корректнее: положительность, в общем случае, никем не гарантирована. Ну тут пути три — 1) вернуть ноль (имеет смысл, если тип данных явно задан как положительный) 2) вернуть null или другое спец.значение 3) кинуть исключение. Выбор за разработчиком исходя из уточнённого смысла задачи.
                        • 0
                          Пардон, но я вообще не понял, что вы хотели сказать. Вы говорите, что корректность постановки задачи про максимальный элемент в (возможно пустом) списке сомнительна, но тут же приводите пример из SQL, где она сформулирована именно так. Так вас устраивает постановка задачи и её решение в SQL или нет? Если да, то в чём тогда проблема с моей формулировкой? Если нет, то какую вы предлагаете альтернативу, на примере того же SQL?
                          • –1
                            Сомнительна в случае, если список может быть пустой, и не указано, что в этом случае если список пустой. В исходном варианте в вышележащих комментариях было «В 3Б классе 25 учеников», так что явно указано, что можно не закладываться на возможность пустого списка.
                            • 0
                              Вы опять противоречите себе: сначала вы говорите, что до и за вас решено, что пустой список тоже придётся как-то обрабатывать, а теперь ссылаетесь на конкретный пример про 25 (что явно не 0) учеников. Давайте так: если вам действительно интересна эта тема, то скажите, что конкретно в моих примерах вас не устраивает и как бы вы их реализовали; иначе я считаю это троллингом и выхожу из дискуссии.
                              • –1
                                Всё устраивает, это были мелкие перфекционистские придирки из серии «а вот тут, как видно, разработчик решил за проектировщика», тут вроде дискуссия не о проектировании ПО, так что тоже не вижу смысла дальше углубляться ;-)
          • 0
            Дабы не быть голословным (python 3.3)
            Скрытый текст
            from functools import reduce
            from timeit import timeit
            import operator
            
            def t0():
                 return sum(lst)
            
            def t1():
                 return reduce(operator.add, lst)
                 
            def t2():
                 a = 0
                 for b in lst:
                     a += b
                 return b
                 
             def t3():
                 return reduce(lambda x, y: x + y, lst)
            
            timeit(t0, number=100)
            # 0.7890550769952824
            
            timeit(t1, number=100)
            # 3.882541635997768
            
            timeit(t2, number=100)
            # 3.9364478749994305
            
            timeit(t3, number=100)
            # 7.679830512999615
            

            В общем быстрее тот код, где меньше вызовов pyhon-функций. Reduce+operator хорошо, свои лямбды — не очень.
            • +1
              Упс, в коде пропущено lst = list(range(1000000)), запускал в интерактивном интерпретаторе, и не все скопировал.
  • –15
    Ошибок и опечаток в тексте многовато.
    • +8
      Вы уж приведите примеры, пожалуйста. Можно в личку. Я обязательно всё поправлю!
      • –27
        Если бы их было две-три — я бы так и поступил, но вычитывать за Вас Вашу же статью, извините, не стану.
        • +15
          Ну на нет и суда нет. Я еще раз по ней пройдусь.
  • 0
    Неплохая подборка, спасибо. Хотел бы высказаться по поводу использования map() filter(). Слышал в некоторых источниках, что не следует их мешать с циклом for. Т.е. например, если есть метод/функция и в нём используется цикл for, то если существует еще какая-то логика в данном методе, то её тоже следует писать с циклом for, а не использовать map, который вполне может заменить использование цикла. Данные идеи исключельно для некоторой цельности и выдерживания кода некоторой единой нотации. Соответственно и обратное замечание, что если уж используется map в логике метода/функции, то и остальную логику следует писать с использованием map, filter и т.д… Весьма спорно, но всё же. Согласны?
    • +1
      Согласен. Но я вот стою на том, что функциональности нужно использовать там, где им пристало быть исконно. Например, отсев каких-то значений в списке, преобразование каждого элемента в списке и т.д. А уж for хорошо испльзовать для сложной логики, тем более, что функциональщина в Python быстрее отрабатывает.
    • +1
      map() и filter() в Python можно полностью заменить на list comprehension, и это будет смотреться вполне прилично — в обоих случаях используется for, но в list comprehension ещё дополнительно показывается, что на выходе должен получится список.
    • 0
      В отличие от LISP, в Python есть синтаксис;-) (в смысле, кроме деревьев), в том числе и специальные идиомы языка, являющиеся аналогами этих функций.
      • 0
        Я ведь немного другое имел ввиду. Я не об аналогах, а о применении рядом в коде (звучит нехорошо, пусть например в одном методе) функциональщины и циклов for.
  • +7
    Картинка по запросу «кодирование»
    image
    • 0
      Да, в ВК на эту тему меня уже троллили:)
  • 0
    > Используйте PyChecker для проверки своего кода.
    PyChecker компилирует и выполняет код, на мой взгляд лучше pylint использовать.
    • +1
      В PyCharm последней версии идёт проверка на лету на pep8, очень так удобно.
      • 0
        sublime text 2 тоже проверяет на PEP8 с модулем «Python PEP8 Autoformat».
    • 0
      pylint разве не компилирует?
      • 0
        Нет, он использует AST для анализа.
        • 0
          Если я правильно помню, то по-умолчанию (в стандартных проверках) даже АСД не использует — только токенайзер.
    • 0
      На текущем проекте используем flake8. Он тоже опрятности способствует, особенно будучи вписанным в предкоммитном хуке.
  • +4
    В опросе не хватает пункта: «Не согласен с положениями GSG частично или полностью»
    • +2
      Ну вопрос состоял не в том, что пользуетесь ли вы гугловским стайлгайдом или нет, а вообще про стайлгайды:)
  • 0
    def ToFloat(Arg, Len=4):
    return "".join([To2Hex(ord(X)) for X in (struct.pack(">f", Arg)[0:Len])][::-1])

    гы гы гы. pychecker на этом упал
    • +3
      Для Вас будет вторая часть статьи скоро!
  • +1
      result = []
      for x in range(10):
         for y in range(5):
             if x * y > 10:
                 result.append((x, y))
    

    vs

    result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]
    


    По-моему, второй вариант читается на порядок проще и быстрее. А когда я встречаю 4хэтажную конструкцию, то я сразу хочу ее развидеть.
    • 0
      Для такого линейного случая — прекрасная альтернатива! Но если логика сложнее, то лучше воздержаться от инлайновых вычислений.
      • +1
        Просто первое выражение не было записано в примеры хорошего, а второе — плохого кода. В итоге, мне все меньше наравится Google Code Guides.
        • +1
          В итоге нам не судить:) Вот пойдете работать в гугл — придется жить по их законам. А пока это только взгляд со стороны на правила гиганта индустрии.
  • +3
    Статья — гуд. Реквестирую PDF.
    • +2
      Спасибо, как только выйдет вторая часть перевода. Я сразу же закатаю всё в PDF и добавлю ссылку.
      • 0
        будем ждать, материал действительно хороший
        • +1
          недолго осталось) Может пара-тройка дней. Я еще этот материал выпиливаю, чтобы ошибок не было. Главное — качество.
  • +1
    Было интересно, спасибо за перевод. Жду продолжения!

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