Spec-Zone.ru › Julia 0.5

Модули

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

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

module MyModule
using Lib

using BigLib: thing1, thing2

import Base.show

importall OtherLib

export MyType, foo

type 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), то к нему можно получить доступ, даже если оно не экспортировано. Это часто бывает полезно при отладке.

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

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

END_OF_DOCUMENT_MARKER

Предварительная компиляция модуля также рекурсивно предварительно компилирует любые модули, которые импортируются в нём. Если вы знаете, что предварительная компиляция вашего модуля небезопасна (по причинам, описанным ниже), вы должны поместить __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. Глобальные счётчики (например, для попытки уникальной идентификации объектов). Рассмотрим следующий фрагмент кода:

    type 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, LambdaInfo, 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.5/manual/modules/

Spec-Zone.ru

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