Spec-Zone.ru › Julia 1.6

Модули

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

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

  2. Модули имеют средства для подробного управления именным пространством: каждый определяет набор имён, которые он exportт, и может импортировать имена из других модулей с помощью 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. Обычно они находятся в верхней части определения модуля, чтобы читатели исходного кода могли легко их найти, как в

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 NiceStuff

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

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

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

import NiceStuff

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

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

using LinearAlgebra, Statistics

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

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

using NiceStuff: nice, DOG

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

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

using NiceStuff: nice, DOG, NiceStuff

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

using NiceStuff: nice
struct Cat end
nice(::Cat) = "nice 😸"

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

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

using NiceStuff
struct Cat end
NiceStuff.nice(::Cat) = "nice 😸"

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

import NiceStuff: nice
struct Cat end
nice(::Cat) = "nice 😸"

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

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

using NiceStuff         # exported names and the module name
import NiceStuff: nice  # allows adding methods to unqualified functions

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

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

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

module A
export f
f() = 1
end

module B
export f
f() = 2
end

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

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

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

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

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

    using A: f as f
    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 содержит всю функциональность, «встроенную» в язык.
  • Base содержит базовую функциональность, полезную практически во всех случаях.
  • Main является модулем верхнего уровня и текущим модулем при запуске Julia.

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

using Test

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

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

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

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

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

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

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–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.6.0/manual/modules/

Spec-Zone.ru

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