Слёрм Universe - Python на примере Minecraft
Модули и их импорт
Лекция 8.1
Помните, не так давно, а именно на лекции по функциям, мы узнали что-такое модули в Python. Итак, любой файл в Python, куда мы записываем наш код, называется модулем. А ещё мы должны знать, что программы на Python, как правило состоят больше, чем из одного файла. Давайте рассмотрим пример ниже:
Представим, что у нас имеется:
  • Файл (модуль) functions.py, который содержит функции программы. В нашем случае, в качестве примера, в нём будет содержаться всего одна функция, которую мы с вами написали на одной из прошлых практик - build_arena(). Данная функция умеет строить арену по переданным в неё аргументам: координаты строительства, габариты арены, ID-блока, из которого она будет возводиться, и аргумент, определяющий будет ли арена заполнена блоками или же пустой. Вот код этого модуля:
from mcpi.minecraft import Minecraft
mc = Minecraft.create()


def build_arena(coords, dimensions, block_id=1, fullness=True):
    x, y, z = coords
    length_size, height_size, width_size = dimensions
    for length_bias in range(length_size):
        for height_bias in range(height_size):
            for width_bias in range(width_size):
                if fullness:
                    mc.setBlock(x + length_bias, y + height_bias, z + width_bias, block_id)
                else:
                    if height_bias == 0 or height_bias == height_size - 1 or width_bias == 0 or width_bias == width_size - 1:
                        mc.setBlock(x + length_bias, y + height_bias, z + width_bias, block_id)
  • Файл (модуль) settings.py, который содержит настройки нашей программы. В нём будут содержаться переменные с габаритами возводимой арены, с ID блока, из которого она будет построена, и переменная со значением логического типа - True, если арена должна быть заполненной блоками, и False, если должна быть пустой:
dimensions = (10, 10, 10)  # Длина, высота и ширина арены
block_id = 41              # ID блока, из которого будем строить арену
fullness = False          # Будет ли наша арена заполнена блоками внутри, либо же будет пустой
  • Ну и наконец файл (модуль) с самой программой - program.py. Данный модуль будет содержать в себе простой и знакомый нам код самой программы. Например, ЕСЛИ под ногами нашего игрока расположен блок с золотом, то пусть построится арена ПРИ ПОМОЩИ функции, записанной в файл functions.py и с настройками, записанными в файл settings.py:
from mcpi.minecraft import Minecraft
import functions
import settings
mc = Minecraft.create()

while True:
    player_x, player_y, player_z = mc.player.getPos()
    if mc.getBlock(player_x, player_y - 1, player_z) == 41:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        mc.postToChat("Строим арену!")
        functions.build_arena((player_x + 10, player_y - 1, player_z), settings.dimensions, settings.block_id,
                              settings.fullness)
Необычно, правда? Ведь мы с вами привыкли записывать код нашей программы в один файл: туда мы помещали: и функции, которые мы предварительно написали своими руками, и переменные с параметрами строительства, и сам код с циклами и условиями. Это логично, ведь наши программы были небольшими и выполняли маленький функционал. Но представьте, что мы решили написать большую и сложную программу, которая выполняет множество действий. В жизни, как правило, такие программы и создаются. А теперь представьте код этой программы, в которой содержится множество функций, условий и параметров. Если записать такую программу в один файл, то разобраться в её коде будет очень тяжёлой задачей, но а если её разбить на множество файлов, в каждом из которых будет записан свой функционал, тогда станет значительно проще.

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

Давайте подробнее взглянем на код нашей программы в файле program.py. Пока пропустим первую строчку кода, связанную с библиотекой mcpi, и рассмотрим что мы импортируем дальше:
  • В самом начале при помощи строчки "import functions" мы импортируем, или, говоря простым языком, загружаем в нашу программу данные модуля functions.py. Обратите внимание, что при импорте мы не указываем расширение модуля, однако, сам модуль ОБЯЗАТЕЛЬНО должен иметь расширение .py, как в нашем примере;
  • После при помощи строчки "import settings" мы импортируем данные из модуля settings.py;
  • Теперь наша программа может получать все функции, записанные в модуль functions и параметры, которые указаны в модуле settings. В принципе, так и происходит:
    • В коде программы, после того, как наш игрок окажется на блоке золота, мы используем нашу импортированную функцию build_arena(). Обратите внимание, что вызов этой функции происходит интересным способом. Сначала мы указываем ИМЯ ИМПОРТИРОВАННОГО МОДУЛЯ functions, а после ЧЕРЕЗ ТОЧКУ мы записываем ИМЯ ФУНКЦИИ bulid_arena. Получилось functions.build_arena();
    • В аргументах импортированной функции build_arena() мы указываем кортеж с координатами, где хотим возвести арену, а после - импортированные из модуля settings переменные: dimensions с длиной, высотой и шириной возводимой арены, block_id с ID блока, из которого она будет построена, и переменная fullness, в которой содержится True или False, в зависимости от того, какой будет арена - заполненной блоками или пустой. Эти переменные мы также записываем через точку после названия модуля settings.

Если мы выполним наш код, то впереди от игрока построится пустая по середине арена и длиной, высотой, шириной в 10 золотых блоков. Всё, как указано в модуле с настройками settings.

В этом примере мы изучили, как выполнять импорт ВСЕГО модуля со ВСЕМИ данные, которые содержатся в нём. А теперь представьте, что в файле functions.py по-мимо нашей функции build_arena() содержатся десятки других функций. Однако, нам нужна исключительно функция build_arena(). Конечно, мы можем выполнить импорт всего модуля со всеми функциями, которые содержатся в нём. Однако, в нашем случае удобнее сделать импорт именно отдельной функции build_arena(). Давайте чуточку видоизменим код в нашем модуле program:
from mcpi.minecraft import Minecraft
from functions import build_arena
import settings
mc = Minecraft.create()

while True:
    player_x, player_y, player_z = mc.player.getPos()
    if mc.getBlock(player_x, player_y - 1, player_z) == 41:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        mc.postToChat("Строим арену!")
        build_arena((player_x + 10, player_y - 1, player_z), settings.dimensions, settings.block_id,
                              settings.fullness)
  • Обратите внимание на вторую строчку кода. Теперь она звучит так: ИЗ (from) модуля functions ИМПОРТИРУЕМ (import) функцию build_arena(). До этого мы пробовали импортировать весь модуль functions, а теперь - только его отдельную функцию build_arena();
  • В прошлом примере, используя в коде нашей программы program функцию build_arena() из импортированного модуля functions, мы обращались к ней через точку после названия этого модуля - functions.build_arena(). Однако, теперь, когда мы импортировали модуль не целиком, а отдельную его функцию, мы обращаемся к ней напрямую - build_arena().
Как видите, всё очень просто!

А как нам быть, если мы вдруг захотим импортировать из файла functions.py вместо одной функции сразу две, или три? Всё очень просто! Мы всё так же можем импортировать модуль с функциями целиком и делать вызов необходимой функции через точку после имени модуля, либо импортировать функции напрямую, перечислив их через запятую:
from functions import build_arena, any_function2, any_function_3
Теперь давайте представим, что в коде нашей программы содержится переменная с названием, которая полностью совпадает с названием нашей функции - build_arena:
from mcpi.minecraft import Minecraft
from functions import build_arena
import settings
mc = Minecraft.create()

build_arena = "Game Arena"
while True:
    player_x, player_y, player_z = mc.player.getPos()
    if mc.getBlock(player_x, player_y - 1, player_z) == 41:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        mc.postToChat(f"Строим арену: {build_arena}!")
        build_arena((player_x + 10, player_y - 1, player_z), settings.dimensions, settings.block_id,
                              settings.fullness)
Как видите, теперь в коде модуля program содержится переменна build_arena со строкой "Game Arena" в качестве значения. А ещё при помощи оператора import мы произвели импортирование функции с точно таким же названием из модуля functions. Говоря простым языком, у нас произошёл конфликт имён. Для любого модуля в Python главный приоритет имеет имя, записанное в его пространство имён. То есть для модуля program в приоритете будет переменная build_arena со строкой "Game Arena", так как она записана непосредственно в нём. Импортированная же в него функция build_arena() теперь работать не будет, так как при вызове её имени, Python будет считать, что это та самая переменная со строкой "Game Arena". Учитывайте такое поведение Python и будьте максимально внимательны, когда пишите код! В идеале мы должны стараться не допускать таких сложностей. Однако, иногда действительно случается так, что при импорте, например, функции из какой-нибудь внешней библиотеки уже в существующий код, происходит конфликт их имён. Давайте немного видоизменим наш текущий код и посмотрим, что можно сделать в такой ситуации:
from mcpi.minecraft import Minecraft
from functions import build_arena as build_arena_function
import settings
mc = Minecraft.create()

build_arena = "Game Arena"
while True:
    player_x, player_y, player_z = mc.player.getPos()
    if mc.getBlock(player_x, player_y - 1, player_z) == 41:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        mc.postToChat(f"Строим арену: {build_arena}!")
        build_arena_function((player_x + 10, player_y - 1, player_z), settings.dimensions, settings.block_id,
                              settings.fullness)
  • Вновь обратите внимание на вторую строчку кода. После импорта наша функции build_arena() при помощи оператора import мы использовали ещё один оператор - as. Данный оператор нам позволил переименовать функцию в модуле, куда мы её импортировали. Или, говоря простым языком, мы импортировали ту же функцию build_arena(), но под новым названием - build_arena_function(). В пространстве имён модуля program такого имени не существует, поэтому конфликта имён не возникнет;
  • И теперь в теле условия IF мы производим вызов функции строительства арены под изменённым названием - build_arena_function().
Важный момент! При импорте с использованием оператора as мы не переименовываем саму функцию, он лишь позволяет записать её под другим названием в том модуле, куда мы её импортировали.

Отлично, теперь мы разобрались с тем, как работает импорт. Однако, в самом начале данной лекции я упоминал, что модули, которые мы импортируем, могут, в свою очередь, тоже импортировать другие модули. Обратите внимание на то, что в данном примере, а именно в модуле functions, который мы импортируем в нашу программу, происходит тоже импорт класса Minecraft. Мы с вами ещё не знаем, что такое классы. Пока просто примите это, как факт. Давайте разберём уже ставшую культовой строчку, которую мы привыкли записывать в самом начале нашего кода:
from mcpi.minecraft import Minecraft
При помощи неё из модуля minecraft мы импортируем класс с названием Minecraft. Но почему, при обращении к модулю minecraft, перед ним указано имя mcpi через точку? Давайте для ответа на этот вопрос разберём, что же такое mcpi? mcpi - это одна из множества библиотек, написанных для Python. Говоря простым языком, в программировании библиотека - это просто набор готовых модулей и функций, которые выполняют специфические задачи и упрощают программисту жизнь. Захотели мы при помощи Python управлять Minecraft? Пожалуйста, мы можем потратить огромное количество времени и написать свою программу для этого, либо же использовать готовую библиотеку, который написал другой программист. Получается, что mcpi - это просто название папки с библиотекой, то есть со множеством файлов одной общей программы, позволяющей нам работать с Minecraft. При импорте модуля, который находится в папке, мы сначала указываем названием этой папки, а после через точку названием самого модуля. Именно поэтому, обращаясь к модулю minecraft мы его вызываем через mcpi.minecraft.
Как видите, импорт модулей не является чем-то сложным - нам лишь достаточно было немного разобраться в этом. В следующей лекции мы узнаем, что такое стандартная библиотека Python и познакомимся с двумя её модулями - random и time.