Модули
Модули в Julia помогают организовать код в связные единицы. Они определяются синтаксически внутри module NameOfModule ... end, и обладают следующими особенностями:
Модули являются отдельными именованными пространствами, каждый из которых вводит новую глобальную область видимости. Это полезно, поскольку позволяет использовать одно и то же имя для различных функций или глобальных переменных без конфликтов, при условии, что они находятся в разных модулях.
Модули имеют средства для подробного управления именованными пространствами: каждый определяет набор имен, которые он
exports, и может импортировать имена из других модулей с помощьюusingиimport(мы объясним их ниже).Модули могут быть предварительно скомпилированы для более быстрого загрузки и содержат код для инициализации во время выполнения.
Как правило, в больших пакетах Julia вы увидите код модуля, организованный в файлы, например
module SomeModule
# export, using, import statements are usually here; we discuss these below
include("file1.jl")
include("file2.jl")
end
Файлы и имена файлов в основном не связаны с модулями; модули связаны только с выражениями модулей. В одном модуле может быть несколько файлов, и в одном файле может быть несколько модулей. include ведет себя так, как если бы содержимое исходного файла оценивалось в глобальной области видимости включаемого модуля. В этой главе мы используем краткие и упрощенные примеры, поэтому не будем использовать include.
Рекомендуемый стиль — не отступать тело модуля, так как это обычно приводит к отступу целых файлов. Также принято использовать UpperCamelCase для имен модулей (так же, как и для типов) и использовать множественное число, если это применимо, особенно если модуль содержит идентификатор с аналогичным именем, чтобы избежать конфликтов имен. Например,
module FastThings
struct FastThing
...
end
end
Управление именованными пространствами
Управление именованными пространствами относится к средствам языка для обеспечения доступности имен в модуле в других модулях. Мы подробно обсудим связанные понятия и функциональность ниже.
Квалифицированные имена
Имена функций, переменных и типов в глобальной области видимости, такие как sin, ARGS, и UnitRange всегда принадлежат модулю, называемому родительским модулем, который можно найти интерактивно с помощью parentmodule, например
julia> parentmodule(UnitRange) Base
Вы также можете ссылаться на эти имена за пределами своего родительского модуля, предваряя их своим модулем, например Base.UnitRange. Это называется квалифицированным именем. Родительский модуль может быть доступен с помощью цепочки подмодулей, например Base.Math.sin, где Base.Math называется путем модуля. Из-за синтаксических неоднозначностей для квалификации имени, содержащего только символы, таких как оператор, необходимо вставить двоеточие, например Base.:+. Небольшое количество операторов дополнительно требует скобок, например Base.:(==).
Если имя квалифицировано, то оно всегда доступно, а в случае функции к нему также можно добавить методы, используя квалифицированное имя в качестве имени функции.
Внутри модуля имя переменной может быть «зарезервировано» без присвоения ему значения, объявив его как global x. Это предотвращает конфликты имен для глобальных переменных, инициализированных после загрузки. Синтаксис M.x = y не работает для присвоения глобальной переменной в другом модуле; глобальное присвоение всегда локально для модуля.
Списки экспорта
Имена (относящиеся к функциям, типам, глобальным переменным и константам) могут быть добавлены в список экспорта модуля с помощью export. Как правило, они находятся в начале или вблизи начала определения модуля, чтобы читатели исходного кода могли легко их найти, как в
julia> module NiceStuff
export nice, DOG
struct Dog end # singleton type, not exported
const DOG = Dog() # named instance, exported
nice(x) = "nice $x" # function, exported
end;
но это просто рекомендация по стилю — в модуле может быть несколько операторов export в произвольных местах.
Часто экспортируются имена, которые являются частью API (интерфейса прикладного программирования). В приведенном выше коде список экспорта предполагает, что пользователи должны использовать nice и DOG. Однако, поскольку квалифицированные имена всегда делают идентификаторы доступными, это всего лишь способ организации API: в отличие от других языков, Julia не имеет средств для истинного скрытия внутренней части модуля.
Кроме того, некоторые модули вообще не экспортируют имена. Это обычно делается, если они используют общие слова, такие как derivative, в своем API, что может легко привести к конфликтам со списками экспорта других модулей. Мы увидим, как управлять конфликтами имен ниже.
Самостоятельные using и import
Возможно, наиболее распространенный способ загрузки модуля — это using ModuleName. Это загружает код, связанный с ModuleName, и загружает
имя модуля
и элементы списка экспорта в окружающее глобальное пространство имен.
Технически, утверждение using ModuleName означает, что модуль под названием ModuleName будет доступен для разрешения имен по мере необходимости. Когда встречается глобальная переменная, у которой нет определения в текущем модуле, система будет искать ее среди переменных, экспортированных модулем ModuleName, и использовать ее, если она найдена. Это означает, что все обращения к этой глобальной переменной в текущем модуле будут связаны с определением этой переменной в ModuleName.
Для загрузки модуля из пакета можно использовать оператор using ModuleName. Для загрузки модуля из локально определенного модуля необходимо добавить точку перед именем модуля, как в using .ModuleName.
Для продолжения нашего примера,
julia> using .NiceStuff
загрузило бы указанный код, сделав NiceStuff (имя модуля), DOG и nice доступными. Dog не находится в списке экспорта, но к нему можно получить доступ, если имя квалифицировано путем модуля (который здесь просто имя модуля) как NiceStuff.Dog.
Важно, что using ModuleName — единственная форма, для которой списки экспорта вообще имеют значение.
В отличие от этого,
julia> import .NiceStuff
вносит только имя модуля в область видимости. Пользователи должны будут использовать NiceStuff.DOG, NiceStuff.Dog, и NiceStuff.nice для доступа к его содержимому. Обычно import ModuleName используется в контекстах, когда пользователь хочет сохранить пространство имен чистым. Как мы увидим в следующем разделе, import .NiceStuff эквивалентно using .NiceStuff: NiceStuff.
Вы можете объединять несколько операторов using и import одного типа в выражении, разделенном запятыми, например
julia> using LinearAlgebra, Statistics
using и import со специфическими идентификаторами и добавлением методов
Когда using ModuleName: или import ModuleName: сопровождаются перечислением имен, разделенных запятыми, модуль загружается, но только эти конкретные имена попадают в область видимости оператором. Например,
julia> using .NiceStuff: nice, DOG
импортирует имена nice и DOG.
Важно, что имя модуля NiceStuff не будет в пространстве имен. Если вы хотите сделать его доступным, вам нужно указать его явно, как
julia> using .NiceStuff: nice, DOG, NiceStuff
Julia имеет две формы для, по-видимому, одного и того же, потому что только import ModuleName: f позволяет добавлять методы к f без пути модуля. То есть, следующий пример выдаст ошибку:
julia> using .NiceStuff: nice julia> struct Cat end julia> nice(::Cat) = "nice 😸" ERROR: error in method definition: function NiceStuff.nice must be explicitly imported to be extended Stacktrace: [1] top-level scope @ none:0 [2] top-level scope @ none:1
Эта ошибка предотвращает случайное добавление методов к функциям в других модулях, которые вы намеревались только использовать.
Существует два способа решения этой проблемы. Вы всегда можете квалифицировать имена функций путем модуля:
julia> using .NiceStuff julia> struct Cat end julia> NiceStuff.nice(::Cat) = "nice 😸"
В качестве альтернативы, вы можете import конкретное имя функции:
julia> import .NiceStuff: nice julia> struct Cat end julia> nice(::Cat) = "nice 😸" nice (generic function with 2 methods)
Какой вариант вы выберите, зависит от стиля. Первая форма ясно показывает, что вы добавляете метод к функции в другом модуле (помните, что импорты и определения методов могут находиться в разных файлах), а вторая — короче, что особенно удобно, если вы определяете несколько методов.
После того, как переменная становится видимой через using или import, модуль не может создать свою собственную переменную с тем же именем. Импортированные переменные являются только для чтения; присвоение значения глобальной переменной всегда влияет на переменную, принадлежащую текущему модулю, или же вызывает ошибку.
Переименование с as
Идентификатор, добавленный в область видимости операторами import или using, может быть переименован с помощью ключевого слова as. Это полезно для устранения конфликтов имен и для сокращения имен. Например, Base экспортирует имя функции read, но пакет CSV.jl также предоставляет CSV.read. Если мы будем многократно вызывать чтение CSV, было бы удобно убрать квалификатор CSV.. Но тогда неясно, к Base.read или CSV.read мы обращаемся:
julia> read; julia> import CSV: read WARNING: ignoring conflicting import of CSV.read into Main
Переименование предоставляет решение:
julia> import CSV: read as rd
Импортированные пакеты также могут быть переименованы:
import BenchmarkTools as BT
as работает с using только когда в область видимости вводится один идентификатор. Например, using CSV: read as rd работает, но using CSV as C не работает, так как он действует на все экспортированные имена в CSV.
Комбинирование нескольких операторов using и import
Когда используется несколько операторов using или import любой из указанных выше форм, их эффект комбинируется в порядке их появления. Например,
julia> using .NiceStuff # exported names and the module name julia> import .NiceStuff: nice # allows adding methods to unqualified functions
введет все экспортированные имена модуля NiceStuff и само имя модуля в область видимости, а также позволит добавлять методы к nice без префикса имени модуля.
Обработка конфликтов имен
Рассмотрим ситуацию, когда два (или более) пакета экспортируют одно и то же имя, как в
julia> module A
export f
f() = 1
end
A
julia> module B
export f
f() = 2
end
B
Оператор using .A, .B работает, но когда вы пытаетесь вызвать f, вы получаете предупреждение
julia> using .A, .B julia> f WARNING: both B and A export "f"; uses of it in module Main must be qualified ERROR: UndefVarError: f not defined
В данном случае Julia не может определить, к какому f вы ссылаетесь, поэтому вам нужно сделать выбор. Обычно используются следующие решения:
Просто продолжайте с квалифицированными именами, такими как
A.fиB.f. Это делает контекст понятным для читателя вашего кода, особенно еслиfслучайно совпадает, но имеет разное значение в различных пакетах. Например,degreeимеет различные применения в математике, естественных науках и в повседневной жизни, и эти значения следует сохранять раздельно.-
Используйте ключевое слово
asвыше, чтобы переименовать один или оба идентификатора, напримерjulia> using .A: f as f julia> using .B: f as g
Это сделает
B.fдоступным какg. Здесь мы предполагаем, что вы не использовалиusing Aранее, что привело быfв пространство имён. Когда имена в вопросе действительно имеют общее значение, обычно один модуль импортирует его из другого или имеет лёгкий «базовый» пакет с единственной функцией определения интерфейса, как в этом случае, который может использоваться другими пакетами. Принято, чтобы имена таких пакетов заканчивались на
...Base(что не имеет никакого отношения к модулюBaseJulia).
Определения по умолчанию верхнего уровня и модули без имён
Модули автоматически содержат using Core, 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
Если даже Core не нужен, модуль, который ничего не импортирует и не определяет никаких имён, может быть определён с помощью Module(:YourNameHere, false, false), и код может быть вычислен в нём с помощью @eval или Core.eval.
Стандартные модули
Существует три важных стандартных модуля:
-
Coreсодержит всю функциональность, «встроенную» в язык. -
Baseсодержит базовые функции, полезные практически во всех случаях. -
Main— это модуль верхнего уровня и текущий модуль при запуске Julia.
По умолчанию Julia поставляется с некоторыми стандартными библиотечными модулями. Они ведут себя как обычные пакеты Julia, за исключением того, что вам не нужно их явно устанавливать. Например, если вы хотите выполнить некоторые тесты модулей, вы можете загрузить стандартную библиотеку Test следующим образом:
using Test
Подмодули и относительные пути
Модули могут содержать подмодули, вложенные с использованием того же синтаксиса module ... end. Они могут быть использованы для введения отдельных пространств имён, что может быть полезно для организации сложных кодовых баз. Обратите внимание, что каждый module вводит свою собственную область видимости, поэтому подмодули не «унаследуют» имена автоматически от своего родительского модуля.
Рекомендуется, чтобы подмодули ссылались на другие модули в закрывающем родительском модуле (включая последний) с использованием относительных квалификаторов модулей в using и import операциях. Относительный квалификатор модуля начинается с точки (.), которая соответствует текущему модулю, и каждая последующая . ведёт к родителю текущего модуля. За этим должны следовать модули при необходимости, а в конце — фактическое имя для доступа, всё разделено ..
Рассмотрим следующий пример, где подмодуль SubA определяет функцию, которая затем расширяется в его «братском» модуле:
julia> module ParentModule
module SubA
export add_D # exported interface
const D = 3
add_D(x) = x + D
end
using .SubA # brings `add_D` into the namespace
export add_D # export it from ParentModule too
module SubB
import ..SubA: add_D # relative path for a “sibling” module
struct Infinity end
add_D(x::Infinity) = x
end
end;
Вы можете увидеть код в пакетах, который в аналогичной ситуации использует
julia> import .ParentModule.SubA: add_D
Однако это работает через загрузку кода, и поэтому работает только если ParentModule находится в пакете. Лучше использовать относительные пути.
Обратите внимание, что порядок определений также имеет значение, если вы вычисляете значения. Рассмотрим
module TestPackage export x, y x = 0 module Sub using ..TestPackage z = y # ERROR: UndefVarError: y not defined end y = 1 end
где Sub пытается использовать TestPackage.y до его определения, поэтому у него нет значения.
По аналогичным причинам вы не можете использовать циклический порядок:
module A module B using ..C # ERROR: UndefVarError: C not defined end module C using ..B end end
Инициализация и предварительная компиляция модулей
Большие модули могут загружаться несколько секунд, потому что выполнение всех операторов в модуле часто включает компиляцию большого объёма кода. 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–2022 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.8/manual/modules/