Spec-Zone.ru › Julia 1.0

Модули

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

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

Spec-Zone.ru

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