реклама
Бургер менюБургер меню

Кеннет Рейтц – Автостопом по Python (страница 15)

18

····cache[args] = result

····return result

Еще один распространенный источник путаницы — способ связывания переменных в замыканиях (или в окружающей глобальной области видимости).

Что вы написали:

def create_multipliers():

····return [lambda x: i * x for i in range(5)]

Чего вы ожидаете:

for multiplier in create_multipliers():

····print(multiplier(2), end="… ")

print()

Список, содержащий пять функций, каждая из них имеет собственную замкнутую переменную i, которая умножается на их аргумент, что приводит к получению следующего результата:

0… 2… 4… 6… 8…

Что происходит на самом деле:

8… 8… 8… 8… 8…

Создаются пять функций, все они умножают х на 4. Почему? В Python замыкания имеют позднее связывание. Это говорит о том, что значения переменных, использованных в замыканиях, определяются в момент вызова внутренней функции.

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

Особенно неудобно то, что вам может показаться, будто ошибка как-то связана с лямбда-выражениями (https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions). Функции, создаваемые с помощью лямбда-выражений, не отличаются от других. Фактически то же самое поведение проявляется и при использовании самого обычного def:

def create_multipliers():

····multipliers = []

····for i in range(5):

········def multiplier(x):

············return i * x

········multipliers.append(multiplier)

····return multipliers

Что вам нужно сделать вместо этого? Наиболее общее решение, возможно, станет «костылем» — временным вариантом устранения проблемы. Из-за уже упомянутого поведения Python, связанного с определением аргументов по умолчанию для функций (см. предыдущий пункт «Изменяемые аргументы функций»), вы можете создать замыкание, которое немедленно связывается со своими аргументами с помощью аргумента по умолчанию:

def create_multipliers():

····return [lambda x, i=i: i * x for i in range(5)]

Помимо этого вы можете использовать функцию functools.partial():

from functools import partial

from operator import mul

def create_multipliers():

····return [partial(mul, i) for i in range(5)]

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

Структурируем проект

Под структурированием мы понимаем решения, которые вы принимаете по поводу функционирования вашего проекта. Его цель состоит в использовании возможностей Python для создания чистого и эффективного кода. На практике это означает, что логика и зависимости в коде и структуре файлов и каталогов прозрачны.

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

В книге Python Cookbook есть глава, посвященная модулям и пакетам (http://bit.ly/python-cookbook-ch10), в которой подробно описывается, как работают выражения __import__ и упаковка. Цель этого раздела — осветить основные аспекты системы модулей и импортирования Python, необходимые для структурирования ваших проектов. Далее мы рассмотрим разные подходы к сборке кода, который легко будет расширять и тестировать.

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

Модули

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

Например, если один уровень проекта предназначен для взаимодействия с пользователем, а другой обрабатывает данные на низком уровне, наиболее логичным способом разделения этих двух слоев является размещение всей функциональности, связанной со взаимодействием, в одном файле, а всех низкоуровневых операций — в другом. Такая группировка разметит их в два разных модуля. Файл для взаимодействия затем импортирует файл для низкоуровневой обработки с помощью выражения import module или from module import attribute.

Как только вы пустите в ход выражение import, вы начнете пользоваться модулями. Модули могут быть либо встроенными (вроде os и sys), либо сторонними пакетами, установленными в среде (вроде Requests или NumPy), либо внутренними модулями проекта.

Далее показан пример некоторых выражений import (подтверждается, что импортированный модуль является объектом Python со своим типом данных):

>>> import sys # built-in module

>>> import matplotlib.pyplot as plt # сторонний модуль

>>>

>>> import mymodule as mod # внутренний модуль проекта

>>>

>>> print(type(sys), type(plt), type(mod))

<class 'module'> <class 'module'> <class 'module'>

В соответствии с руководством по стилю кода (https://www.python.org/dev/peps/pep-0008/) присваивайте модулям короткие имена, которые начинаются со строчной буквы. И убедитесь, что не использовали специальные символы вроде точки (.) или вопросительного знака (?), поскольку это может нарушить вид Python для модулей. Поэтому вам следует избегать имен файла вроде my.spam.py[42] (Python попытается найти файл spam.py в каталоге с именем my, а это неверно). В документации Python (http://docs.python.org/tutorial/modules.html#packages) более подробно описывается нотация с точкой.

Импортирование модулей. Помимо следования некоторым ограничениям в именовании, для использования файла Python в качестве модуля не требуется больше ничего особенного. Однако понимать механизм импортирования будет нелишним. Во-первых, выражение import modu начнет искать определение modu в файле с именем modu.py в том же каталоге, где находится и вызывающая сторона, если такой файл существует. При неудаче интерпретатор Python будет рекурсивно искать файл modu.py в пути поиска Python (https://docs.python.org/2/library/sys.html#sys.path) и сгенерирует исключение ImportError, если не найдет. Путь поиска зависит от платформы и включает в себя определенные пользователем или системой каталоги, указанные в переменной среды $PYTHONPATH (или %PYTHONPATH% в Windows). Ее можно просмотреть или изменить в сессии Python:

import sys

>>> sys.path

['', '/current/absolute/path', 'etc']

# Реальный список содержит каждый путь, где выполняется поиск,

# когда вы импортируете библиотеки в Python в том порядке,

# в котором они проверяются.

Как только файл modu.py будет найден, интерпретатор Python запустит модуль в ограниченной области видимости. Любое выражение верхнего уровня в файле modu.py будет выполнено, включая другие выражения импорта, если таковые существуют. Определения функций и классов хранятся в словаре модуля. Наконец, переменные функции и классы модуля будут доступны вызывающей стороне с помощью пространства имен модуля — основной концепции программирования, которая особенно эффективна в Python. Пространства имен предоставляют область видимости, содержащую именованные атрибуты, которые видны друг другу, но к ним нельзя получить доступ из-за пределов пространства имен.

Во многих языках директива заставляет препроцессор, по сути, скопировать содержимое включаемого файла в код вызывающей стороны. В Python все происходит иначе: включаемый код изолируется в пространстве имен модуля. Результатом выполнения выражения import modu станет объект модуля с именем modu, который будет находиться в глобальном пространстве имен, его атрибуты будут доступны с помощью точечной нотации. Например modu.sqrt — это объект sqrt, определенный внутри файла modu.py. Это означает, что вам, как правило, не нужно волноваться о том, что включаемый код может делать что-то нежелательное, к примеру переопределять существующую функцию с тем же именем.

Функции dir(), globals() и locals() помогают быстро исследовать пространства имен:

• dir(object) возвращает список атрибутов, к которым объект может получить доступ;

• globals() возвращает словарь атрибутов, находящихся в данный момент в глобальном пространстве имен, а также их значения;

• locals() возвращает словарь атрибутов в текущем локальном пространстве имен (например, внутри функции), а также их значения.

Для получения более подробной информации обратитесь к разделу Data model официальной документации Python (https://docs.python.org/3/reference/datamodel.html).