Spec-Zone.ru › Julia 1.8

Модули

Модули в Julia помогают организовать код в связные единицы. Они определяются синтаксически внутри module NameOfModule ... end, и обладают следующими особенностями:

  1. Модули являются отдельными именованными пространствами, каждый из которых вводит новую глобальную область видимости. Это полезно, поскольку позволяет использовать одно и то же имя для различных функций или глобальных переменных без конфликтов, при условии, что они находятся в разных модулях.

  2. Модули имеют средства для подробного управления именованными пространствами: каждый определяет набор имен, которые он exports, и может импортировать имена из других модулей с помощью using и import (мы объясним их ниже).

  3. Модули могут быть предварительно скомпилированы для более быстрого загрузки и содержат код для инициализации во время выполнения.

Как правило, в больших пакетах Julia вы увидите код модуля, организованный в файлы, например

module SomeModule

# export, using, import statements are usually here; we discuss these below

include("file1.jl")
include("file2.jl")

end

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

Рекомендуемый стиль — не отступать тело модуля, так как это обычно приводит к отступу целых файлов. Также принято использовать UpperCamelCase для имен модулей (так же, как и для типов) и использовать множественное число, если это применимо, особенно если модуль содержит идентификатор с аналогичным именем, чтобы избежать конфликтов имен. Например,

module FastThings

struct FastThing
    ...
end

end

Управление именованными пространствами

Управление именованными пространствами относится к средствам языка для обеспечения доступности имен в модуле в других модулях. Мы подробно обсудим связанные понятия и функциональность ниже.

Квалифицированные имена

Имена функций, переменных и типов в глобальной области видимости, такие как sin, ARGS, и UnitRange всегда принадлежат модулю, называемому родительским модулем, который можно найти интерактивно с помощью parentmodule, например

julia> parentmodule(UnitRange)
Base

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

Если имя квалифицировано, то оно всегда доступно, а в случае функции к нему также можно добавить методы, используя квалифицированное имя в качестве имени функции.

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

Списки экспорта

Имена (относящиеся к функциям, типам, глобальным переменным и константам) могут быть добавлены в список экспорта модуля с помощью export. Как правило, они находятся в начале или вблизи начала определения модуля, чтобы читатели исходного кода могли легко их найти, как в

julia> module NiceStuff
       export nice, DOG
       struct Dog end      # singleton type, not exported
       const DOG = Dog()   # named instance, exported
       nice(x) = "nice $x" # function, exported
       end;

но это просто рекомендация по стилю — в модуле может быть несколько операторов export в произвольных местах.

Часто экспортируются имена, которые являются частью API (интерфейса прикладного программирования). В приведенном выше коде список экспорта предполагает, что пользователи должны использовать nice и DOG. Однако, поскольку квалифицированные имена всегда делают идентификаторы доступными, это всего лишь способ организации API: в отличие от других языков, Julia не имеет средств для истинного скрытия внутренней части модуля.

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

Самостоятельные using и import

Возможно, наиболее распространенный способ загрузки модуля — это using ModuleName. Это загружает код, связанный с ModuleName, и загружает

  1. имя модуля

  2. и элементы списка экспорта в окружающее глобальное пространство имен.

Технически, утверждение using ModuleName означает, что модуль под названием ModuleName будет доступен для разрешения имен по мере необходимости. Когда встречается глобальная переменная, у которой нет определения в текущем модуле, система будет искать ее среди переменных, экспортированных модулем ModuleName, и использовать ее, если она найдена. Это означает, что все обращения к этой глобальной переменной в текущем модуле будут связаны с определением этой переменной в ModuleName.

Для загрузки модуля из пакета можно использовать оператор using ModuleName. Для загрузки модуля из локально определенного модуля необходимо добавить точку перед именем модуля, как в using .ModuleName.

Для продолжения нашего примера,

julia> using .NiceStuff

загрузило бы указанный код, сделав NiceStuff (имя модуля), DOG и nice доступными. Dog не находится в списке экспорта, но к нему можно получить доступ, если имя квалифицировано путем модуля (который здесь просто имя модуля) как NiceStuff.Dog.

Важно, что using ModuleName — единственная форма, для которой списки экспорта вообще имеют значение.

В отличие от этого,

julia> import .NiceStuff

вносит только имя модуля в область видимости. Пользователи должны будут использовать NiceStuff.DOG, NiceStuff.Dog, и NiceStuff.nice для доступа к его содержимому. Обычно import ModuleName используется в контекстах, когда пользователь хочет сохранить пространство имен чистым. Как мы увидим в следующем разделе, import .NiceStuff эквивалентно using .NiceStuff: NiceStuff.

Вы можете объединять несколько операторов using и import одного типа в выражении, разделенном запятыми, например

julia> using LinearAlgebra, Statistics

using и import со специфическими идентификаторами и добавлением методов

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

julia> using .NiceStuff: nice, DOG

импортирует имена nice и DOG.

Важно, что имя модуля NiceStuff не будет в пространстве имен. Если вы хотите сделать его доступным, вам нужно указать его явно, как

julia> using .NiceStuff: nice, DOG, NiceStuff

Julia имеет две формы для, по-видимому, одного и того же, потому что только import ModuleName: f позволяет добавлять методы к f без пути модуля. То есть, следующий пример выдаст ошибку:

julia> using .NiceStuff: nice

julia> struct Cat end

julia> nice(::Cat) = "nice 😸"
ERROR: error in method definition: function NiceStuff.nice must be explicitly imported to be extended
Stacktrace:
 [1] top-level scope
   @ none:0
 [2] top-level scope
   @ none:1

Эта ошибка предотвращает случайное добавление методов к функциям в других модулях, которые вы намеревались только использовать.

Существует два способа решения этой проблемы. Вы всегда можете квалифицировать имена функций путем модуля:

julia> using .NiceStuff

julia> struct Cat end

julia> NiceStuff.nice(::Cat) = "nice 😸"

В качестве альтернативы, вы можете import конкретное имя функции:

julia> import .NiceStuff: nice

julia> struct Cat end

julia> nice(::Cat) = "nice 😸"
nice (generic function with 2 methods)

Какой вариант вы выберите, зависит от стиля. Первая форма ясно показывает, что вы добавляете метод к функции в другом модуле (помните, что импорты и определения методов могут находиться в разных файлах), а вторая — короче, что особенно удобно, если вы определяете несколько методов.

После того, как переменная становится видимой через using или import, модуль не может создать свою собственную переменную с тем же именем. Импортированные переменные являются только для чтения; присвоение значения глобальной переменной всегда влияет на переменную, принадлежащую текущему модулю, или же вызывает ошибку.

Переименование с as

Идентификатор, добавленный в область видимости операторами import или using, может быть переименован с помощью ключевого слова as. Это полезно для устранения конфликтов имен и для сокращения имен. Например, Base экспортирует имя функции read, но пакет CSV.jl также предоставляет CSV.read. Если мы будем многократно вызывать чтение CSV, было бы удобно убрать квалификатор CSV.. Но тогда неясно, к Base.read или CSV.read мы обращаемся:

julia> read;

julia> import CSV: read
WARNING: ignoring conflicting import of CSV.read into Main

Переименование предоставляет решение:

julia> import CSV: read as rd

Импортированные пакеты также могут быть переименованы:

import BenchmarkTools as BT

as работает с using только когда в область видимости вводится один идентификатор. Например, using CSV: read as rd работает, но using CSV as C не работает, так как он действует на все экспортированные имена в CSV.

Комбинирование нескольких операторов using и import

Когда используется несколько операторов using или import любой из указанных выше форм, их эффект комбинируется в порядке их появления. Например,

julia> using .NiceStuff         # exported names and the module name

julia> import .NiceStuff: nice  # allows adding methods to unqualified functions

введет все экспортированные имена модуля NiceStuff и само имя модуля в область видимости, а также позволит добавлять методы к nice без префикса имени модуля.

Обработка конфликтов имен

Рассмотрим ситуацию, когда два (или более) пакета экспортируют одно и то же имя, как в

julia> module A
       export f
       f() = 1
       end
A
julia> module B
       export f
       f() = 2
       end
B

Оператор using .A, .B работает, но когда вы пытаетесь вызвать f, вы получаете предупреждение

julia> using .A, .B

julia> f
WARNING: both B and A export "f"; uses of it in module Main must be qualified
ERROR: UndefVarError: f not defined

В данном случае Julia не может определить, к какому f вы ссылаетесь, поэтому вам нужно сделать выбор. Обычно используются следующие решения:

  1. Просто продолжайте с квалифицированными именами, такими как A.f и B.f. Это делает контекст понятным для читателя вашего кода, особенно если f случайно совпадает, но имеет разное значение в различных пакетах. Например, degree имеет различные применения в математике, естественных науках и в повседневной жизни, и эти значения следует сохранять раздельно.

  2. Используйте ключевое слово as выше, чтобы переименовать один или оба идентификатора, например

    julia> using .A: f as f
    
    julia> using .B: f as g
    

    Это сделает B.f доступным как g. Здесь мы предполагаем, что вы не использовали using A ранее, что привело бы f в пространство имён.

  3. Когда имена в вопросе действительно имеют общее значение, обычно один модуль импортирует его из другого или имеет лёгкий «базовый» пакет с единственной функцией определения интерфейса, как в этом случае, который может использоваться другими пакетами. Принято, чтобы имена таких пакетов заканчивались на ...Base (что не имеет никакого отношения к модулю Base Julia).

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

Модули автоматически содержат using Core, 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

Если даже Core не нужен, модуль, который ничего не импортирует и не определяет никаких имён, может быть определён с помощью Module(:YourNameHere, false, false), и код может быть вычислен в нём с помощью @eval или Core.eval.

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

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

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

По умолчанию Julia поставляется с некоторыми стандартными библиотечными модулями. Они ведут себя как обычные пакеты Julia, за исключением того, что вам не нужно их явно устанавливать. Например, если вы хотите выполнить некоторые тесты модулей, вы можете загрузить стандартную библиотеку Test следующим образом:

using Test

Подмодули и относительные пути

Модули могут содержать подмодули, вложенные с использованием того же синтаксиса module ... end. Они могут быть использованы для введения отдельных пространств имён, что может быть полезно для организации сложных кодовых баз. Обратите внимание, что каждый module вводит свою собственную область видимости, поэтому подмодули не «унаследуют» имена автоматически от своего родительского модуля.

Рекомендуется, чтобы подмодули ссылались на другие модули в закрывающем родительском модуле (включая последний) с использованием относительных квалификаторов модулей в using и import операциях. Относительный квалификатор модуля начинается с точки (.), которая соответствует текущему модулю, и каждая последующая . ведёт к родителю текущего модуля. За этим должны следовать модули при необходимости, а в конце — фактическое имя для доступа, всё разделено ..

Рассмотрим следующий пример, где подмодуль SubA определяет функцию, которая затем расширяется в его «братском» модуле:

julia> module ParentModule
       module SubA
       export add_D  # exported interface
       const D = 3
       add_D(x) = x + D
       end
       using .SubA  # brings `add_D` into the namespace
       export add_D # export it from ParentModule too
       module SubB
       import ..SubA: add_D # relative path for a “sibling” module
       struct Infinity end
       add_D(x::Infinity) = x
       end
       end;

Вы можете увидеть код в пакетах, который в аналогичной ситуации использует

julia> import .ParentModule.SubA: add_D

Однако это работает через загрузку кода, и поэтому работает только если ParentModule находится в пакете. Лучше использовать относительные пути.

Обратите внимание, что порядок определений также имеет значение, если вы вычисляете значения. Рассмотрим

module TestPackage

export x, y

x = 0

module Sub
using ..TestPackage
z = y # ERROR: UndefVarError: y not defined
end

y = 1

end

где Sub пытается использовать TestPackage.y до его определения, поэтому у него нет значения.

По аналогичным причинам вы не можете использовать циклический порядок:

module A

module B
using ..C # ERROR: UndefVarError: C not defined
end

module C
using ..B
end

end

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

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

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

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

Если вы знаете, что модуль не безопасен для предварительной компиляции (например, по одной из причин, описанных ниже), вы должны поместить __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–2022 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.8/manual/modules/

Spec-Zone.ru

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