Продолжение статьи об идеоматичных конструкциях в Python.

На самом деле, многое из описанного ниже есть в любом более-менее толковом учебнике, но только разбавленное n-ным количеством воды. Я же постараюсь дать только самое основное: примеры и, самое главное, краткое заключение. Мне эта идея кажется удачной, т.к примеры должны лучше объяснить и доказать каждую мысль, но не у всех хватает терпения и желания в этом разбираться - можно сразу ознакомится с заключительной частью раздела как с теоремой, не вникая в доказательства:)

Параметры по-умолчанию в функции

Python, как и большая часть современных языков, позволяет указывать в функциях параметры по-умолчанию. Делается это так:

def foo(x='hello'):
    pass

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

>>> def foo(x, y=[]):
...    y.append(x)
...    return y

>>> foo(1)
[1]
>>> foo(1)
[1, 1]
>>> foo(1)
[1, 1, 1]

Несколько неожиданное поведение. Список начал увеличиваться от вызова к вызову по одной простой причине: значения по-умолчанию вычисляются один раз в момент объявления функции и ссылка на него сохраняется в атрибуте func_defaults в Python2. и в атрибуте __defaults__ в Python3..

Избавится от эффекта из примера выше можно, хоть и не очень элегантно:

>>> def foo(x, y=None):
...    if y is None:
...        y = []
...    y.append(x)
...    return y
>>> foo(1)
[1]
>>> foo(1)
[1]
>>> foo(1)
[1]

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

>>> a = 1
>>> def foo(x=a):
...     return x
...
>>> foo()
1
>>> a = 2
>>> foo()
1

Как видно, x ассоциируется с a только при инициализации, в дальнейшем изменения а на x не влияют - все именно так, как было описано выше. Еще интереснее такая ситуация:

>>> a = []
>>> def foo(x=a):
...     return x
...
>>> foo()
[]
>>> a.append(1)
>>> foo()
[1]

Есть ли противоречие с предыдущим примером? Нет, т.к list - тип изменяемый и мы имеем дело с одной и той же ссылкой. Если подробно: в качестве значения по-умолчанию для x записалась ссылка на a, затем к a добавили 1 - значение по-умолчанию тоже изменилось, т.к дефолт x и a ссылаются на один и то же объект.

>>> a = [1, 2, 3]
>>> foo()
[1]

Что здесь произошло? Мы указали a ссылаться на другой объект [1, 2, 3], в то время как дефолтное значение x по-прежнему ссылается на старое значение a.

Если кратко: значения по-умолчанию вычисляются один раз в момент объявления функции и ссылка на него сохраняется в атрибуте функции. При объявлении в качестве значения по-умолчанию изменяемого типа данных или использовании значения по-умолчанию в замыкании нужно быть готовым к спецэффектам.

Именованные логеры

Очень часто в туториалах о логировании для новичков можно встретить такое:

import logging
logging.info("Hello word!")

Так делать нельзя. Особенно в библиотеках. Нельзя потому, что таким образом все сообщения начинают падать в рутовый логер, что весьма затрудняет настройку логирования в дальнейшем. Временами возникает что-то изменить в логировании только для одной части системы (например, включить DEBUG уровень для админки, поскольку в ней нашли баг, или изменить срок ротации логирования подключения к БД из-за нехватки места на дисках). При использовании рутового логера все это делается исключительно костылями. Есть ли выход? Конечно есть - нужно использовать именованные логеры:

import logging
logger = logging.getLogger("name_of_logger")
logger.info("Hello word!")

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

Отдельного разговора заслуживает именование логеров. Я встречал два варианта:

  • Через имя модуля logging.getLogger(__name__).
  • По желанию разработчика logging.getLogger("name_of_logger").

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

Если кратко: логировать следует только через именованные логеры, которые лучше всего инициализировать через logging.getLogger(__name__)

try и менеджеры контекста

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

import json

def get_config(path):
    try:
        f = open(path)
        return json.load(f)
    except ValueError:
        print('Invalid config')
    finally:
        f.close()

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

import threading

lock = threading.Lock()

<код>
try:
    lock.acquire()
    <еще код>
finally:
    lock.release()

В случае возникновения исключения при захвате блокировки в Python2.x из-за отсутствия цепочек исключений даже не получится понять, что именно произошло.

Как же правильно захватывать ресурсы? Есть два способа - не очень правильный и правильный:

  1. Независимо обрабатывать захват ресурса.
  2. Использовать менеджеры контекста и оператор with

В первом (не очень правильном) случае:

import json

def get_config(path):
    try:
        f = open(path)
        try:
            return json.load(f)
        except ValueError:
            print('Invalid config')
        finally:
            f.close()
    except IOError:
        print('Invalid config path')

Во втором (правильном):

import json

def get_config(path):
    try:
        with open(path) as f:
            return json.load(f)
    except IOError:
        print('Invalid config path')
    except ValueError:
        print('Invalid config')

При работе через with и с менеджерами контекста мы можем явно в одном месте определить, как должен происходить для каждого объекта экземпляра захват (метод enter) и освобождение (метод exit) во избежание дублирования кода в блоках finaly. Интерфейс менеджера контекста поддерживают многие объекты из стандартной библиотеки, а те, которые не поддерживают, можно обернуть с помощью средств из contextlib.

Если кратко: захват ресурсов должен быть выполнен до блока try. Управлять захватом и освобождением ресурсов нужно с помощью оператора with и менеджеров контекста.


Comments

comments powered by Disqus