Модули
Модули в Julia — это отдельные рабочие пространства переменных, то есть они вводят новую глобальную область видимости. Они ограничены синтаксически внутри module Name ... end. Модули позволяют создавать определения верхнего уровня (также известные как глобальные переменные) без необходимости беспокоиться о конфликтах имен, когда ваш код используется вместе с чьим-то другим. Внутри модуля вы можете управлять тем, какие имена из других модулей видимы (через импорт), и указывать, какие из ваших имен предназначены для публикации (через экспорт).
Следующий пример демонстрирует основные возможности модулей. Он не предназначен для выполнения, а показан для иллюстративных целей:
module MyModule
using Lib
using BigLib: thing1, thing2
import Base.show
export MyType, foo
struct MyType
x
end
bar(x) = 2x
foo(a::MyType) = bar(a.x) + 1
show(io::IO, a::MyType) = print(io, "MyType $(a.x)")
end
Обратите внимание, что стиль не предполагает отступы тела модуля, так как это обычно приводит к отступам всего файла.
Этот модуль определяет тип MyType, и две функции. Функция foo и тип MyType экспортированы, и поэтому будут доступны для импорта в другие модули. Функция bar является приватной для MyModule.
Выражение using Lib означает, что модуль под названием Lib будет доступен для разрешения имен по мере необходимости. Когда встречается глобальная переменная, у которой нет определения в текущем модуле, система будет искать ее среди переменных, экспортированных модулем Lib, и импортирует ее, если она будет найдена там. Это означает, что все использования этой глобальной переменной внутри текущего модуля будут ссылаться на определение этой переменной в Lib.
Выражение using BigLib: thing1, thing2 вносит в область видимости только идентификаторы thing1 и thing2 из модуля BigLib. Если эти имена относятся к функциям, добавление методов к ним не будет разрешено (вы можете только "использовать" их, а не расширять).
Ключевое слово import поддерживает тот же синтаксис, что и using. Оно не добавляет модули для поиска так, как это делает using. import также отличается от using тем, что функции, импортированные с использованием import могут быть расширены новыми методами.
В MyModule выше мы хотели добавить метод к стандартной функции show, поэтому мы должны были написать import Base.show. Функции, имена которых видны только через using расширять нельзя.
После того, как переменная становится видимой через using или import, модуль не может создать свою собственную переменную с тем же именем. Импортированные переменные являются только для чтения; присвоение глобальной переменной всегда влияет на переменную, принадлежащую текущему модулю, или же вызывает ошибку.
Краткое описание использования модулей
Для загрузки модуля можно использовать два основных ключевых слова: using и import. Чтобы понять их различия, рассмотрите следующий пример:
module MyModule export x, y x() = "x" y() = "y" p() = "p" end
В этом модуле мы экспортируем функции x и y (с ключевым словом export), а также имеем неэкспортированную функцию p. Существует несколько способов загрузить Модуль и его внутренние функции в текущее рабочее пространство:
| Команда импорта | Что попадает в область видимости | Доступно для расширения методов |
|---|---|---|
using MyModule |
Все экспортированные имена (x и y), MyModule.x, MyModule.y и MyModule.p
|
MyModule.x, MyModule.y и MyModule.p
|
using MyModule: x, p |
x и p
|
|
import MyModule |
MyModule.x, MyModule.y и MyModule.p
|
MyModule.x, MyModule.y и MyModule.p
|
import MyModule.x, MyModule.p |
x и p
|
x и p
|
import MyModule: x, p |
x и p
|
x и p
|
Модули и файлы
Файлы и имена файлов в основном не связаны с модулями; модули связаны только с выражениями модулей. Можно иметь несколько файлов на один модуль и несколько модулей в одном файле:
module Foo
include("file1.jl")
include("file2.jl")
end
Включение одного и того же кода в разные модули обеспечивает поведение, аналогичное миксинам. Это можно использовать для запуска одного и того же кода с разными базовыми определениями, например, для тестирования кода, запуская его с "безопасными" версиями некоторых операторов:
module Normal
include("mycode.jl")
end
module Testing
include("safe_operators.jl")
include("mycode.jl")
end
Стандартные модули
Существует три важных стандартных модуля:
-
Coreсодержит всю функциональность, "встроенную" в язык. -
Baseсодержит базовые функции, полезные практически во всех случаях. -
Main— это модуль верхнего уровня и текущий модуль при запуске Julia.
По умолчанию определения верхнего уровня и модули без параметров
В дополнение к using Base, модули также автоматически содержат определения функций eval и include, которые оценивают выражения/файлы в глобальной области видимости этого модуля.
Если эти определения по умолчанию не нужны, модули могут быть определены с использованием ключевого слова baremodule вместо (обратите внимание: Core по-прежнему импортируется, как указано выше). С точки зрения baremodule, стандартный module выглядит следующим образом:
baremodule Mod using Base eval(x) = Core.eval(Mod, x) include(p) = Base.include(Mod, p) ... end
Относительные и абсолютные пути к модулям
Учитывая выражение using Foo, система обращается к внутренней таблице модулей верхнего уровня, чтобы найти модуль с именем Foo. Если модуль не существует, система пытается require(:Foo), что обычно приводит к загрузке кода из установленного пакета.
Однако некоторые модули содержат подмодули, что означает, что иногда вам нужно получить доступ к подмодулю не верхнего уровня. Есть два способа сделать это. Первый — использовать абсолютный путь, например using Base.Sort. Второй — использовать относительный путь, что упрощает импорт подмодулей текущего модуля или любого из его окружающих модулей:
module Parent module Utils ... end using .Utils ... end
Здесь модуль Parent содержит подмодуль Utils, и код в Parent хочет, чтобы содержимое Utils было видно. Это делается путем добавления точки в начало пути using. Добавление дополнительных ведущих точек перемещает на дополнительные уровни в иерархии модулей. Например, using ..Utils будет искать Utils в окружающем модуле Parent, а не в самом Parent.
Обратите внимание, что квалификаторы относительного импорта действительны только в операторах using и import.
Разное по именованному пространству
Если имя квалифицировано (например, Base.sin), к нему можно получить доступ, даже если оно не экспортировано. Это часто полезно при отладке. К нему также можно добавить методы, используя квалифицированное имя как имя функции. Однако из-за синтаксических неоднозначностей, если вы хотите добавить методы к функции в другом модуле, имя которой содержит только символы, например, оператор, Base.+, вы должны использовать Base.:+ для ссылки на него. Если оператор состоит более чем из одного символа, вы должны заключить его в скобки, например: Base.:(==).
Имена макросов записываются с @ в операторах импорта и экспорта, например, import Mod.@mac. Макросы в других модулях можно вызывать как Mod.@mac или @Mod.mac.
Синтаксис M.x = y не работает для присвоения глобальной переменной в другом модуле; присвоение глобальной переменной всегда локально для модуля.
Имя переменной может быть "зарезервировано" без присвоения ему значения, объявив его как global x. Это предотвращает конфликты имен для глобальных переменных, инициализированных после загрузки.
Инициализация модуля и предварительная компиляция
Загрузка больших модулей может занять несколько секунд, поскольку выполнение всех операторов в модуле часто связано с компиляцией большого количества кода. Julia создает предварительно скомпилированные кэши модуля, чтобы уменьшить это время.
Инкрементальные предварительно скомпилированные файлы модуля создаются и используются автоматически при использовании import или using для загрузки модуля. Это приведет к его автоматической компиляции при первом импорте. В качестве альтернативы, вы можете вручную вызвать Base.compilecache(modulename). Результирующие файлы кэша будут храниться в DEPOT_PATH[1]/compiled/. Впоследствии модуль автоматически перекомпилируется при using или import всякий раз, когда меняются какие-либо из его зависимостей; зависимости — это модули, которые он импортирует, сборка Julia, файлы, которые он включает, или явные зависимости, объявленные с помощью include_dependency(path) в файлах модуля.
Для зависимостей файлов изменение определяется путем проверки, не изменилось ли время модификации (mtime) каждого загруженного файла include или явно добавленного include_dependency, или равно ли оно времени модификации, усеченному до ближайшей секунды (для учета систем, которые не могут копировать mtime с точностью до долей секунды). Также учитывается, соответствует ли путь к файлу, выбранный логикой поиска в require, пути, который создал файл предварительной компиляции. Также учитывается набор зависимостей, уже загруженных в текущий процесс, и эти модули не будут перекомпилированы, даже если их файлы изменяются или исчезают, чтобы избежать создания несовместимости между работающей системой и кэшем предварительной компиляции.
Если вы знаете, что модуль не безопасен для предварительной компиляции вашего модуля (например, по одной из причин, описанных ниже), вы должны поместить __precompile__(false) в файл модуля (обычно вверху). Это заставит Base.compilecache выдать ошибку и заставит using / import загрузить его непосредственно в текущий процесс и пропустить предварительную компиляцию и кэширование. Это также предотвращает импортирование модуля любым другим предварительно скомпилированным модулем.
Вам может потребоваться учитывать определенное поведение, присущее созданию инкрементных общих библиотек, что может потребовать осторожности при написании вашего модуля. Например, внешнее состояние не сохраняется. Чтобы учесть это, явно разделите любые шаги инициализации, которые должны выполняться во время выполнения, от шагов, которые могут выполняться во время компиляции. Для этой цели Julia позволяет определить функцию __init__() в вашем модуле, которая выполняет любые шаги инициализации, которые должны выполняться во время выполнения. Эта функция не будет вызываться во время компиляции (--output-*). По сути, вы можете предположить, что она будет запущена ровно один раз за время существования кода. Вы можете, конечно, вызвать её вручную, если необходимо, но по умолчанию предполагается, что эта функция обрабатывает вычисление состояния для локальной машины, которое не нужно – или даже не должно – записываться в скомпилированный образ. Она будет вызвана после загрузки модуля в процесс, включая случай загрузки в инкрементальную компиляцию (--output-incremental=yes), но не при загрузке в процесс полной компиляции.
В частности, если вы определите function __init__() в модуле, Julia вызовет __init__() немедленно после загрузки модуля (например, import, using, или require) во время выполнения в первый раз (т.е., __init__ вызывается только один раз и только после выполнения всех инструкций в модуле). Поскольку она вызывается после полного импорта модуля, все подмодули или другие импортированные модули вызовут свои функции __init__ до вызова __init__ модуля-контейнера.
Два типичных использования __init__ — это вызов функций инициализации во время выполнения внешних библиотек C и инициализация глобальных констант, которые включают указатели, возвращаемые внешними библиотеками. Например, предположим, что мы вызываем библиотеку C libfoo, которая требует вызова функции инициализации foo_init() во время выполнения. Предположим, что мы также хотим определить глобальную константу foo_data_ptr, которая содержит возвращаемое значение функции void *foo_data(), определенной libfoo; эта константа должна быть инициализирована во время выполнения (а не во время компиляции), потому что адрес указателя будет меняться от выполнения к выполнению. Вы можете этого достичь, определив функцию __init__ в вашем модуле:
const foo_data_ptr = Ref{Ptr{Cvoid}}(0)
function __init__()
ccall((:foo_init, :libfoo), Cvoid, ())
foo_data_ptr[] = ccall((:foo_data, :libfoo), Ptr{Cvoid}, ())
nothing
end
Обратите внимание, что вполне возможно определить глобальную переменную внутри функции, как __init__; это одно из преимуществ использования динамического языка. Но, сделав её константой на уровне глобального пространства имён, мы можем гарантировать, что тип известен компилятору и позволит ему генерировать более оптимизированный код. Очевидно, что любые другие глобальные переменные в вашем модуле, которые зависят от foo_data_ptr, также должны быть инициализированы в __init__.
Константы, включающие большинство объектов Julia, которые не создаются с помощью ccall, не нужно размещать в __init__; их определения можно предварительно скомпилировать и загрузить из кэшированного образа модуля. Это включает сложные объекты, размещаемые в куче, такие как массивы. Однако любая процедура, возвращающая значение необработанного указателя, должна вызываться во время выполнения для работы предварительной компиляции (Ptr объекты превратятся в нулевые указатели, если они не скрыты внутри isbits объекта). Это включает возвращаемые значения функций Julia cfunction и pointer.
Сложнее дело обстоит с типами словарей и множеств или, в общем, со всем, что зависит от результата метода hash(key). В общем случае, когда ключами являются числа, строки, символы, диапазоны, Expr, или комбинации этих типов (через массивы, кортежи, множества, пары и т.д.) они безопасны для предварительной компиляции. Однако для некоторых других типов ключей, таких как Function или DataType и общих пользовательских типов, где вы не определили метод hash, метод-обработчик по умолчанию hash зависит от адреса объекта в памяти (через его objectid) и, следовательно, может меняться от запуска к запуску. Если у вас есть один из этих типов ключей или вы не уверены, для безопасности вы можете инициализировать этот словарь внутри функции __init__. В качестве альтернативы вы можете использовать тип словаря IdDict, который специально обрабатывается при предварительной компиляции, чтобы его можно было безопасно инициализировать во время компиляции.
При использовании предварительной компиляции важно четко понимать разницу между фазой компиляции и фазой выполнения. В этом режиме часто будет гораздо яснее, что Julia — это компилятор, позволяющий выполнять произвольный код Julia, а не автономный интерпретатор, который также генерирует скомпилированный код.
Другие известные потенциальные сценарии ошибок включают:
-
Глобальные счётчики (например, для попытки уникальной идентификации объектов). Рассмотрим следующий фрагмент кода:
mutable struct UniquedById myid::Int let counter = 0 UniquedById() = new(counter += 1) end endхотя цель этого кода заключалась в том, чтобы предоставить каждому экземпляру уникальный идентификатор, значение счётчика записывается в конце компиляции. Все последующие использования этого инкрементально скомпилированного модуля будут начинаться с того же значения счётчика.
Обратите внимание, что
objectid(который работает путём хеширования указателя памяти) имеет аналогичные проблемы (см. примечания по использованиюDictниже).Альтернативой является использование макроса для захвата
@__MODULE__и хранения его вместе с текущим значениемcounter, однако, лучше перепроектировать код, чтобы он не зависел от этого глобального состояния. Ассоциативные коллекции (такие как
DictиSet) должны быть перехешированы в__init__. (В будущем может быть предоставлен механизм для регистрации функции инициализации.)В зависимости от влияний на время компиляции, сохраняющихся во время загрузки. Примеры включают: модификацию массивов или других переменных в других модулях Julia; поддержание дескрипторов открытых файлов или устройств; хранение указателей на другие системные ресурсы (включая память);
-
Создание случайных "копий" глобального состояния из другого модуля путём прямого обращения к нему вместо использования его пути поиска. Например, (в глобальном пространстве имён):
#mystdout = Base.stdout #= will not work correctly, since this will copy Base.stdout into this module =# # instead use accessor functions: getstdout() = Base.stdout #= best option =# # or move the assignment into the runtime: __init__() = global mystdout = Base.stdout #= also works =#
Несколько дополнительных ограничений налагается на операции, которые могут выполняться при предварительной компиляции кода, чтобы помочь пользователю избежать других ситуаций с некорректным поведением:
- Вызов
evalдля вызова побочного эффекта в другом модуле. Это также вызовет предупреждение при включении флага инкрементальной предварительной компиляции. -
global constинструкции из локального пространства имён после того, как__init__()была запущена (см. вопрос #12010 для планов по добавлению ошибки для этого) - Замена модуля — ошибка во время выполнения при инкрементальной предварительной компиляции.
Несколько других моментов, которые следует учитывать:
- После внесения изменений в исходные файлы (включая изменения, внесённые
Pkg.update) не выполняется перезагрузка кода/очистка кэша, и послеPkg.rmне выполняется очистка - Поведение совместного использования памяти переформатированного массива игнорируется предварительной компиляцией (каждый вид получает свою копию)
- Ожидание неизменности файловой системы между временем компиляции и временем выполнения, например,
@__FILE__/source_path()для поиска ресурсов во время выполнения, или макрос BinDeps@checked_lib. Иногда это неизбежно. Однако, когда это возможно, хорошей практикой является копирование ресурсов в модуль во время компиляции, чтобы они не нуждались в поиске во время выполнения. -
WeakRefобъекты и финализаторы в настоящее время не обрабатываются должным образом сериализатором (это будет исправлено в ближайшем выпуске). - Обычно лучше избегать захвата ссылок на экземпляры внутренних объектов метаданных, таких как
Method,MethodInstance,MethodTable,TypeMapLevel,TypeMapEntryи полей этих объектов, так как это может ввести в заблуждение сериализатор и может не привести к желаемому результату. Это не обязательно ошибка, но вы должны быть готовы к тому, что система попытается скопировать некоторые из этих объектов и создать единственный уникальный экземпляр других.
Иногда для разработки модулей полезно отключить инкрементальную предварительную компиляцию. Флаг командной строки --compiled-modules={yes|no} позволяет включать и выключать предварительную компиляцию модулей. Когда Julia запускается с --compiled-modules=no, сериализованные модули в кэше компиляции игнорируются при загрузке модулей и зависимостей модулей. Base.compilecache по-прежнему можно вызвать вручную. Состояние этого флага командной строки передаётся Pkg.build для отключения автоматического запуска предварительной компиляции при установке, обновлении и явном построении пакетов.
© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.4.2/manual/modules/