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

http://www.siafoo.net/article/52
  • Перевод
В этой части статьи рассматриваются уловки для выбора одного из двух значений на основе логического условия, передача и получение произвольного числа аргументов функций, а также распространенный источник ошибок — тот факт, что дефолтные значения аргументов функции вычисляются только один раз.

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
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 47
  • +2
    Спасибо
    • +3
      5.2.1. Уточнение
      Использование именованных аргументов и произвольного числа обычных аргументов одновременно, по-видимому, невозможно, потому что именованные аргументы должны быть определены до "*"-параметра. Например, представим функцию:

      А как же стандартное
      def x(a,b, *args, **kwargs):
          pass
      x(1,2,'a','b',named='123',named2='456')

      Полагаю, правильно было бы написать что-то вроде «использование неименованных аргументов _после_ именованных»
      • –3
        Мало каметариев… Судя по всему многие мало что поняли из написанного.

        Я лично выкрутил мозг на этом абзаце

        «4.2. Уловка and/or

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

          'habr' — True
          1337, -1, 5 — все True
          [(a,b),(c,d)] — все True

          'habr' and 5 == 5 (последнее из выражений, если все они true)
          'habr' and [] == [] (первое false-выражение)
          0 and '' == 0 (первое false-выражение)

          0 or '' == ''
          'habr' or 0 == 'habr'
          [] or 1 == 1
          • 0
            Да, 4.2 напрягает мозг и заставляет лезть в документацию :-)

            result = test and 'Test is True' or 'Test is False'

            Сначала нужно определиться с приоритетом логических операторов (http://docs.python.org/library/stdtypes.html#boolean-operations-and-or-not). В порядке убывания он такой — not, and, or. Значит выражение можно записать так:

            result = (test and 'Test is True') or 'Test is False'

            Теперь нужно понять, как интерпретируются значения операндов в контексте логических операторов (http://docs.python.org/reference/expressions.html#boolean-operations). Из первого абзаца становится ясно, что False, None, 0, 0.0, '', (), [], {}, set() интерпретируется как ложь, все остальное как истина. Значит, строки 'Test is True' и 'Test is False' интерпретируются как истина.

            test = True

            Читаем 3 абзац в последней ссылке и понимаем, что в случае выражения test and 'Test is True', если оба аргумента интерпретируются как истина, то оператор вернет значение второго аргумента, то есть строку 'Test is True'.

            Из четвертого абзаца в той же ссылке понимаем, что 'Test is True' or 'Test is False' вернет значение первого аргумента, т.е. 'Test is True'.

            test = False

            test and 'Test is True' вернет False без заморочек, а False or 'Test is False' конечно второй аргумент, 'Test is False'

            Да, про то, что логические операторы возвращают не True и False, а значения самих операндов, написано в последнем абзаце в той же ссылке.
          • 0
            По поводу пункта 4.2 можно было бы явно указать, что логические операторы возвращают объект, а не логическую величину:

            print (True or object()).__class__
            print (False or object()).__class__
            • НЛО прилетело и опубликовало эту надпись здесь
              • +5
                Прямо-таки пересказ? Riateche добросовестно (и при том достаточно качественно) переводит весьма интересную статью. Если же Вы намекаете на то, что все эти tips-ы так или иначе описаны в документации к Питону, то Вы совершенно правы.
                Но согласитесь: выуживать эти хитрости по одной оттуда — дело не самое приятное. А по сравнению с возможностями многих других языков большую часть описанного кроме как хаками и трюками никак и не назовешь.

                Вобщем так держать и большое спасибо за перевод… и еще большее — за линк на оригинал =)
                • НЛО прилетело и опубликовало эту надпись здесь
                  • 0
                    Может быть, в этом фрагменте статьи хаков мало, но в первых двух частях они точно были.

                    Вещи действительно несложные, но они затрагиваются в приличных книжках, поэтому мне о них писать тоже можно.
                    • НЛО прилетело и опубликовало эту надпись здесь
                      • 0
                        Мне кажется, что писать с использованием недокументированных возможностей языка можно только при крайней необходимости. Для Python я это смутно себе представляю, язык довольно гибкий.
              • +1
                В 5.2 второй кусок кода == первому, а должен быть явно другим.
                • 0
                  А за статью спасибо! Жду продолжения.
                  • 0
                    Верно, исправил.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • 0
                      > что дефолтные значения аргументов функции выполняются только один раз

                      коряво сказано, имхо. значения выполняются или определения?
                    • +3
                      def func(item, stuff=()):
                      … stuff = list(stuff)
                      … stuff.append(item)
                      … print stuff

                      а так не будет удобней? чтоб не использовать иф?
                      • +1
                        О, круто. Не приходило такое в голову :)
                        Даже так будет корректно работать:
                            def func(item, stuff=()):
                                stuff = dict(stuff)
                        
                        • 0
                          просто часто вижу этот пример со словарем и None и мне так не нравится этот пример
                          • 0
                            Ну, фиг знает. Я хорошо помню, что кортежный кортеж диктится. Оно же часто используется для «генератора словарей» :)
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • 0
                                Кортежный кортеж это {}.items(). Список кортежей :)
                      • 0
                        Респект за перевод.

                        А сама глава надуманная какая-то. Вряд ли стоит использовать неочевидности из 4.2 и 4.3. Во всяком случае, я их успешно забыл и не жалею.

                        Тоже самое касается @resetDefaults. Одноразовое вычисление значений всё равно нужно держать в голове, чтобы этот декоратор не удивлял.

                        Вообще от статьи ощущение, что автор не долго на питоне кодит. Не юзает свои же советы:
                            if stuff is None:
                                stuff = []
                        

                        Равно: stuff = [] if stuff is None else stuf

                        По кваргсам примеры надуманные. Есть куча практических примеров, типа:
                        def __init__(self, **kw):
                            (setattr(self, k, v) for k, v in kw.iteritems())
                        
                        • 0
                          Ой, конечно так: [setattr(self, k, v) for k, v in kw.iteritems()]
                          • +1
                            Хехе, ничо что я тут онанирую?
                                def __init__(self, **kw):
                                    self.__dict__.update(kw)
                            
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • 0
                                Ну, это может быть самостоятельным классом для превращения дикта в объект :)
                          • 0
                            Равно: stuff = [] if stuff is None else stuf

                            Или даже
                            stuff = stuff or []
                            Вообще мне кажется, что знание, например, Си, делает множество высокоуровневых языков практически очевидными.
                            • 0
                              Ага, если нам не жалко потерять '', {} и т.п.
                              • 0
                                Не жалко, потому что у нас должен быть либо массив, либо None. Ничего другого быть не должно по условию.
                            • 0
                              Имхо, автор не юзает свои же советы, потому что хочет сделать примеры максимально понятными.
                              • 0
                                Ну вот о том и речь, что его советы делают код непонятным :)
                            • 0
                              Мало и менее полезно, чем предыдущие части.
                              Так же как-то совсем скомкана часть про передачу списка или словаря в качестве переменных — всего один пример и тот не показательный. В документации как-то попонятнее.

                              Но — спасибо за сам цикл статей!
                              • +2
                                Если уж цикл дошёл до тернарного оператора, тогда вы забыли про switch case.
                                Как-то так:
                                case = {
                                   'case1': case1_result,
                                   'case2': case2_result
                                   }[case_value]
                                
                                • НЛО прилетело и опубликовало эту надпись здесь
                                  • +1
                                    >>> func = lambda x: print(x)
                                    >>> {'case1': lambda: func('fail1'),
                                    ...  'case2': lambda: func('fail2')}['case2']()
                                    fail2
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                      • 0
                                        print(x) печатает x
                                        просто def func… заменил на лямбду для краткости

                                        еще вы кажется не понимаете как работают списки — значения в них вычисляются перед использованием и даже простое объявление {«case1»: func(«fail1»), «case2»: func(«fail2»)} выведет fail1 и fail2

                                        по этому нужно поместить в списки функцию, а потом ее вызывать, что я и сделал через лямбды
                                        • 0
                                          Верно, но это уже слишком громоздко.
                                          • 0
                                            в данном случае да, для: {'case1': func1, 'case2': funct2}['case1'](params) удобнее, но не так часто используется
                                            и короче чем if:… elif:… elif:… за счет отсутствия переноса строк в любом случае, даже, несколько более читабельнее по-моему
                                          • НЛО прилетело и опубликовало эту надпись здесь
                                            • 0
                                              в примере siasia все будет хорошо, и если case1_result будут фунциями и если переменными, но замечание полезное :)
                                  • 0
                                    Чтоб не путаться в if… else и в or… and и не «испытывать смешанных чувств» лучше всё-таки не полениться и расставить скобочки для ясности. Чтоб ещё и не мучаться, вспоминая приоритеты операторов.
                                    • +1
                                      да с [] в парамерах по умалчанию унесло не один час моего времени ))))
                                      • 0
                                        Да, ещё одно: не многие знают, что в слайсы можно передавать 3 значения, например list(range(10))[::2] в python3 или range(10)[::2] в 2.5 выведут все парные числа до 10ти. range(10)[1::2] — непарные

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