Spec-Zone.ru › Julia 0.7

Модули

Модули в 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

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

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

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

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

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

По умолчанию определения верхнего уровня и модули без определений

Помимо 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.

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

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

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

Размещение этого оператора в файле ~/.julia/config/startup.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 создаёт предварительно скомпилированные кэши модулей, чтобы сократить это время.

Инкрементные предварительно скомпилированные файлы модуля создаются и используются автоматически при использовании import или using для загрузки модуля. Это приведет к его автоматической компиляции при первом импорте. В качестве альтернативы, вы можете вручную вызвать Base.compilecache(modulename). Результирующие файлы кэша будут храниться в DEPOT_PATH[1]/compiled/. Впоследствии модуль автоматически перекомпилируется при using или import всякий раз, когда изменяются какие-либо из его зависимостей; зависимости — это модули, которые он импортирует, сборка Julia, файлы, которые он включает, или явные зависимости, объявленные include_dependency(path) в файлах модуля.

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

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

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/v0.7.0/manual/modules/

Spec-Zone.ru

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