Julia Modules

YebinPapa·2022년 12월 20일
0

Julia documents

목록 보기
5/5

Modules

주의
Julia 의 modules.jl 에 대한 매뉴얼 번역

2022.12.20

Julia의 모듈은 코드를 일관된 단위로 구성하는 데 도움이 됩니다. 모듈 NameOfModule ... end 내에서 구문적으로 구분되며 다음과 같은 기능이 있습니다.

  1. 모듈은 각각 새로운 전역 범위를 도입하는 별도의 네임스페이스입니다. 이는 서로 다른 함수나 전역 변수가 별도의 모듈에 있는 한 충돌 없이 동일한 이름을 사용할 수 있기 때문에 유용합니다.

  2. 모듈에는 상세한 네임스페이스 관리를 위한 기능이 있습니다. 각 모듈은 내보내는 이름 집합을 정의하고 usingimport를 사용하여 다른 모듈에서 이름을 가져올 수 있습니다(아래에서 설명).

  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, ARGSUnitRange 와 같은 전역 범위의 함수, 변수 및 타입의 이름은 항상 부모모듈(parent module) 이라는 모듈에 속하며,parentsmodule 을 사용하여 대화식으로 찾을 수 있습니다. 예를 들어,

julia> parentmodule(UnitRange)
Base

Base.UnitRange 처럼 이름에 부모 모듈의 이름을 접두사로 붙여 부모 모듈 외부에서 이름을 참조할 수도 있습니다. 이를 정규화된 이름 (qualified name) 이라고 합니다. 상위 모듈은 Base.Math.sin 과 같은 하위 모듈 체인을 사용하여 액세스할 수 있습니다. 여기서 Base.Math 는 모듈 경로라고 합니다. 구문적 모호성으로 인해 연산자와 같은 기호만 포함된 이름을 한정하려면 콜론을 삽입해야 합니다. 예를 들면 Base.:+. 소수의 연산자에는 추가로 괄호가 필요합니다. Base.:(==).

이름이 정규화되면 항상 액세스할 수 있으며 함수의 경우 정규화 이름을 함수 이름으로 사용하여 메서드를 추가할 수도 있습니다.

모듈 내에서 변수 이름을 전역 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(응용 프로그래밍 인터페이스)의 일부를 구성하는 이름을 내보냅니다. 위의 코드에서 내보내기 목록은 사용자가 niceDOG 를 사용해야 한다고 제안합니다. 그러나 정규화된 이름은 항상 식별자에 액세스할 수 있게 하므로 이는 API를 구성하기 위한 옵션일 뿐입니다. 다른 언어와 달리 Julia에는 모듈 내부를 진정으로 숨길 수 있는 기능이 없습니다.

또한 일부 모듈은 이름을 전혀 내보내지 않습니다. 이는 일반적으로 다른 모듈의 내보내기 목록과 쉽게 충돌할 수 있는 API에서 파생어와 같은 일반적인 단어를 사용하는 경우 수행됩니다. 아래에서 이름 충돌을 관리하는 방법을 살펴보겠습니다.


usingimport

아마도 모듈을 로드하는 가장 일반적인 방법은 using ModuleName 입니다. 그러면 ModuleName 과 연결된 코드가 로드되고

  1. 모듈 이름
  2. 내보내기 목록의 요소

를 주변 전역 네임스페이스로 가져옵니다.

기술적으로 ModuleName 을 사용하는 문은 필요에 따라 이름을 확인하는 데 ModuleName 이라는 모듈을 사용할 수 있음을 의미합니다. 현재 모듈에 정의가 없는 전역 변수가 발견되면 시스템은 ModuleName 에서 내보낸 변수 중에서 이를 검색하고 발견되면 사용합니다. 이것은 현재 모듈 내에서 해당 전역을 사용하는 모든 것이 ModuleName 에서 해당 변수의 정의로 해석됨을 의미합니다.

패키지에서 모듈을 로드하려면 ModuleName 을 사용하는 문을 사용할 수 있습니다. 로컬로 정의된 모듈에서 모듈을 로드하려면 .ModuleName 을 사용하는 것과 같이 모듈 이름 앞에 점을 추가해야 합니다.

julia> using .NiceStuff

라고 하면 NiceStuff (모듈 이름), DOGnice 를 사용할 수 있도록 위의 코드를 로드합니다. Dog 는 내보내기 목록에 없지만 이름이 NiceStuff.Dog 로 모듈 경로(여기서는 모듈 이름임)로 정규화 되는 경우 접근 할 수 있습니다.

중요한 것은 using ModuleName 은 목록을 내보내는 유일한 형식이라는 것입니다.

대조적으로,

julia> import .NiceStuff

는 모듈 이름만 범위로 가져옵니다. 사용자가 콘텐츠에 액세스하려면 NiceStuff.DOG, NiceStuff.DogNiceStuff.nice 를 사용해야 합니다. 일반적으로 import ModuleName 은 사용자가 네임스페이스를 깨끗하게 유지하려 할 때 에서 사용됩니다. 다음 섹션에서 볼 수 있듯이 import .NiceStuffusing .NiceStuff: NiceStuff 를 사용하는 것과 같습니다.

같은 종류의 여러 usingimport 문을 쉼표로 구분하여 연결 할 수 있습니다.

julia> using LinearAlgebra, Statistics

특정 식별자와 함께 usinginport 하고 메서드 추가

using ModuleName: 또는 import ModuleName: 다음에 쉼표로 구분된 이름 목록이 오는 경우 모듈은 로드되지만 해당 이름만 명령문에 의해 네임스페이스로 가져와집니다. 예를 들어,

julia> using .NiceStuff: nice, DOG

niceDOG 만 가져옵니다.

중요한 것은 모듈 이름 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)

어떤 것을 선택하느냐는 스타일의 문제입니다. 첫 번째 형식은 다른 모듈의 함수에 메서드를 추가하고 있음을 분명히 보여줍니다(imports 들과 메서드 정의가 별도의 파일에 있을 수 있음을 기억하십시오). 반면 두 번째 형식은 더 짧아서 정의할 때 특히 편리합니다. 여러 방법.

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 된 패키지 자신도 이름이 변경될 수 있습니다.

import BenchmarkTools as BT

하나의 식별자만 스코프에 넣을 때, asusing 과 같이 사용한다. 예를 들어 using CSV: read as rd 는 동작하지만 using CSV as C 는 동작하지 않는다. 이렇게 하면 CSV 에 포함된 모든 이름에 대해 동작해야 하기 때문이다.


여러개의 usingimport 를 섞어서 사용하기

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.fB.f 와 같은 정규화된 이름으로 진행합니다. 이렇게 하면 특히 f가 우연히 일치하지만 다양한 패키지에서 다른 의미를 갖는 경우 코드를 읽는 사람에게 맥락이 명확해집니다. 예를 들어 degree 는 수학, 자연 과학 및 일상 생활에서 다양하게 사용되며 이러한 의미는 별도로 유지되어야 합니다.

  2. as 키워드를 사용하여 하나 혹은 둘 다 이름을 변경합니다.

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

    이렇게 하면 B.fg 라는 이름으로 사용할 수 있습니다. 여기서는 이전에 using A 를 사용하지 않았다고 가정합니다. 이렇게 하면 f 가 네임스페이스에 들어와 있기 때문입니다.

  3. 문제의 이름이 의미를 공유하는 경우 한 모듈이 다른 모듈에서 가져오거나 이와 같은 인터페이스를 정의하는 유일한 기능이 있는 경량 "기본" 패키지를 갖는 것이 일반적이며 다른 패키지에서 사용할 수 있습니다. 이러한 패키지 이름은 ...Base 로 끝나는 것이 일반적입니다(Julia의 Base 모듈과 관련이 없음).


기본 최상위 정의 및 베어 모듈

모듈은 자동으로 using Core, using Base, 그리고 evalinclude 함수의 정의를 포함하며, 이것들은 해당 모듈의 전역 범위 내에서 표현식/파일을 평가합니다.

이러한 기본 정의가 필요하지 않은 경우에는 대신 baremodule 키워드를 사용하여 모듈을 정의할 수 있습니다(참고: Core는 여전히 imported 됩니다). 베어모듈 측면에서 표준 모듈은 다음과 같습니다.

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

하위모듈과 상대적인 경로

모듈은 하위 모듈(submodules)을 포함할 수 있으며 동일한 module ... end 구문 안에 중첩합니다. 별도의 네임스페이스를 도입하는 데 사용하는데, 복잡한 코드베이스를 구성하는 데 도움이 됩니다. 각 module은 자체 범위 (scope) 를 도입하므로 하위 모듈은 상위 모듈에서 이름을 자동으로 "상속"하지 않습니다.

하위 모듈이 상위 모듈에 포함되는 다른 모듈(뒤에 나오는 모듈이더라도) 을 참조할때는 usingimport 문에서 상대적인 모듈 한정자를 사용하는 것이 좋습니다. 상대적인 모듈 한정자는 현재 모듈에 해당하는 마침표(.)로 시작하고 각 연속 . 현재 모듈의 부모로 이어집니다. 그 다음에는 필요한 경우 모듈이 와야 하며 결국 액세스할 실제 이름이 모두 . 들로 구분됩니다.

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

에서는 SubTestPackage.y 를 정의되기도 전, 즉 값을 갖기도 전에 사용하려고 합니다.

비슷한 이유로, 순환적인 순서로 사용 할 수 없습니다.

module A

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

module C
using ..B
end

end

모듈 초기화화 사전컴파일

모듈의 모든 문을 실행하려면 종종 많은 양의 코드를 컴파일해야 하므로 큰 모듈을 로드하는 데 몇 초가 걸릴 수 있습니다. Julia는 이 시간을 줄이기 위해 미리 컴파일된 모듈 캐시를 만듭니다.

증분식 사전 컴파일된(변경될 때마다 변경된 부분과 관련된 부분을 컴파일하는) 모듈 파일은 importusing 을 사용하여 모듈을 로드할 때 자동으로 생성되고 사용됩니다. 이렇게 하면 처음 import 될 때 자동으로 컴파일됩니다. 또는 Base.compilecache(modulename)를 수동으로 호출할 수 있습니다. 결과 캐시 파일은 DEPOT_PATH[1]/compiled/에 저장됩니다. 결과적으로 모듈은 의존성이 변경될 때마다 using 혹은 import 될 때 자동으로 다시 컴파일됩니다; 의존성은 모듈이 임포트 하는, Julia 의 빌드, 포함하는 파일 또는 모듈 파일의 include_dependency(path)[https://docs.julialang.org/en/v1/base/base/#Base.include_dependency]에 의해 선언된 명시적 의존성을 말합니다.

파일 의존성의 경우 include 에 의해 로드되거나 include_dependency 에 의해 명시적으로 추가된 각각 파일의 수정 시간(mtime)이 변경되지 않았는지, 또는 (1초 미만의 정확도로 mtime 복사할 수 없는 시스템도 수용하기 위해) 가장 가까운 초로 잘린 수정 시간과 같은지 여부를 검사하여 변경이 결정됩니다. 또한 require 에서의 탐색 로직이 선택한 파일 경로가 사전 컴파일 파일을 만든 경로와 일치하는지 여부도 고려합니다. 또한 현재 프로세스에 이미 로드된 의존성 집합을 고려하고 실행 중인 시스템과 사전 컴파일 캐시 사이에 비호환성 생성을 방지하기 위해 파일이 변경되거나 사라지더라도 해당 모듈을 다시 컴파일하지 않습니다.

모듈이 미리 컴파일하기에 안전하지 않다는 것을 알고 있는 경우(예를 들어 아래에 설명된 이유 중 하나로 인해) 모듈 파일에 __precompile__(false)를 넣어야 합니다(보통은 맨 위에 넣습니다). 이로 인해 Base.compilecache 에서 오류가 발생하고 using / import 가 현재 프로세스에 직접 로드하고 사전 컴파일 및 캐싱을 건너뜁니다. 이것은 또한 다른 미리 컴파일된 모듈에서 모듈을 가져오는 것을 방지합니다.

모듈을 작성할 때 주의가 필요할 수 있는 증분 공유 라이브러리 생성에 내재된 특정 동작을 알고 있어야 할 수도 있습니다. 예를 들어 외부 상태는 보존되지 않습니다. 이를 수용하려면 컴파일 시간에 발생할 수 있는 단계에서 런타임에 발생해야 하는 초기화 단계를 명시적으로 분리하십시오. 이를 위해 Julia는 런타임에 발생해야 하는 모든 초기화 단계를 실행하는 모듈에서 __init__() 함수를 정의할 수 있도록 합니다. 이 함수는 컴파일 중에 호출되지 않습니다(--output-*). 사실상 코드 수명 동안 정확히 한 번 실행될 것이라고 가정할 수 있습니다. 물론 필요한 경우 수동으로 호출할 수 있지만 기본적으로 이 함수가 컴파일된 이미지에 캡처될 필요가 없거나 캡처되어서는 안 되는 로컬 컴퓨터의 컴퓨팅 상태를 처리한다고 가정하는 것입니다. 증분 컴파일(--output-incremental=yes)에 로드되는 경우를 포함하여 모듈이 프로세스에 로드된 후에 호출되지만 전체 컴파일 프로세스에 로드되는 경우에는 호출되지 않습니다.

특히 모듈에 __init__() 함수를 정의하면 Julia는 모듈이 로드된 직후(예: import, using 또는 require 에 의해) 런타임에 처음으로 __init__() 를 호출합니다(즉, __init__ 은 한 번만 호출되고 모듈의 모든 명령문이 실행된 후에만 호출됨). 모듈이 완전히 가져온 후에 호출되기 때문에 모든 하위 모듈이나 다른 가져온 모듈에는 둘러싸는 모듈의 __init__ 전에 호출되는 __init__ 함수가 있습니다.

__init__ 의 두 가지 일반적인 용도는 외부 C 라이브러리의 런타임 초기화 함수를 호출하고 외부 라이브러리에서 반환된 포인터를 포함하는 전역 상수를 초기화하는 것입니다. 예를 들어 런타임에 foo_init() 초기화 함수를 호출해야 하는 C 라이브러리 libfoo를 호출한다고 가정합니다. libfoo 에 의해 정의된 void *foo_data() 함수의 반환 값을 보유하는 전역 상수 foo_data_ptr도 정의하고 싶다고 가정합니다. 포인터 주소가 실행마다 변경되기 때문에 이 상수는 런타임(컴파일 시간이 아님)에 초기화되어야 합니다. 모듈에서 다음 __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__ 에서 초기화되어야 합니다.

ccall에 의해 생성되지 않는 대부분의 Julia 개체와 관련된 상수는 __init__ 에 배치할 필요가 없습니다. 해당 정의는 캐시된 모듈 이미지에서 미리 컴파일하고 로드할 수 있습니다. 여기에는 배열과 같은 복잡한 힙 할당 개체가 포함됩니다. 그러나 사전 컴파일이 작동하려면 원시 포인터 값을 반환하는 모든 루틴을 런타임에 호출해야 합니다(Ptr 개체는 isbits 개체 내부에 숨겨져 있지 않는 한 null 포인터로 바뀝니다). 여기에는 Julia 함수 @cfunction포인터(pointer)의 반환 값이 포함됩니다.

사전(dictionary) 및 세트(set) 타입 또는 일반적으로 hash(key) 메서드의 출력에 의존하는 모든 것은 까다로운 경우입니다. 키가 숫자, 문자열, 기호, 범위, Expr 또는 이러한 타입의 조합(배열, 튜플, 집합, 쌍 등을 통해)인 일반적인 경우에는 미리 컴파일해도 안전합니다. 그러나 Function 또는 DataType 과 같은 몇 가지 다른 키 유형과 해시 메서드를 정의하지 않은 일반 사용자 정의 유형의 경우 대체 해시 메서드는 개체의 메모리 주소(objectid를 통해)에 따라 달라지므로 실행에서 실행으로 변경합니다. 이러한 키 유형 중 하나가 있거나 확실하지 않은 경우 안전을 위해 __init__함수 내에서 이 사전을 초기화할 수 있습니다. 또는 컴파일 타임에 초기화하는 것이 안전하도록 사전 컴파일에서 특별히 처리되는 IdDict 사전 타입을 사용할 수 있습니다.

사전 컴파일을 사용하는 경우 컴파일 단계와 실행 단계를 명확하게 구분하는 것이 중요합니다. 이 모드에서는 Julia가 컴파일된 코드를 생성하는 독립 실행형 인터프리터가 아니라 임의의 Julia 코드 실행을 허용하는 컴파일러라는 것이 훨씬 더 분명해집니다.

다른 알려진 잠재적 오류 시나리오는 다음과 같습니다.

  1. 전역 카운터(예: 개체를 고유하게 식별하려는 경우). 다음 코드 스니펫을 생각해 봅시다.

    mutable struct UniquedById
        myid::Int
        let counter = 0
            UniquedById() = new(counter += 1)
        end
    end

    이 코드의 의도는 모든 인스턴스에 고유한 ID를 부여하는 것이었지만 카운터 값은 컴파일이 끝날 때 기록됩니다. 증분 컴파일된 이 모듈의 모든 후속 사용은 동일한 카운터 값에서 시작됩니다.

    objectid(메모리 포인터를 해싱하여 작동)에는 유사한 문제가 있습니다(아래 Dict 사용에 대한 참고 사항 참조).

    한 가지 대안은 매크로를 사용하여 @__MODULE__ 을 캡처하고 현재 카운터 값과 함께 단독으로 저장하는 것이지만 이 전역 상태에 의존하지 않도록 코드를 재설계하는 것이 더 나을 수 있습니다.

  2. 연관 컬렉션(예: DictSet)은 __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. __init__() 이 시작된 후 로컬 범위의 global const 문(이에 대한 오류 추가 계획은 Issue #12010 참조)
  3. 증분 프리컴파일을 수행하는 동안 모듈 교체는 런타임 오류입니다.

알아야 할 몇 가지 다른 사항:

  1. 소스 파일 자체가 변경된 후(Pkg.update 포함) 코드의 리로드/캐시 무효화가 수행되지 않으며 Pkg.rm 이후에는 cleanup 이 수행되지 않습니다.

  2. reshape 된 배열의 메모리 공유 행위는 사전 컴파일에서 무시됩니다(각 뷰는 자체 복사본을 얻습니다).

  3. 컴파일 타임과 런타임 사이에 파일 시스템이 변경되지 않을 것으로 예상합니다. 예를 들면 런타임에 리소스를 찾기 위한 @__FILE__/source_path() 혹은 BinDeps 의 @checked_lib 매크로가 있습니다. 때때로 이것은 불가피합니다. 그러나 가능하면 런타임에 리소스를 찾을 필요가 없도록 컴파일 타임에 리소스를 모듈에 복사하는 것이 좋습니다.

  4. WeakRef 개체 및 종료자는 현재 serializer에서 제대로 처리되지 않습니다(이 문제는 다음 릴리스에서 수정될 예정임).

  5. 일반적으로 Method, MethodInstance, MethodTable, TypeMapLevel, TypeMapEntry 와 같은 내부적인 메타데이터 개체 및 이러한 개체의 필드 인스턴스에 대한 참조 캡처를 피하는 것이 가장 좋습니다. 이렇게 하면 serializer 를 혼란스럽게 하여 원하는 결과를 얻지 못할 수 있습니다. 이렇게 하는 것이 반드시 오류는 아니며, 시스템이 이들 중 일부를 복사하고 다른 것의 고유한 단일 인스턴스를 생성하려고 시도할 준비가 되어 있으면 됩니다.

증분 사전 컴파일을 해제하는 것이 모듈 개발 중에 도움이 되는 경우가 있습니다. 명령줄 플래그 --compiled-modules={yes|no} 를 사용하면 모듈 사전 컴파일을 켜거나 끌 수 있습니다. --compiled-modules=no 로 Julia를 시작하면 모듈 및 모듈 의존성을 로드할 때 컴파일 캐시의 직렬화된 모듈이 무시됩니다. Base.compilecache는 여전히 수동으로 호출할 수 있습니다. 이 명령줄 플래그의 상태는 Pkg.build로 전달되어 패키지를 설치, 업데이트 및 명시적으로 빌드할 때 자동 사전 컴파일 트리거를 비활성화합니다.

profile
Julia, Python, JS;Physics, Math, Image processing

0개의 댓글