Pull to refresh

Python Tips, Tricks, and Hacks (часть 3)

Reading time 6 min
Views 49K
Original author: David
В этой части статьи рассматриваются уловки для выбора одного из двух значений на основе логического условия, передача и получение произвольного числа аргументов функций, а также распространенный источник ошибок — тот факт, что дефолтные значения аргументов функции вычисляются только один раз.

4. Выбор значений


4.1. Правильный путь

Начиная с версии 2.5, Python поддерживает синтаксис «value_if_true if test else value_if_false». Таким образом, вы можете выбрать одно из двух значений, не прибегая к странному синтаксису и подробным пояснениям:
test = True
# test = False
result = 'Test is True' if test else 'Test is False'
# result = 'Test is True'

Увы, это всё еще немного некрасиво. Вы также можете использовать несколько таких конструкций в одной строке:
test1 = False
test2 = True
result = 'Test1 is True' if test1 else 'Test1 is False, test2 is True' if test2 else 'Test1 and Test2 are both False'

Сначала выполняется первый if/else, а если test1 = false, выполняется второй if/else. Вы можете делать и более сложные вещи, особенно если воспользуетесь скобками.

Этот способ весьма новый, и я испытываю к нему смешанные чувства. Это правильная, понятная конструкция, она мне нравится… но она всё еще уродлива, особенно при использовании нескольких вложенных конструкций. Конечно, синтаксис всех уловок для выбора значений некрасив. У меня слабость к описанному ниже способу с and/or, сейчас я нахожу его интуитивным, сейчас я понимаю, как он работает. К тому же он ничуть не менее эффективен, чем «правильный» способ.

Хотя инлайновый if/else — новый, более правильный способ, вам всё же стоит ознакомиться со следующими пунктами. Даже если вы планируете использовать Python 2.5, вы встретите эти способы в старом коде. Разумеется, если вам нужна обратная совместимость, будет действительно лучше просмотреть их.

4.2. Уловка and/or

«and» и «or» в Python — сложные создания. Применение and к нескольким выражениям не просто возвращает True или False. Оно возвращает первое false-выражение, либо последнее из выражений, если все они true. Результат ожидаем: если все выражения верны, возвращается последнее, являющееся true; если одно из них false, оно и возвращается и преобразуется к False при проверке логического значения.

Аналогично, операция or возвращает первое true-значение, либо последнее, если ни одно из них не true.

Это вам не поможет, если вы просто проверяете логическое значение выражения. Но можно использовать and и or в других целях. Мой любимый способ — выбор значения в стиле, аналогичном тернарному оператору языка C «test? value_if_true: value_if_false»:
test = True
# test = False
result = test and 'Test is True' or 'Test is False'
# теперь result = 'Test is True'

Как это работает? Если test=true, оператор and пропускает его и возвращает второе (последнее) из данных ему значений: 'Test is True' or 'Test is False'. Далее, or вернет первое true выражение, т. е. 'Test is True'.

Если test=false, and вернет test, останется test or 'Test is False'. Т. к. test=false, or его пропустит и вернет второе выражение, 'Test is False'.

Внимание, будьте осторожны со средним значением («if_true»). Если оно окажется false, выражение с or будет всегда пропускать его и возвращать последнее значение («if_false»), независимо от значения test.

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

Конечно, если вам нужна совместимость с предыдущими версиями Python, «правильный» способ не будет работать. В этом случае and/or — лучший выбор в большинстве ситуаций.

4.3. True/False в качестве индексов

Другой способ выбора из двух значений — использование True и False как индексов списка с учетом того факта, что False == 0 и True == 1:
test = True
# test = False
result = ['Test is False','Test is True'][test]
# теперь result = 'Test is True'

Этот способ более честный, и value_if_true не обязано быть true. Однако у него есть существенный недостаток: оба элемента списка вычисляются перед проверкой. Для строк и других простых элементов это не проблема. Но если каждый из них требует больших вычислений или операций ввода-вывода, вычисление обоих выражений недопустимо. Поэтому я предпочитаю обычную конструкцию или and/or.

Также заметьте, что этот способ работает только тогда, когда вы уверены, что test — булево значение, а не какой-то объект. Иначе придется писать bool(test) вместо test, чтобы он работал правильно.

5. Функции


5.1. Значения по умолчанию для аргументов вычисляются только один раз

Начнем этот раздел с предупреждения. Эта проблема много раз смущала многих программистов, включая меня, даже после того, как я разобрался в проблеме. Легко ошибиться, используя значения по умолчанию:
def function(item, stuff = []):
    stuff.append(item)
    print stuff

function(1)
# выводит '[1]'

function(2)
# выводит '[1,2]' !!!

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

Решение проблемы: не используйте изменяемые объекты в качестве значений по умолчанию. Вы можете оставить всё как есть, если не изменяете их, но это плохая идея. Вот как следовало написать предыдущий пример:
def function(item, stuff = None):
    if stuff is None:
        stuff = []
    stuff.append(item)
    print stuff

function(1)
# выводит '[1]'

function(2)
# выводит '[2]', как и ожидалось

None неизменяем (в любом случае, мы не пытаемся его изменить), так что мы обезопасили себя от внезапного изменения дефолтного значения.

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

5.1.1. Заставляем дефолтные значения вычисляться каждый раз

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

def resetDefaults(f):
    defaults = f.func_defaults
    def resetter(*args, **kwds):
        f.func_defaults = deepcopy(defaults)
        return f(*args, **kwds)
    resetter.__name__ = f.__name__
    return resetter

Просто примените этот декоратор к функции, чтобы получить ожидаемые результаты:
@resetDefaults # так мы применяем декоратор
def function(item, stuff = []):
    stuff.append(item)
    print stuff

function(1)
# выводит '[1]'

function(2)
# выводит '[2]', как и ожидалось

5.2. Переменное число аргументов

Python позволяет использовать произвольное число аргументов в функциях. Сначала определяются обязательные аргументы (если они есть), затем нужно указать переменную со звездочкой. Python присвоит ей значение списка остальных (не именованных) аргументов:
def do_something(a, b, c, *args):
    print a, b, c, args

do_something(1,2,3,4,5,6,7,8,9)
# выводит '1, 2, 3, (4, 5, 6, 7, 8, 9)'

Зачем это нужно? Например, функция должна принимать несколько элементов и делать с ними одно и то же (например, складывать). Можно заставить пользователя передавать функции список: sum_all([1,2,3]). А можно позволить передавать произвольное число аргументов, тогда получится более чистый код: sum_all(1,2,3).

Функция также может иметь переменное число именованных аргументов. После определения всех остальных аргументов укажите переменную с "**" в начале. Python присвоит этой переменной словарь полученных именованных аргументов, кроме обязательных:
def do_something_else(a, b, c, *args, **kwargs):
    print a, b, c, args, kwargs

do_something_else(1,2,3,4,5,6,7,8,9, timeout=1.5)
# выводит '1, 2, 3, (4, 5, 6, 7, 8, 9), {"timeout": 1.5}'

Зачем так делать? Я считаю, самая распространенная причина — функция является оберткой другой функции (или функций), и неиспользуемые именованные аргументы могут быть переданы другой функции (см. п. 5.3).

5.2.1. Уточнение
Использование именованных аргументов и произвольного числа обычных аргументов после них, по-видимому, невозможно, потому что именованные аргументы должны быть определены до "*"-параметра. Например, представим функцию:
def do_something(a, b, c, actually_print = True, *args):
    if actually_print:
        print a, b, c, args

У нас проблема: не получится передать actually_print как именованный аргумент, если при этом нужно передать несколько неименованных. Оба следующих варианта вызовут ошибку:
do_something(1, 2, 3, 4, 5, actually_print = True)
# actually_print сначала приравнивается к 4 (понятно, почему?), а затем 
# переопределяется, вызывая TypeError ('got multiple values for keyword argument')

do_something(1, 2, 3, actually_print = True, 4, 5, 6)
# Именованные аргументы не могут предшествовать обычным. Происходит SyntaxError.

Единственный способ задать actually_print в этой ситуации — передать его как обычный аргумент:

do_something(1, 2, 3, True, 4, 5, 6)
# результат: '1, 2, 3, (4, 5, 6)'

Единственный способ задать actually_print в этой ситуации — передать его как обычный аргумент:
do_something(1, 2, 3, True, 4, 5, 6)
# результат: '1, 2, 3, (4, 5, 6)'

5.3. Передача списка или словаря в качестве нескольких аргументов

Поскольку можно получить переданные аргументы в виде списка или словаря, нет ничего удивительного в том, что передавать аргументы функции тоже можно из списка или словаря. Синтаксис совершенно такой же, как в предыдущем пункте, нужно поставить перед списком звездочку:
args = [5,2]
pow(*args)
# возвращает pow(5,2), т. е. 25

А для словаря (что используется чаще) нужно поставить две звездочки:
def do_something(actually_do_something=True, print_a_bunch_of_numbers=False):
    if actually_do_something:
        print 'Something has been done'
        #
        if print_a_bunch_of_numbers:
            print range(10)

kwargs = {'actually_do_something': True, 'print_a_bunch_of_numbers': True}
do_something(**kwargs)

# печатает 'Something has been done', затем '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'

Историческая справка: в Python до версии 2.3 для этих целей использовалась встроенная функция apply (function, arg_list, keyword_arg_dict)'.

Статья целиком в PDF
Tags:
Hubs:
+59
Comments 48
Comments Comments 48

Articles