Modules
Les modules en Julia aident à organiser le code en unités cohérentes. Ils sont délimités syntaxiquement à l'intérieur de module NomDuModule ... end
, et ont les caractéristiques suivantes :
Les modules sont des espaces de noms séparés, chacun introduisant un nouvel espace global. Cela est utile, car cela permet d'utiliser le même nom pour différentes fonctions ou variables globales sans conflit, tant qu'elles se trouvent dans des modules séparés.
Les modules disposent de fonctionnalités pour une gestion détaillée des espaces de noms : chacun définit un ensemble de noms qu'il
exporte
et marque commepublic
, et peut importer des noms d'autres modules avecusing
etimport
(nous expliquons cela ci-dessous).Les modules peuvent être précompilés pour un chargement plus rapide et peuvent contenir du code pour l'initialisation à l'exécution.
Typiquement, dans les plus grands paquets Julia, vous verrez le code du module organisé en fichiers, par exemple
module SomeModule
# export, public, using, import statements are usually here; we discuss these below
include("file1.jl")
include("file2.jl")
end
Les fichiers et les noms de fichiers sont principalement sans rapport avec les modules ; les modules ne sont associés qu'aux expressions de module. On peut avoir plusieurs fichiers par module, et plusieurs modules par fichier. include
se comporte comme si le contenu du fichier source était évalué dans le scope global du module incluant. Dans ce chapitre, nous utilisons des exemples courts et simplifiés, donc nous n'utiliserons pas include
.
Le style recommandé est de ne pas indenter le corps du module, car cela conduirait généralement à l'indentation de fichiers entiers. De plus, il est courant d'utiliser UpperCamelCase
pour les noms de modules (tout comme pour les types) et d'utiliser la forme plurielle si applicable, surtout si le module contient un identifiant de nom similaire, afin d'éviter les conflits de noms. Par exemple,
module FastThings
struct FastThing
...
end
end
Namespace management
La gestion des espaces de noms fait référence aux fonctionnalités que le langage offre pour rendre les noms d'un module disponibles dans d'autres modules. Nous discutons ci-dessous des concepts et des fonctionnalités connexes en détail.
Qualified names
Les noms des fonctions, des variables et des types dans l'espace global, comme sin
, ARGS
et UnitRange
, appartiennent toujours à un module, appelé le module parent, qui peut être trouvé de manière interactive avec parentmodule
, par exemple.
julia> parentmodule(UnitRange)
Base
On peut également se référer à ces noms en dehors de leur module parent en les préfixant avec leur module, par exemple Base.UnitRange
. Cela s'appelle un nom qualifié. Le module parent peut être accessible en utilisant une chaîne de sous-modules comme Base.Math.sin
, où Base.Math
est appelé le chemin du module. En raison d'ambiguïtés syntaxiques, qualifier un nom qui ne contient que des symboles, comme un opérateur, nécessite d'insérer un deux-points, par exemple Base.:+
. Un petit nombre d'opérateurs nécessite également des parenthèses, par exemple Base.:(==)
.
Si un nom est qualifié, alors il est toujours accessible, et dans le cas d'une fonction, il peut également avoir des méthodes ajoutées en utilisant le nom qualifié comme nom de fonction.
Dans un module, un nom de variable peut être "réservé" sans lui assigner une valeur en le déclarant comme global x
. Cela empêche les conflits de noms pour les variables globales initialisées après le temps de chargement. La syntaxe M.x = y
ne fonctionne pas pour assigner une variable globale dans un autre module ; l'assignation globale est toujours locale au module.
Export lists
Les noms (se référant aux fonctions, types, variables globales et constantes) peuvent être ajoutés à la liste d'exportation d'un module avec export
: ce sont les symboles qui sont importés lors de l'utilisation du module. En général, ils se trouvent en haut ou près du début de la définition du module afin que les lecteurs du code source puissent les trouver facilement, comme dans
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;
mais ceci n'est qu'une suggestion de style — un module peut avoir plusieurs déclarations export
à des emplacements arbitraires.
Il est courant d'exporter des noms qui font partie de l'API (interface de programmation d'application). Dans le code ci-dessus, la liste d'exportation suggère que les utilisateurs devraient utiliser nice
et DOG
. Cependant, puisque les noms qualifiés rendent toujours les identifiants accessibles, cela n'est qu'une option pour organiser les API : contrairement à d'autres langages, Julia n'a pas de moyens pour cacher véritablement les internes des modules.
Aussi, certains modules n'exportent pas de noms du tout. Cela est généralement fait s'ils utilisent des mots courants, tels que dérivée
, dans leur API, ce qui pourrait facilement entrer en conflit avec les listes d'exportation d'autres modules. Nous verrons comment gérer les conflits de noms ci-dessous.
Pour marquer un nom comme public sans l'exporter dans l'espace de noms des personnes qui appellent using NiceStuff
, on peut utiliser public
au lieu de export
. Cela marque le(s) nom(s) public(s) comme faisant partie de l'API publique, mais n'a pas d'implications sur l'espace de noms. Le mot-clé public
n'est disponible qu'à partir de Julia 1.11. Pour maintenir la compatibilité avec Julia 1.10 et les versions antérieures, utilisez la macro @compat
du package Compat.
Standalone using
and import
Pour une utilisation interactive, la manière la plus courante de charger un module est using ModuleName
. Cela loads le code associé à ModuleName
, et apporte
le nom du module
et les éléments de la liste d'exportation dans l'espace de noms global environnant.
Techniquement, l'instruction using ModuleName
signifie qu'un module appelé ModuleName
sera disponible pour résoudre les noms au besoin. Lorsqu'une variable globale est rencontrée et qu'elle n'a pas de définition dans le module actuel, le système la recherchera parmi les variables exportées par ModuleName
et l'utilisera si elle y est trouvée. Cela signifie que toutes les utilisations de cette variable globale dans le module actuel se résoudront à la définition de cette variable dans ModuleName
.
Pour charger un module à partir d'un package, l'instruction using ModuleName
peut être utilisée. Pour charger un module à partir d'un module défini localement, un point doit être ajouté avant le nom du module comme using .ModuleName
.
Pour continuer avec notre exemple,
julia> using .NiceStuff
charger le code ci-dessus, rendant NiceStuff
(le nom du module), DOG
et nice
disponibles. Dog
n'est pas sur la liste des exportations, mais il peut être accessible si le nom est qualifié avec le chemin du module (qui ici est juste le nom du module) sous la forme NiceStuff.Dog
.
Il est important de noter que using ModuleName
est la seule forme pour laquelle les listes d'exportation ont de l'importance.
En revanche,
julia> import .NiceStuff
apporte uniquement le nom du module dans le scope. Les utilisateurs devront utiliser NiceStuff.DOG
, NiceStuff.Dog
et NiceStuff.nice
pour accéder à son contenu. En général, import ModuleName
est utilisé dans des contextes où l'utilisateur souhaite garder l'espace de noms propre. Comme nous le verrons dans la section suivante, import .NiceStuff
est équivalent à using .NiceStuff: NiceStuff
.
Vous pouvez combiner plusieurs déclarations using
et import
du même type dans une expression séparée par des virgules, par exemple.
julia> using LinearAlgebra, Random
using
and import
with specific identifiers, and adding methods
Lorsque using ModuleName:
ou import ModuleName:
est suivi d'une liste de noms séparés par des virgules, le module est chargé, mais seuls ces noms spécifiques sont intégrés dans l'espace de noms par l'instruction. Par exemple,
julia> using .NiceStuff: nice, DOG
importer les noms nice
et DOG
.
Il est important de noter que le nom du module NiceStuff
ne sera pas dans l'espace de noms. Si vous souhaitez le rendre accessible, vous devez le lister explicitement, comme
julia> using .NiceStuff: nice, DOG, NiceStuff
Lorsque deux ou plusieurs packages/modules exportent un nom et que ce nom ne fait pas référence à la même chose dans chacun des packages, et que les packages sont chargés via using
sans une liste explicite de noms, il est alors erroné de référencer ce nom sans qualification. Il est donc recommandé que le code destiné à être compatible avec les futures versions de ses dépendances et de Julia, par exemple, le code dans les packages publiés, liste les noms qu'il utilise de chaque package chargé, par exemple, using Foo: Foo, f
plutôt que using Foo
.
Julia a deux formes pour apparemment la même chose car seul import ModuleName: f
permet d'ajouter des méthodes à f
sans un chemin de module. C'est-à-dire que l'exemple suivant donnera une erreur :
julia> using .NiceStuff: nice
julia> struct Cat end
julia> nice(::Cat) = "nice 😸"
ERROR: invalid method definition in Main: function NiceStuff.nice must be explicitly imported to be extended
Stacktrace:
[1] top-level scope
@ none:0
[2] top-level scope
@ none:1
Cette erreur empêche d'ajouter accidentellement des méthodes à des fonctions dans d'autres modules que vous aviez seulement l'intention d'utiliser.
Il y a deux façons de traiter cela. Vous pouvez toujours qualifier les noms de fonction avec un chemin de module :
julia> using .NiceStuff
julia> struct Cat end
julia> NiceStuff.nice(::Cat) = "nice 😸"
Alternativement, vous pouvez importer
le nom de la fonction spécifique :
julia> import .NiceStuff: nice
julia> struct Cat end
julia> nice(::Cat) = "nice 😸"
nice (generic function with 2 methods)
Le choix que vous faites est une question de style. La première forme rend clair que vous ajoutez une méthode à une fonction dans un autre module (rappelez-vous que les imports et la définition de la méthode peuvent être dans des fichiers séparés), tandis que la seconde est plus courte, ce qui est particulièrement pratique si vous définissez plusieurs méthodes.
Une fois qu'une variable est rendue visible via using
ou import
, un module ne peut pas créer sa propre variable avec le même nom. Les variables importées sont en lecture seule ; l'assignation à une variable globale affecte toujours une variable appartenant au module actuel, sinon cela soulève une erreur.
Renaming with as
Un identifiant introduit dans le scope par import
ou using
peut être renommé avec le mot-clé as
. Cela est utile pour contourner les conflits de noms ainsi que pour raccourcir les noms. Par exemple, Base
exporte le nom de fonction read
, mais le package CSV.jl fournit également CSV.read
. Si nous allons invoquer la lecture CSV plusieurs fois, il serait pratique de supprimer le qualificateur CSV.
. Mais alors, il est ambigu de savoir si nous faisons référence à Base.read
ou CSV.read
:
julia> read;
julia> import CSV: read
WARNING: ignoring conflicting import of CSV.read into Main
Renommer offre une solution :
julia> import CSV: read as rd
Les packages importés eux-mêmes peuvent également être renommés :
import BenchmarkTools as BT
as
fonctionne avec using
uniquement lorsqu'un seul identifiant est mis en portée. Par exemple, using CSV: read as rd
fonctionne, mais using CSV as C
ne fonctionne pas, car cela opère sur tous les noms exportés dans CSV
.
Mixing multiple using
and import
statements
Lorsque plusieurs déclarations using
ou import
de l'une des formes ci-dessus sont utilisées, leur effet est combiné dans l'ordre dans lequel elles apparaissent. Par exemple,
julia> using .NiceStuff # exported names and the module name
julia> import .NiceStuff: nice # allows adding methods to unqualified functions
apporterait tous les noms exportés de NiceStuff
et le nom du module lui-même dans le scope, et permettrait également d'ajouter des méthodes à nice
sans le préfixer avec un nom de module.
Handling name conflicts
Considérez la situation où deux (ou plusieurs) packages exportent le même nom, comme dans
julia> module A
export f
f() = 1
end
A
julia> module B
export f
f() = 2
end
B
L'instruction using .A, .B
fonctionne, mais lorsque vous essayez d'appeler f
, vous obtenez une erreur avec un indice.
julia> using .A, .B
julia> f
ERROR: UndefVarError: `f` not defined in `Main`
Hint: It looks like two or more modules export different bindings with this name, resulting in ambiguity. Try explicitly importing it from a particular module, or qualifying the name with the module it should come from.
Ici, Julia ne peut pas décider de quel f
vous parlez, donc vous devez faire un choix. Les solutions suivantes sont couramment utilisées :
Procédez simplement avec des noms qualifiés comme
A.f
etB.f
. Cela rend le contexte clair pour le lecteur de votre code, surtout sif
coïncide par hasard mais a une signification différente dans divers packages. Par exemple,degré
a diverses utilisations en mathématiques, dans les sciences naturelles et dans la vie quotidienne, et ces significations doivent être gardées séparées.Utilisez le mot-clé
as
ci-dessus pour renommer un ou les deux identifiants, par exemplejulia> using .A: f as f julia> using .B: f as g
rendrait
B.f
disponible sous le nomg
. Ici, nous supposons que vous n'avez pas utiliséusing A
auparavant, ce qui aurait amenéf
dans l'espace de noms.Lorsque les noms en question partagent un sens, il est courant qu'un module l'importe d'un autre, ou qu'il ait un paquet "de base" léger ayant pour seule fonction de définir une interface comme celle-ci, qui peut être utilisée par d'autres paquets. Il est conventionnel que de tels noms de paquets se terminent par
...Base
(ce qui n'a rien à voir avec le moduleBase
de Julia).
Default top-level definitions and bare modules
Les modules contiennent automatiquement using Core
, using Base
, et les définitions des fonctions eval
et include
, qui évaluent des expressions/fichiers dans le scope global de ce module.
Si ces définitions par défaut ne sont pas souhaitées, des modules peuvent être définis en utilisant le mot-clé baremodule
à la place (note : Core
est toujours importé). En termes de baremodule
, un module
standard ressemble à ceci :
baremodule Mod
using Base
eval(x) = Core.eval(Mod, x)
include(p) = Base.include(Mod, p)
...
end
Si même Core
n'est pas souhaité, un module qui n'importe rien et ne définit aucun nom peut être défini avec Module(:YourNameHere, false, false)
et du code peut y être évalué avec @eval
ou Core.eval
:
julia> arithmetic = Module(:arithmetic, false, false)
Main.arithmetic
julia> @eval arithmetic add(x, y) = $(+)(x, y)
add (generic function with 1 method)
julia> arithmetic.add(12, 13)
25
Standard modules
Il y a trois modules standards importants :
Core
contient toutes les fonctionnalités "intégrées" dans le langage.Base
contient des fonctionnalités de base qui sont utiles dans presque tous les cas.Main
est le module de niveau supérieur et le module actuel, lorsque Julia est démarré.
Par défaut, Julia est livré avec certains modules de bibliothèque standard. Ceux-ci se comportent comme des paquets Julia réguliers, sauf que vous n'avez pas besoin de les installer explicitement. Par exemple, si vous souhaitez effectuer des tests unitaires, vous pouvez charger la bibliothèque standard Test
comme suit :
using Test
Submodules and relative paths
Les modules peuvent contenir des sous-modules, en imbriquant la même syntaxe module ... end
. Ils peuvent être utilisés pour introduire des espaces de noms séparés, ce qui peut être utile pour organiser des bases de code complexes. Notez que chaque module
introduit son propre scope, donc les sous-modules n'héritent pas automatiquement des noms de leur parent.
Il est recommandé que les sous-modules se réfèrent à d'autres modules au sein du module parent englobant (y compris ce dernier) en utilisant des qualificateurs de module relatifs dans les déclarations using
et import
. Un qualificateur de module relatif commence par un point (.
), qui correspond au module actuel, et chaque .
successif mène au parent du module actuel. Cela doit être suivi des modules si nécessaire, et finalement du nom réel à accéder, tous séparés par des .
.
Considérez l'exemple suivant, où le sous-module SubA
définit une fonction, qui est ensuite étendue dans son module "frère" :
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;
Vous pouvez voir du code dans des packages, qui, dans une situation similaire, utilise
julia> import .ParentModule.SubA: add_D
Cependant, cela fonctionne via code loading, et donc ne fonctionne que si ParentModule
est dans un package. Il est préférable d'utiliser des chemins relatifs.
Notez que l'ordre des définitions est également important si vous évaluez des valeurs. Considérez
module TestPackage
export x, y
x = 0
module Sub
using ..TestPackage
z = y # ERROR: UndefVarError: `y` not defined in `Main`
end
y = 1
end
où Sub
essaie d'utiliser TestPackage.y
avant qu'il ne soit défini, donc il n'a pas de valeur.
Pour des raisons similaires, vous ne pouvez pas utiliser un ordre cyclique :
module A
module B
using ..C # ERROR: UndefVarError: `C` not defined in `Main.A`
end
module C
using ..B
end
end
Module initialization and precompilation
Les grands modules peuvent prendre plusieurs secondes à charger car l'exécution de toutes les instructions d'un module implique souvent de compiler une grande quantité de code. Julia crée des caches précompilés du module pour réduire ce temps.
Les fichiers de module précompilés (parfois appelés "fichiers de cache") sont créés et utilisés automatiquement lorsque import
ou using
charge un module. Si le(s) fichier(s) de cache n'existent pas encore, le module sera compilé et enregistré pour une réutilisation future. Vous pouvez également appeler manuellement Base.compilecache(Base.identify_package("modulename"))
pour créer ces fichiers sans charger le module. Les fichiers de cache résultants seront stockés dans le sous-dossier compiled
de DEPOT_PATH[1]
. Si rien ne change dans votre système, ces fichiers de cache seront utilisés lorsque vous chargerez le module avec import
ou using
.
Les fichiers de cache de précompilation stockent les définitions de modules, de types, de méthodes et de constantes. Ils peuvent également stocker des spécialisations de méthodes et le code généré pour celles-ci, mais cela nécessite généralement que le développeur ajoute des directives explicites precompile
ou exécute des charges de travail qui forcent la compilation pendant la construction du package.
Cependant, si vous mettez à jour les dépendances du module ou modifiez son code source, le module est automatiquement recompilé lors de l'utilisation de using
ou import
. Les dépendances sont des modules qu'il importe, la construction Julia, les fichiers qu'il inclut, ou des dépendances explicites déclarées par include_dependency(path)
dans le(s) fichier(s) du module.
Pour les dépendances de fichiers chargées par include
, un changement est déterminé en examinant si la taille du fichier (fsize
) ou le contenu (condensé en un hachage) est inchangé. Pour les dépendances de fichiers chargées par include_dependency
, un changement est déterminé en examinant si le temps de modification (mtime
) est inchangé, ou égal au temps de modification tronqué à la seconde la plus proche (pour tenir compte des systèmes qui ne peuvent pas copier le mtime avec une précision inférieure à la seconde). Il prend également en compte si le chemin vers le fichier choisi par la logique de recherche dans require
correspond au chemin qui avait créé le fichier de précompilation. Il prend également en compte l'ensemble des dépendances déjà chargées dans le processus actuel et ne recompilera pas ces modules, même si leurs fichiers changent ou disparaissent, afin d'éviter de créer des incompatibilités entre le système en cours d'exécution et le cache de précompilation. Enfin, il prend en compte les changements dans tout compile-time preferences.
Si vous savez qu'un module n'est pas sûr à précompiler (par exemple, pour l'une des raisons décrites ci-dessous), vous devez mettre __precompile__(false)
dans le fichier du module (généralement placé en haut). Cela fera en sorte que Base.compilecache
génère une erreur et que using
/ import
le charge directement dans le processus actuel, en sautant la précompilation et la mise en cache. Cela empêche également le module d'être importé par tout autre module précompilé.
Vous devez être conscient de certains comportements inhérents à la création de bibliothèques partagées incrémentales qui peuvent nécessiter de la prudence lors de l'écriture de votre module. Par exemple, l'état externe n'est pas préservé. Pour y remédier, séparez explicitement les étapes d'initialisation qui doivent se produire à l'exécution des étapes qui peuvent se produire à la compilation. À cette fin, Julia vous permet de définir une fonction __init__()
dans votre module qui exécute toutes les étapes d'initialisation qui doivent se produire à l'exécution. Cette fonction ne sera pas appelée lors de la compilation (--output-*
). En effet, vous pouvez supposer qu'elle sera exécutée exactement une fois dans la durée de vie du code. Vous pouvez, bien sûr, l'appeler manuellement si nécessaire, mais la norme est de supposer que cette fonction traite de l'état de calcul pour la machine locale, qui n'a pas besoin d'être – ou même ne devrait pas être – capturée dans l'image compilée. Elle sera appelée après le chargement du module dans un processus, y compris si elle est chargée dans une compilation incrémentale (--output-incremental=yes
), mais pas si elle est chargée dans un processus de compilation complète.
En particulier, si vous définissez une function __init__()
dans un module, alors Julia appellera __init__()
immédiatement après le chargement du module (par exemple, par import
, using
ou require
) à l'exécution pour la première fois (c'est-à-dire que __init__
n'est appelé qu'une seule fois, et seulement après que toutes les instructions du module ont été exécutées). Comme il est appelé après que le module a été entièrement importé, toutes les sous-modules ou autres modules importés ont leurs fonctions __init__
appelées avant le __init__
du module englobant.
Deux utilisations typiques de __init__
consistent à appeler des fonctions d'initialisation à l'exécution de bibliothèques C externes et à initialiser des constantes globales qui impliquent des pointeurs retournés par des bibliothèques externes. Par exemple, supposons que nous appelons une bibliothèque C libfoo
qui nécessite que nous appelions une fonction d'initialisation foo_init()
à l'exécution. Supposons également que nous souhaitions définir une constante globale foo_data_ptr
qui contient la valeur de retour d'une fonction void *foo_data()
définie par libfoo
– cette constante doit être initialisée à l'exécution (et non à la compilation) car l'adresse du pointeur changera d'une exécution à l'autre. Vous pourriez accomplir cela en définissant la fonction __init__
suivante dans votre module :
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
Remarquez qu'il est parfaitement possible de définir une variable globale à l'intérieur d'une fonction comme __init__
; c'est l'un des avantages d'utiliser un langage dynamique. Mais en en faisant une constante au niveau global, nous pouvons garantir que le type est connu du compilateur et lui permettre de générer un code mieux optimisé. Évidemment, toutes les autres variables globales de votre module qui dépendent de foo_data_ptr
devraient également être initialisées dans __init__
.
Les constantes impliquant la plupart des objets Julia qui ne sont pas produits par ccall
n'ont pas besoin d'être placées dans __init__
: leurs définitions peuvent être précompilées et chargées à partir de l'image de module mise en cache. Cela inclut des objets compliqués alloués sur le tas comme les tableaux. Cependant, toute routine qui renvoie une valeur de pointeur brut doit être appelée à l'exécution pour que la précompilation fonctionne (Ptr
les objets se transformeront en pointeurs nuls à moins qu'ils ne soient cachés à l'intérieur d'un objet isbits
). Cela inclut les valeurs de retour des fonctions Julia @cfunction
et pointer
.
Les types de dictionnaires et d'ensembles, ou en général tout ce qui dépend de la sortie d'une méthode hash(key)
, sont un cas plus délicat. Dans le cas courant où les clés sont des nombres, des chaînes de caractères, des symboles, des plages, Expr
, ou des compositions de ces types (via des tableaux, des tuples, des ensembles, des paires, etc.), elles sont sûres à précompiler. Cependant, pour quelques autres types de clés, tels que Function
ou DataType
et des types définis par l'utilisateur génériques où vous n'avez pas défini de méthode hash
, la méthode hash
par défaut dépend de l'adresse mémoire de l'objet (via son objectid
) et peut donc changer d'une exécution à l'autre. Si vous avez l'un de ces types de clés, ou si vous n'êtes pas sûr, pour être prudent, vous pouvez initialiser ce dictionnaire depuis votre fonction __init__
. Alternativement, vous pouvez utiliser le type de dictionnaire IdDict
, qui est spécialement géré par la précompilation afin qu'il soit sûr à initialiser au moment de la compilation.
Lors de l'utilisation de la précompilation, il est important de garder une distinction claire entre la phase de compilation et la phase d'exécution. Dans ce mode, il sera souvent beaucoup plus évident que Julia est un compilateur qui permet l'exécution de code Julia arbitraire, et non un interpréteur autonome qui génère également du code compilé.
D'autres scénarios de défaillance potentiels connus incluent :
Compteurs globaux (par exemple, pour tenter d'identifier de manière unique des objets). Considérez le code suivant :
mutable struct UniquedById myid::Int let counter = 0 UniquedById() = new(counter += 1) end end
bien que l'intention de ce code était de donner à chaque instance un identifiant unique, la valeur du compteur est enregistrée à la fin de la compilation. Tous les usages ultérieurs de ce module compilé de manière incrémentale commenceront à partir de cette même valeur de compteur.
Notez que
objectid
(qui fonctionne en hachant le pointeur mémoire) présente des problèmes similaires (voir les notes sur l'utilisation deDict
ci-dessous).Une alternative consiste à utiliser une macro pour capturer
@__MODULE__
et la stocker seule avec la valeur actuelle decounter
, cependant, il peut être préférable de repenser le code pour ne pas dépendre de cet état global.Les collections associatives (telles que
Dict
etSet
) doivent être re-hachées dans__init__
. (À l'avenir, un mécanisme pourrait être fourni pour enregistrer une fonction d'initialisation.)En fonction des effets secondaires à la compilation persistant à travers le temps de chargement. Les exemples incluent : la modification de tableaux ou d'autres variables dans d'autres modules Julia ; le maintien de poignées pour des fichiers ou des dispositifs ouverts ; le stockage de pointeurs vers d'autres ressources système (y compris la mémoire) ;
Créer des "copies" accidentelles de l'état global d'un autre module, en le référencant directement au lieu de passer par son chemin de recherche. Par exemple, (dans le scope global) :
#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 =#
Plusieurs restrictions supplémentaires sont imposées sur les opérations qui peuvent être effectuées lors de la précompilation du code pour aider l'utilisateur à éviter d'autres situations de comportement incorrect :
- Appel de
eval
pour provoquer un effet secondaire dans un autre module. Cela entraînera également l'émission d'un avertissement lorsque le drapeau de précompilation incrémentale est activé. global const
déclarations depuis le scope local après le début de__init__()
(voir l'issue #12010 pour les plans d'ajout d'une erreur pour cela)- Remplacer un module est une erreur d'exécution lors d'une précompilation incrémentale.
Quelques autres points à garder à l'esprit :
- Aucune rechargement de code / invalidation de cache n'est effectué après que des modifications ont été apportées aux fichiers source eux-mêmes (y compris par
Pkg.update
), et aucun nettoyage n'est effectué aprèsPkg.rm
. - Le comportement de partage de mémoire d'un tableau remodelé est ignoré par la précompilation (chaque vue obtient sa propre copie)
- S'attendre à ce que le système de fichiers reste inchangé entre le temps de compilation et le temps d'exécution, par exemple
@__FILE__
/source_path()
pour trouver des ressources à l'exécution, ou le macro@checked_lib
de BinDeps. Parfois, cela est inévitable. Cependant, lorsque cela est possible, il peut être judicieux de copier les ressources dans le module au moment de la compilation afin qu'elles n'aient pas besoin d'être trouvées à l'exécution. - Les objets
WeakRef
et les finalizers ne sont actuellement pas gérés correctement par le sérialiseur (cela sera corrigé dans une prochaine version). - Il est généralement préférable d'éviter de capturer des références à des instances d'objets de métadonnées internes tels que
Method
,MethodInstance
,MethodTable
,TypeMapLevel
,TypeMapEntry
et les champs de ces objets, car cela peut confondre le sérialiseur et ne pas conduire au résultat souhaité. Ce n'est pas nécessairement une erreur de le faire, mais vous devez simplement être préparé à ce que le système essaie de copier certains d'entre eux et de créer une instance unique pour d'autres.
Il est parfois utile, lors du développement de modules, de désactiver la précompilation incrémentielle. Le drapeau de ligne de commande --compiled-modules={yes|no|existing}
vous permet d'activer ou de désactiver la précompilation des modules. Lorsque Julia est démarré avec --compiled-modules=no
, les modules sérialisés dans le cache de compilation sont ignorés lors du chargement des modules et des dépendances de modules. Dans certains cas, vous pouvez vouloir charger des modules précompilés existants, mais ne pas en créer de nouveaux. Cela peut être fait en démarrant Julia avec --compiled-modules=existing
. Un contrôle plus fin est disponible avec --pkgimages={yes|no|existing}
, qui n'affecte que le stockage du code natif lors de la précompilation. Base.compilecache
peut toujours être appelé manuellement. L'état de ce drapeau de ligne de commande est transmis à Pkg.build
pour désactiver le déclenchement automatique de la précompilation lors de l'installation, de la mise à jour et de la construction explicite de packages.
Vous pouvez également déboguer certaines erreurs de précompilation avec des variables d'environnement. Définir JULIA_VERBOSE_LINKING=true
peut aider à résoudre les échecs de liaison des bibliothèques partagées de code natif compilé. Consultez la partie Documentation des développeurs du manuel Julia, où vous trouverez plus de détails dans la section documentant les internals de Julia sous "Images de paquets".