Spec-Zone.ru › Julia 1.3

Модули

Модули в 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–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.3.1/manual/modules/

Spec-Zone.ru

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