Spec-Zone.ru › Julia 0.6

Модули

Модули в Julia представляют собой отдельные рабочие пространства переменных, то есть они вводят новую глобальную область видимости. Они разграничены синтаксически, внутри module Name ... end. Модули позволяют создавать определения верхнего уровня (также известные как глобальные переменные) без опасений возникновения конфликтов имен, когда ваш код используется вместе с кодом других людей. Внутри модуля вы можете управлять тем, какие имена из других модулей будут видны (через импорт), и указывать, какие из ваших имен должны быть общедоступными (через экспорт).

Следующий пример демонстрирует основные возможности модулей. Он не предназначен для выполнения, а показан в иллюстративных целях:

module MyModule
using Lib

using BigLib: thing1, thing2

import Base.show

importall OtherLib

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 является синтаксическим сокращением для using BigLib.thing1, BigLib.thing2.

Ключевое слово import поддерживает тот же синтаксис, что и using, но работает только с одним именем за раз. Оно не добавляет модули для поиска, как using. import также отличается от using тем, что функции должны импортироваться с использованием import для расширения новыми методами.

В MyModule выше мы хотели добавить метод к стандартной функции show, поэтому мы должны были написать import Base.show. Функции, имена которых видны только через using, не могут быть расширены.

Ключевое слово importall явно импортирует все имена, экспортированные указанным модулем, как если бы import использовалось для каждого из них индивидуально.

После того, как переменная становится видимой через 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, MyModule.p x и 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
importall MyModule Все экспортированные имена (x и y) x и y

Модули и файлы

Файлы и имена файлов в основном не связаны с модулями; модули связаны только с выражениями модулей. Можно иметь несколько файлов на один модуль и несколько модулей в одном файле:

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

Стандартные модули

Существует три важных стандартных модуля: Main, Core и Base.

Main — это модуль верхнего уровня, и Julia запускается с Main в качестве текущего модуля. Переменные, определённые в командной строке, попадают в Main, а whos() перечисляет переменные в Main.

Core содержит все идентификаторы, рассматриваемые как "встроенные" в язык, то есть являющиеся частью основного языка, а не библиотек. Каждый модуль неявно указывает на using Core, поскольку без этих определений вы ничего не можете сделать.

Base — это стандартная библиотека (содержимое base/). Все модули неявно содержат using Base, поскольку это необходимо в подавляющем большинстве случаев.

По умолчанию определения верхнего уровня и "голые" модули

В дополнение к using Base, модули также автоматически содержат определение функции eval, которая вычисляет выражения в контексте этого модуля.

Если эти определения по умолчанию нежелательны, модули могут быть определены с использованием ключевого слова baremodule вместо (примечание: Core все равно импортируется, как указано выше). С точки зрения baremodule, стандартный module выглядит так:

baremodule Mod

using Base

eval(x) = Core.eval(Mod, x)
eval(m,x) = Core.eval(m, x)

...

end

Относительные и абсолютные пути модулей

Учитывая утверждение using Foo, система ищет Foo внутри Main. Если модуль не существует, система пытается require("Foo"), что обычно приводит к загрузке кода из установленного пакета.

Однако некоторые модули содержат подмодули, что означает, что иногда вам нужно получить доступ к модулю, который напрямую недоступен в Main. Существует два способа сделать это. Первый — использовать абсолютный путь, например using Base.Sort. Второй — использовать относительный путь, что упрощает импорт подмодулей текущего модуля или любого из его модулей-окружений:

module Parent

module Utils
...
end

using .Utils

...
end

Здесь модуль Parent содержит подмодуль Utils, а код в Parent хочет, чтобы содержимое Utils было видно. Это делается путём начала пути using с точки. Добавление дополнительных ведущих точек перемещает на дополнительные уровни в иерархии модулей. Например, using ..Utils будет искать Utils в модуле-окружении Parent вместо того, чтобы искать в самом Parent.

Обратите внимание, что квалификаторы относительного импорта допустимы только в операторах using и import.

Пути к файлам модулей

Глобальная переменная LOAD_PATH содержит каталоги, в которых Julia ищет модули при вызове require. Она может быть расширена с помощью push!:

push!(LOAD_PATH, "/Path/To/My/Module/")

Размещение этого оператора в файле ~/.juliarc.jl позволит расширить LOAD_PATH при каждом запуске Julia. В качестве альтернативы, путь загрузки модулей можно расширить, определив переменную среды JULIA_LOAD_PATH.

Разное по именованному пространству

Если имя квалифицировано (например, Base.sin), то к нему можно получить доступ, даже если оно не экспортировано. Это часто полезно при отладке. К нему также можно добавить методы, используя квалифицированное имя в качестве имени функции. Однако из-за синтаксических неоднозначностей, возникающих, если вы хотите добавить методы к функции в другом модуле, имя которой содержит только символы, например, оператор, Base.+, например, вы должны использовать Base.:+ для ссылки на него. Если оператор имеет длину более одного символа, вы должны заключить его в квадратные скобки, например: Base.:(==).

Имена макросов записываются с @ в операторах импорта и экспорта, например, import Mod.@mac. Макросы в других модулях могут вызываться как Mod.@mac или @Mod.mac.

Синтаксис M.x = y не работает для присвоения глобальной переменной в другом модуле; глобальное присвоение всегда локально для модуля.

Переменная может быть «зарезервирована» для текущего модуля без присваивания ей значения, объявив её как global x на верхнем уровне. Это может использоваться для предотвращения конфликтов имён глобальных переменных, инициализированных после загрузки.

Инициализация и предварительная компиляция модулей

Крупные модули могут занимать несколько секунд для загрузки, потому что выполнение всех операторов в модуле часто связано с компиляцией большого объёма кода. Julia предоставляет возможность создания предварительно скомпилированных версий модулей для сокращения этого времени.

Для создания файла предварительно скомпилированного модуля с инкрементальным обновлением добавьте __precompile__() в верхней части файла модуля (перед началом module). Это заставит его автоматически компилироваться при первом импорте. В качестве альтернативы, вы можете вручную вызвать Base.compilecache(modulename). Полученные файлы кэша будут храниться в Base.LOAD_CACHE_PATH[1]. В дальнейшем модуль автоматически перекомпилируется при import всякий раз, когда изменяются какие-либо из его зависимостей; зависимостями являются импортируемые модули, сборка Julia, файлы, которые он включает, или явные зависимости, объявленные с помощью include_dependency(path) в файлах модуля.

Для зависимостей от файлов изменение определяется проверкой, не изменилось ли время изменения (mtime) каждого загружаемого модулем файла include или явным образом добавленного модулем include_dependency, или же равно ли оно времени изменения, усечённому до ближайшей секунды (чтобы учесть системы, которые не могут копировать mtime с точностью до долей секунды). Также учитывается, соответствует ли путь к файлу, выбранный логикой поиска в require, пути, создавшему файл предварительной компиляции.

END_OF_DOCUMENT_MARKER

Также учитывается набор зависимостей, уже загруженных в текущий процесс, и не будет перекомпилировать эти модули, даже если их файлы изменятся или исчезнут, чтобы избежать создания несовместимостей между работающей системой и кэшем предварительной компиляции. Если вы хотите отразить изменения в исходном коде в работающей системе, вы должны вызвать reload("Module") в модуле, который вы изменили, и в любом модуле, от которого он зависел, в котором вы хотите увидеть отраженные изменения.

Предварительная компиляция модуля также рекурсивно выполняет предварительную компиляцию любых модулей, импортированных в него. Если вы знаете, что предварительная компиляция вашего модуля небезопасна (по причинам, описанным ниже), вы должны поместить __precompile__(false) в файл модуля, чтобы вызвать Base.compilecache для выброса ошибки (и тем самым предотвратить импорт модуля любым другим предварительно скомпилированным модулем).

__precompile__() не следует использовать в модуле, если все его зависимости также не используют __precompile__(). Отсутствие этого может привести к ошибке во время выполнения при загрузке модуля.

Однако, чтобы ваш модуль работал с предварительной компиляцией, вам может потребоваться изменить свой модуль, чтобы явно разделить любые этапы инициализации, которые должны произойти во время выполнения, от шагов, которые могут произойти во время компиляции. Для этой цели Julia позволяет определить функцию __init__() в вашем модуле, которая выполняет любые шаги инициализации, которые должны произойти во время выполнения. Эта функция не будет вызвана во время компиляции (--output-* или __precompile__()). Вы, конечно, можете вызвать её вручную, если необходимо, но по умолчанию предполагается, что эта функция обрабатывает вычисление состояния для локальной машины, которое не нужно – или даже не следует – сохранять в скомпилированном изображении. Она будет вызвана после загрузки модуля в процесс, включая случай загрузки в инкрементальную компиляцию (--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{Void}}(0)
function __init__()
    ccall((:foo_init, :libfoo), Void, ())
    foo_data_ptr[] = ccall((:foo_data, :libfoo), Ptr{Void}, ())
end

Заметьте, что вполне возможно определить глобальную переменную внутри функции, как __init__; это одно из преимуществ использования динамичного языка. Но, сделав её константой на глобальном уровне, мы можем гарантировать, что тип известен компилятору и позволить ему генерировать более оптимизированный код. Очевидно, что любые другие глобальные переменные в вашем модуле, зависящие от foo_data_ptr, также должны быть инициализированы в __init__.

Константы, включающие большинство объектов Julia, которые не создаются ccall, не нужно помещать в __init__: их определения могут быть предварительно скомпилированы и загружены из кэшированного изображения модуля. Это включает сложные объекты, размещаемые в куче, такие как массивы. Однако любая процедура, возвращающая значение сырого указателя, должна вызываться во время выполнения для работы предварительной компиляции (объекты Ptr преобразуются в нулевые указатели, если они не скрыты внутри объекта isbits). Это включает возвращаемые значения функций Julia cfunction и pointer.

Типы словарей и множеств или, в общем, всё, что зависит от результата метода hash(key), – это более сложный случай. В распространённом случае, когда ключи являются числами, строками, символами, диапазонами, Expr, или композициями этих типов (через массивы, кортежи, множества, пары и т. д.), они безопасны для предварительной компиляции. Однако для некоторых других типов ключей, таких как Function или DataType и общих пользовательских типов, где вы не определили метод hash, метод-обработчик hash зависит от адреса памяти объекта (через его object_id) и, следовательно, может меняться от запуска к запуску. Если у вас есть один из этих типов ключей или вы не уверены, для большей безопасности вы можете инициализировать этот словарь внутри функции __init__. Кроме того, вы можете использовать тип словаря ObjectIdDict, который специально обрабатывается предварительной компиляцией, поэтому его безопасно инициализировать во время компиляции.

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

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

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

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

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

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

    Альтернативой является хранение как current_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. Замена модуля (или вызов workspace()) – это ошибка во время выполнения при выполнении инкрементальной предварительной компиляции.

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

  1. После внесения изменений в исходные файлы (включая изменения с помощью Pkg.update) не выполняется перегрузка кода/обновление кэша, и после вызова Pkg.rm не выполняется очистка.

  2. Поведение совместного использования памяти реконструированного массива игнорируется предварительной компиляцией (каждому представлению создаётся своя копия)

  3. Ожидание, что файловая система не изменится между этапом компиляции и этапом выполнения, например, @__FILE__/source_path() для поиска ресурсов во время выполнения или макрос BinDeps @checked_lib. Иногда это неизбежно. Однако, когда это возможно, рекомендуется копировать ресурсы в модуль на этапе компиляции, чтобы их не нужно было искать во время выполнения.

  4. Объекты WeakRef и финализаторы в настоящее время не обрабатываются должным образом сериализатором (это будет исправлено в будущей версии).

  5. Обычно лучше избегать захвата ссылок на экземпляры внутренних метаданных-объектов, таких как Method, MethodInstance, MethodTable, TypeMapLevel, TypeMapEntry и полей этих объектов, так как это может сбить с толку сериализатор и может привести к нежелаемому результату. Это не обязательно ошибка, но вы должны быть готовы к тому, что система попытается скопировать некоторые из них и создать единственный уникальный экземпляр других.

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

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

Spec-Zone.ru

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