Модули
Модули в 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.5.3/manual/modules/