В одном из уроков курса "Введение в Python" уже рассматривались основы работы с модулями. Было объяснено, как импортировать модули, а также создавать свои собственные. В этом материале акцентировалось внимание на использовании функций, которые удобно импортировать в различные программы. Однако стоит отметить, что модули включают в себя в основном классы, а не только функции.

Импорт модулей

В этом уроке рассмотрим, как объединить несколько модулей в пакет, и как модули могут быть использованы как полноценные программы.

Пакеты модулей

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

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

Назовем наш каталог-пакет geometry и создадим два модуля — planimetry.py и stereometry.py. Поместим пакет в один из каталогов, перечисленных в списке sys.path. Первая запись этого списка — домашний каталог, обозначенный пустой строкой. Таким образом, проще всего разместить пакет в том же каталоге, что и основной скрипт.

В случае, если скрипт не требуется, и есть необходимость проверить пакет в интерактивном режиме, в Linux удобнее разместить его в домашнем каталоге.

Содержимое файла planimetry.py:

from math import pi, pow

class Rectangle:
    def __init__(self, a, b):
        self.width = a
        self.height = b
    def square(self):
        return round(self.width * self.height, 2)
    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle:
    def __init__(self, radius):
        self.r = radius
    def square(self):
        return round(pi * pow(self.r, 2), 2)
    def length(self):
        return round(2 * pi * self.r)

Код файла stereometry.py:

from math import pi, pow

class Cuboid:
    def __init__(self, a, b, c):
        self.length = a
        self.width = b
        self.height = c
        self.__squareSurface = 2 * (a*b + a*c + b*c)
        self.__volume = a * b * c
    def S(self):
        return round(self.__squareSurface, 2)
    def V(self):
        return round(self.__volume, 2)

class Ball:
    def __init__(self, radius):
        self.r = radius
    def S(self):
        s = 4 * pi * pow(self.r, 2)
        return round(s, 2)
    def V(self):
        v = (4 / 3) * pi * pow(self.r, 3)
        return round(v, 2)

Обязательно разместите в каталоге пакета файл __init__.py, даже если он будет пустым. Это помогает интерпретатору понять, что вы работаете с пакетом, а не с обычным каталогом. В файл __init__.py можно записать список модулей и переменные, которые будут импортироваться при использовании команды from имя_пакета import *, а также любой поддерживающий код, например, подключение к базе данных.

Попробуем импортировать модули из нашего пакета:

>>> import geometry.planimetry, geometry.stereometry
>>> a = geometry.planimetry.Rectangle(3, 4)
>>> b = geometry.stereometry.Ball(5)
>>> a.square()
12
>>> b.V()
523.6

Импортируя только пакет без явного указания модулей, мы не сможем к ним обратиться:

pl@pl-desk:~$ python3
Python 3.5.2 (default, Nov 23 2017, 16:37:01)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import geometry
>>> b = geometry.stereometry.Ball(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'geometry' has no attribute 'stereometry'

Так в чем же тогда преимущество использования пакетов, если модули все равно приходится импортировать по отдельности? Основная задача пакетов — организовать структуры имен. Когда имеем несколько пакетов с одноименными модулями или классами, точечная нотация позволяет использовать одинаковые сущности из разных пакетов, как, например, a.samename и b.samename. Точечная нотация также может давать представление о сути объекта, например, выражения geometry.planimetry.House() или geometry.stereometry.House() показывают, что в первом случае создается плоский объект-дом, а во втором — объемный.

Файл __init__.py позволяет указать, какие модули будут импортированы с помощью from имя_пакет import *:

__all__ = ['planimetry', 'stereometry']

После этого модули можно импортировать следующей командой:

>>> from geometry import *
>>> b = stereometry.Ball(5)
>>> a = planimetry.Circle(5)

Выполнение модуля как скрипта

В языке Python файлы-скрипты и файлы-модули практически ничем не отличаются. В языке нет специальных команд, которые бы обозначали, файл является модулем или скриптом. Разница зачастую только в том, что модули обычно не включают код, который выполняется в основной ветке программы. В модулях обычно определяются классы и функции.

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

class A:
    def __str__(self):
        return "A"

if __name__ == "__main__":
    print(A())

Код внутри if выполнится, только если файл запущен как скрипт:

pl@pl-desk:~$ python3 test.py
A

У каждого файла в Python изначально есть встроенный атрибут __name__, который при импорте файла равен имени модуля:

>>> import math
>>> math.__name__
'math'
>>> planimetry.__name__
'geometry.planimetry'

Однако, когда файл запускается как скрипт, значение __name__ становится равным "__main__". Это можно проверить, добавив в код print(__name__) и запустив файл как скрипт.

Таким образом, проверка на __name__ равное "__main__" позволяет размещать в модуле код, который будет выполнен только если файл запускается напрямую. Обычно это используется для тестирования модуля в процессе разработки, а в готовых модулях — для примера использования.

Практическая работа

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

Вопросы для самопроверки:

  1. Какое значение имеет '__name__', когда файл запускается как скрипт?
  2. Что следует обязательно разместить в каталоге пакета, чтобы интерпретатор понимал, что это именно пакет?
  3. Каковы основные преимущества использования пакетов в Python?
  4. Каким образом можно указать, какие модули импортировать с помощью команды from имя_пакета import *?