рефакторинг


Недавно наткнулся на полезную, но немного устаревшую статью «Python Idioms and anti-idioms»

Давайте разберем ее применительно к сегодняшней реальности.

from module import *

Цитата:

from module import * is invalid inside function definitions.

While it is valid to use from module import * at module level it is usually a bad idea. For one, this loses an important property Python otherwise has — you can know where each toplevel name is defined by a simple “search” function in your favorite editor. You also open yourself to trouble in the future, if some module grows additional functions or classes.

Remember, you can never know for sure what names a module exports, so either take what you need — from module import name1, name2, or keep them in the module and access on a per-need basis — import module; print module.name.

[оригинал]

Почти согласен.

  1. Возможность четко найти в текущем файле откуда взято любое имя действительно ценна.

  2. Использование import * рано или поздно приводит к каскадному распространению имен, что значительно затрудняет отладку и снижает логическую изолированность частей программы.

Unadorned exec, execfile() and friends

Цитата:

The word “unadorned” refers to the use without an explicit dictionary, in which case those constructs evaluate code in the current environment. This is dangerous for the same reasons from import * is dangerous — it might step over variables you are counting on and mess up things for the rest of your code. Simply do not do that.

[оригинал]

Полностью согласен.

Кроме того, надо помнить, что оттенки eval всегда опасны, и должны использоваться только если совсем нет других вариантов. При малейшей возможности eval надо менять на getattr/setattr и т.п.

from module import name1, name2

Цитата:

This is a “don’t” which is much weaker then the previous “don’t”s but is still something you should not do if you don’t have good reasons to do that. The reason it is usually bad idea is because you suddenly have an object which lives in two separate namespaces. When the binding in one namespace changes, the binding in the other will not, so there will be a discrepancy between them. This happens when, for example, one module is reloaded, or changes the definition of a function at runtime.

[оригинал]

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

except:

Цитата:

Python has the except: clause, which catches all exceptions. Since every error in Python raises an exception, this makes many programming errors look like runtime problems, and hinders the debugging process.

[оригинал]

Полностью согласен, ловля всех исключений без разбора неприемлема. Даже базовый Exception допустимо ловить только тогда, когда ты очень хорошо понимаешь, что делаешь. В подавляющем большинстве случаев задача решается отловом одного-двух более конкретных типов исключений.

Exceptions

Цитата:

Exceptions are a useful feature of Python. You should learn to raise them whenever something unexpected occurs, and catch them only where you can do something about them.

So, try to make as few except clauses in your code — those will usually be a catch-all in the main(), or inside calls which should always succeed.

[оригинал]

Полностью согласен, изложены грамотные практики работы с исключениями.

Единственное замечание к этой главе в том, что показана устаревшая практика утилизации ресурсов:

def get_status(file):
    fp = open(file)
    try:
        return fp.readline()
    finally:
        fp.close()

Современный подход предполагает использование with:

def get_status(file):
    with open(file) as fp:
        return fp.readline()

Using the Batteries

Цитата:

Every so often, people seem to be writing stuff in the Python library again, usually poorly. While the occasional module has a poor interface, it is usually much better to use the rich standard library and data types that come with Python then inventing your own.

[оригинал]

Да, изобретение велосипеда никогда не было хорошей идеей.

Между тем, все приведенные в оригинальной статье примеры не очень удачны:

  1. Формирование путей через сложение строк вполне допустимо, если всегда использовать прямой слэш, который вполне нормально работает и в Windows.

  2. Использование reduce сейчас считается плохой практикой, да и стандартная функция sum давно существует.

Using Backslash to Continue Statements

Цитата:

Since Python treats a newline as a statement terminator, and since statements are often more then is comfortable to put in one line, many people do:

if foo.bar()['first'][0] == baz.quux(1, 2)[5:9] and \
    calculate_number(10, 20) != forbulate(500, 360):
        pass

You should realize that this is dangerous: a stray space after the \ would make this line wrong, and stray spaces are notoriously hard to see in editors.

It is usually much better to use the implicit continuation inside parenthesis

[оригинал]

Полностью согласен.

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

дополнение

Вот несколько примеров переноса строк без обратного слэша:

  1. Длинный if

    if request.host in self.lastRequestTime and \
        (time.time() - self.lastRequestTime[request.host] < self.throttleDelay):
    

    Меняем на:

    if (request.host in self.lastRequestTime and
        (time.time() - self.lastRequestTime[request.host] < self.throttleDelay)):
    
  2. Используем уже имеющиеся скобки

    if internal_id in [str(ch.settings_dict.get('id')) \
        for ch in self.campaign_obj.channel_set.filter(type=channel.type)]:
        raise forms.ValidationError(['You have already subscribed for this %s' % channel.type_name]) 
    

    Меняем на:

    if internal_id in [
        str(ch.settings_dict.get('id'))
        for ch in self.campaign_obj.channel_set.filter(type=channel.type)
        ]:
        raise forms.ValidationError(['You have already subscribed for this %s' % channel.type_name])
    
  3. Автоматическре склеиваение строковых переменных

    raw_input("== External authentication\n" +
              "Please open this URI in browser:\n%s\n" +
              "When page finishes loading press any key to continue" % api_client.get_login_uri())
    

    Меняем на:

    raw_input("== External authentication\n"
              "Please open this URI in browser:\n%s\n"
              "When page finishes loading press any key to continue" % api_client.get_login_uri())
    

Спасибо Артему Скорецкому и Анатолию Иванову за участие в обсуждении.