Spec-Zone.ru › Julia 1.2

Модули

Модули в 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, с путём, который создал файл предварительной компиляции. Также учитывается набор зависимостей, уже загруженных в текущий процесс, и эти модули не будут перекомпилированы, даже если их файлы изменятся или исчезнут, чтобы избежать создания несовместимости между работающей системой и кэшем предварительной компиляции.

END_OF_DOCUMENT_MARKER

Если вам известно, что модуль не подходит для предварительной компиляции (например, по одной из причин, описанных ниже), вы должны поместить __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, а не автономный интерпретатор, который также генерирует скомпилированный код.

Другие известные потенциальные сценарии сбоев:

  1. Глобальные счетчики (например, для попытки уникальной идентификации объектов). Рассмотрим следующий фрагмент кода:

    mutable struct UniquedById
        myid::Int
        let counter = 0
            UniquedById() = new(counter += 1)
        end
    end

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

    Обратите внимание, что objectid (который работает путем хэширования указателя памяти) имеет похожие проблемы (см. примечания к использованию Dict ниже).

    Альтернативой является использование макроса для захвата @__MODULE__ и хранения его вместе с текущим значением counter, однако лучше переработать код, чтобы он не зависел от этого глобального состояния.

  2. Ассоциативные коллекции (например, Dict и Set) необходимо перехэшировать в __init__. (В будущем может быть предоставлен механизм для регистрации функции инициализации.)

  3. Зависимость от побочных эффектов на этапе компиляции, сохраняющихся на этапе загрузки. Примеры включают: изменение массивов или других переменных в других модулях Julia; поддержание дескрипторов открытых файлов или устройств; хранение указателей на другие системные ресурсы (включая память);

  4. Создание случайных «копий» глобального состояния из другого модуля путем прямого ссылки на него вместо использования его пути поиска. Например, (на глобальном уровне):

    #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 =#

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

  1. Вызов eval для создания побочного эффекта в другом модуле. Это также приведет к выводу предупреждения при установке флага инкрементной предварительной компиляции.
  2. global const операторы из локальной области видимости после запуска __init__() (см. вопрос #12010 для планов добавления ошибки для этого)
  3. Замена модуля является ошибкой во время выполнения при инкрементной предварительной компиляции.

Некоторые другие моменты, которые следует учитывать:

  1. После внесения изменений в исходные файлы (включая изменения, внесенные Pkg.update) и после Pkg.rm не выполняется перезагрузка кода/очистка кэша.
  2. Поведение совместного использования памяти реструктурированного массива игнорируется предварительной компиляцией (каждый вид получает свою собственную копию).
  3. Предполагается, что файловая система не изменится между этапом компиляции и этапом выполнения, например, @__FILE__/source_path() для поиска ресурсов во время выполнения или макрос BinDeps @checked_lib. Иногда этого избежать нельзя. Однако, если возможно, рекомендуется копировать ресурсы в модуль на этапе компиляции, чтобы они не требовались для поиска во время выполнения.
  4. WeakRef объекты и финализаторы в настоящее время не обрабатываются сериализатором должным образом (это будет исправлено в ближайшем выпуске).
  5. Обычно лучше избегать захвата ссылок на экземпляры внутренних метаданных, таких как Method, MethodInstance, MethodTable, TypeMapLevel, TypeMapEntry и поля этих объектов, так как это может сбить сериализатор с толку и может привести к результату, которого вы не ожидаете. Это не обязательно ошибка, но вам просто нужно быть готовым к тому, что система попытается скопировать некоторые из них и создать один единственный экземпляр других.

Иногда при разработке модуля полезно отключить инкрементную предварительную компиляцию. Флаг командной строки --compiled-modules={yes|no} позволяет включить и выключить предварительную компиляцию модулей. Когда Julia запускается с флагом --compiled-modules=no, сериализованные модули в кэше компиляции игнорируются при загрузке модулей и зависимостей модулей. Base.compilecache все еще можно вызвать вручную. Состояние этого флага командной строки передается в Pkg.build для отключения автоматического запуска предварительной компиляции при установке, обновлении и явном построении пакетов.

© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.2.0/manual/modules/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API