Embrasser Julia Une lettre d’invitation
Embrasser Julia Une lettre d'invitation irrésistible
Chaleureusement étendu aux amoureux de Python, aux magiciens du calcul scientifique et aux scientifiques des données
Julia est un langage de programmation généraliste, dynamique, performant et de haut niveau, compilé en temps réel. C’est un langage assez récent avec sa version majeure 1.0 sortie seulement en 2018. Dans cette histoire, nous visons à démontrer que ce langage vaut absolument la peine d’être ajouté à votre arsenal si vous êtes dans les domaines de la science des données, du calcul scientifique ou si vous êtes tout simplement un utilisateur passionné de Python. Il est possible que ce soit le langage de programmation le plus beau que vous rencontrerez jamais.
Dans cette histoire, nous explorerons les sommets des idées avec Julia et pourquoi il vaut la peine d’être appris. Une fois que vous avez terminé, nous vous recommandons vivement de consulter l’histoire suivante De Python à Julia : Un guide ultime pour une transition facile de Python à Julia.
Table des matières
· Julia est de Haut Niveau ∘ Syntaxe de Base ∘ Syntaxe Élégante pour les Mathématiques· Julia est Rapide ∘ Benchmarks ∘ Le Problème des Deux Langages ∘ Julia est Compilé en Temps Réel· Julia Résout le Problème d’Expression ∘ Le Problème d’Expression ∘ Dispatch Multiple ∘ Types Abstraits et Concrets· Julia est Entièrement Fonctionnelle ∘ Support des Tableaux ∘ Support des Chaînes de Caractères ∘ Multithreading ∘ Intégration Facile avec du Code en C ∘ La Bibliothèque Standard· Julia est Polyvalent ∘ Introduction ∘ Automatisation et Scripting· Julia est Extensivement Extensible ∘ Introduction ∘ Macros· Conclusion
Julia est haut-niveau
L’introduction vous a peut-être déjà fait sentir que cela ressemblera à Python – un langage généraliste, dynamique et de haut niveau également. Pour vérifier, voyons à quoi ressemble le code Julia de base par rapport à Python.
- Améliorer l’IA conversationnelle avec BERT la puissance du remplissage des espaces réservés
- Transformer du texte en vecteurs L’approche non supervisée de TSDAE pour des plongements améliorés
- Rencontrez LLMWare Un cadre tout-en-un d’intelligence artificielle pour simplifier le développement d’applications basées sur LLM pour les applications d’IA générative.
Syntaxe de base
Considérez le jeu de devinette suivant en Python :
import randomdef jeu_de_devinette(max): nombre_aleatoire = random.randint(1, max) print(f"Devinez un nombre entre 1 et {max}") while True: saisie_utilisateur = input() devinette = int(saisie_utilisateur) if devinette < nombre_aleatoire: print("Trop bas") elif devinette > nombre_aleatoire: print("Trop haut") else: print("C'est juste !") break jeu_de_devinette(100)
Voici l’équivalent en Julia :
function jeu_de_devinette(max::Integer) nombre_aleatoire = rand(1:100) println("Devinez un nombre entre 1 et $max") while true saisie_utilisateur::String = readline() devinette = parse(Int, saisie_utilisateur) if devinette < nombre_aleatoire println("Trop bas") elseif devinette > nombre_aleatoire println("Trop haut") else println("C'est juste !") break end endendjeu_de_devinette(100)
Les principales différences ici sont que Julia n’assume aucune indentation ou nécessite de deux-points, mais au lieu de cela, nécessite un “end” explicite pour terminer les blocs de construction tels que les conditions if, les boucles et les fonctions. Vous devriez vous sentir à l’aise avec cela si vous venez de Matlab ou de Fortran.
Une autre différence que vous avez peut-être remarquée est que Julia supporte naturellement les annotations de type dans les déclarations de variables, les arguments de fonction (et les types de retour, bien que rarement utilisés). Elles sont toujours facultatives, mais sont généralement utilisées pour des assertions de type, permettant au compilateur de choisir la bonne instance de méthode à appeler lorsque la même méthode est surchargée pour plusieurs types, et dans certains cas de déclaration de variable et de struct, pour des avantages de performances.
Syntaxe élégante pour les mathématiques
# Expressions élégantes x = 2z = 2y + 3x - 5# Prise en charge Unicode officielleα, β, γ = 1, 2, π/2# fonctions d'une seule lignef(r) = π*r^2f'(3) # dérivée (avec le package Flux.jl)# Un vecteur colonnev₁ = [1 2 3 4] v₂ = [1 2 3 4]# transposéprintln(v1' == v2)# Ceci est littéralement une matrice 3x3M⁽ⁱ⁾ = [1 2 3 4 5 7 7 8 9]# Modélisation explicite des valeurs manquantesX = [1, 2, missing, 3, missing]
Un sérieux avantage que Julia a sur Python est le support syntaxique pour les mathématiques. On n’a pas besoin d’utiliser * lors de la multiplication de constantes par des variables, les symboles latex sont pris en charge pour les noms de variable (il peut être nécessaire d’utiliser une extension VSCode pour convertir \pi en π, v\_1 en v₁, etc.) et les matrices en général respectent la mise en page dans la définition du code.
Par exemple, si vous deviez implémenter une descente de gradient pour un réseau neuronal.
En Python, vous écririez probablement :
import numpy as np# Descente de gradient dans un réseau neuronalJ_del_B_n = [np.zeros(b) for b in B_n]J_del_W_n = [np.zeros(W) for W in W_n]for (x, y) in zip(x_batch, y_batch): J_del_B_n_s, J_del_W_n_s = backprop(x, y) J_del_B_n = [J_del_b + J_del_b_s for J_del_b, J_del_b_s in zip(J_del_B_n, J_del_B_n_s)] J_del_W_n = [J_del_W + J_del_W_s for J_del_W, J_del_W_s in zip(J_del_W_n, J_del_W_n_s)]d = len(x_batch)W_n = [(1 - lambda_val * alpha / d) * W - lambda_val / d * J_del_W for W, J_del_W in zip(W_n, J_del_W_n)]B_n = [(1 - lambda_val * alpha / d) * b - lambda_val / d * J_del_b for b, J_del_b in zip(B_n, J_del_B_n)]
Comparez la lisibilité de cela à ce que vous pouvez écrire en utilisant Julia :
# Gradient Descent dans un NNმJⳆმBₙ = [zeros(b) for b in Bₙ]მJⳆმWₙ = [zeros(W) for W in Wₙ]for (x, y) in zip(x_batch, y_batch) მJⳆმBₙₛ, მJⳆმWₙₛ = backprop(x, y) მJⳆმBₙ = [მJⳆმb + მJⳆმbₛ for მJⳆმb, მJⳆმbₛ in zip(მJⳆმBₙ, მJⳆმBₙₛ)] მJⳆმWₙ = [მJⳆმW + მJⳆმWₛ for მJⳆმW, მJⳆმWₛ in zip(მJⳆმWₙ, მJⳆმWₙₛ)]d = len(x_batch)Wₙ = [(1 - λ*α/d)* W - λ/d * მJⳆმW for W, მJⳆმW in zip(Wₙ, მJⳆმWₙ)]Bₙ = [(1 - λ*α/d)* b - λ/d * მJⳆმb for b, მJⳆმb in zip(Bₙ, მJⳆმBₙ)]
Vous pouvez essayer d’écrire du code comme celui-ci en Python, mais les éditeurs mettront souvent des carrés jaunes autour des variables Unicode (ou ne les mettront pas en évidence) et votre code risque de ne pas fonctionner avec des packages tiers tels que Pickle.
Julia est rapide
Une autre raison majeure pour laquelle Julia peut être considérée comme un rêve de Python réalisé, c’est que, contrairement à Python, Ruby et autres langages de haut niveau, elle ne compromet pas la vitesse pour être de haut niveau. En fait, elle peut être aussi rapide que des langages de bas niveau tels que C et C++.
Benchmark
Pour référence, ce qui suit présente les performances de Julia, ainsi que d’autres langages sur des benchmarks de performance populaires :
Le problème des deux langages
Un corollaire de la performance de Julia est qu’elle résout le problème des deux langages :
- Le code de recherche (par exemple, un modèle d’apprentissage automatique) est généralement rédigé dans un langage de haut niveau tel que Python parce qu’il est de haut niveau et interactif ; il permet donc de se concentrer davantage sur la science (moins de problèmes de code) et permet une plus grande exploration.
- Une fois que le code de recherche est finalisé, il doit être réécrit dans un langage de bas niveau comme C avant d’être déployé en production.
Le problème ici est que le même code doit être réécrit dans plus d’un langage. C’est généralement difficile et sujet aux erreurs ; imaginez si le code de recherche est modifié après son déploiement, dans le pire des cas, il faudra tout réécrire dans le langage de bas niveau.
Une façon de contourner ce problème consiste à écrire des bibliothèques critiques en termes de performances (par exemple, Numpy) dans des langages de bas niveau tels que C, puis il est possible de les envelopper avec des fonctions Python qui appellent internement les fonctions C, ce qui peut être utilisé à la fois pour la recherche et la production sans se soucier des performances. En réalité, cela est très limité car :
- Il devient vraiment difficile pour les nouveaux développeurs de contribuer ou de collaborer avec des méthodes scientifiques nouvelles qu’ils ont écrites, car ils peuvent avoir besoin de les réécrire dans un langage de bas niveau tel que C pour des performances avant de les exposer dans la bibliothèque de haut niveau.
- Des contraintes hilarantes peuvent être imposées aux développeurs du langage de haut niveau dans le domaine du calcul scientifique. Par exemple, écrire des boucles explicitement peut être fortement déconseillé.
Julia résout le problème des deux langages en étant de haut niveau, interactif ET assez rapide, même pour la production.
Julia est compilé Just-in-time
Il y a une petite note liée aux performances de Julia. Parce que Julia est compilé JIT, la première exécution de n’importe quel morceau de code Julia prendra plus de temps à se terminer. Pendant ce temps, chaque code de fonction sera converti en code natif (c’est-à-dire du code que le processeur peut interpréter) pour les types de variables spécifiques inférés à partir du code. Une fois cela fait, il mettra en cache la représentation compilée de sorte que si la fonction est appelée à nouveau avec des entrées différentes du même type, alors elle sera interprétée immédiatement.
Pour préciser davantage, pour une fonction avec N arguments, il existe potentiellement un nombre exponentiel de représentations possibles de code natif ; une pour chaque combinaison possible de types pour les N arguments. Julia compilera la fonction vers la représentation qui correspond aux types inférés à partir du code la première fois que le code est exécuté. Une fois cela fait, les appels ultérieurs de la fonction seront sans effort. Notez qu’il n’utilise pas nécessairement des annotations de type (qui sont facultatives et peuvent avoir d’autres objectifs que nous avons mentionnés) pendant l’inférence de type, les types peuvent être inférés à partir des valeurs d’exécution des entrées.
Ce n’est pas un problème car le code de recherche ou le code s’exécutant sur un serveur doit être compilé initialement une seule fois et une fois que c’est fait, toutes les exécutions ultérieures (vraies appels d’API ou expérimentations ultérieures) du code sont extrêmement rapides.
Julia résout le problème de l’expression
Le problème de l’expression
Le problème de l’expression consiste à pouvoir définir une abstraction de données qui est extensible à la fois dans ses représentations (c’est-à-dire les types pris en charge) et dans ses comportements (c’est-à-dire les méthodes prises en charge). Autrement dit, une solution au problème de l’expression permet de :
- Ajouter de nouveaux types auxquels s’appliquent des opérations existantes
- Ajouter de nouvelles opérations auxquelles s’appliquent des types existants
sans violer le principe ouvert-fermé (ou causer d’autres problèmes). Cela implique qu’il devrait être possible d’ajouter de nouveaux types sans modifier le code des opérations existantes et il devrait être possible d’ajouter de nouvelles opérations sans modifier le code des types existants.
Python, comme de nombreux autres langages de programmation, est orienté objet et ne parvient pas à résoudre le problème de l’expression.
Supposons que nous avons l’abstraction de données suivante :
# Classe de baseclass Forme: def __init__(self, couleur): pass def surface(self): pass# Classe enfantclass Cercle(Forme): def __init__(self, rayon): super().__init__() self.rayon = rayon def surface(self): return 3.14 * self.rayon * self.rayon
Il est très facile d’ajouter de nouveaux types auxquels s’appliquent des méthodes existantes. Il suffit de hériter de la classe de base Forme
. Cela ne nécessite aucune modification de code existant :
class Rectangle(Forme): def __init__(self, largeur, hauteur): super().__init__() self.largeur = largeur self.hauteur = hauteur def surface(self): return self.largeur * self.hauteur
En revanche, il n’est pas facile d’ajouter des opérations auxquelles s’appliquent des types existants. Si nous voulons ajouter une méthode périmètre
, alors nous devons modifier la classe de base et chaque classe enfant déjà implémentée.
Une conséquence de ce problème est que si le package x est maintenu par l’auteur X et qu’il prend initialement en charge l’ensemble d’opérations Sx, et si un autre ensemble d’opérations Sy est utile à un autre groupe de développeurs Y, ils doivent être en mesure de modifier le package de X pour ajouter ces méthodes. En pratique, les développeurs Y créent simplement leur propre package, en dupliquant éventuellement du code provenant du package x pour mettre en œuvre le type, car l’auteur X peut ne pas être satisfait d’avoir plus de code à maintenir et Sy peut être un genre différent de méthodes qui n’a pas besoin de résider dans le même package.
<!–D’autre part, parce qu’il est facile d’ajouter de nouveaux types pour lesquels des opérations existantes s’appliquent, si le développeur Y voulait simplement définir un nouveau type qui implémente des opérations dans le type implémenté par X, alors ils pourraient facilement le faire sans même avoir besoin de modifier le paquetage x ou de dupliquer du code. Il suffirait d’importer le type et ensuite d’hériter de celui-ci.
Dispatch Multiple
Pour résoudre le problème de l’expression, qui permet une intégration massive entre différents paquets, Julia se passe complètement de la programmation orientée objet traditionnelle. Au lieu des classes, Julia utilise des définitions de types abstraits, des structs (instances de types abstraits personnalisés) et des méthodes et une technique appelée dispatch multiple qui, comme nous le verrons, résout parfaitement le problème de l’expression.
Pour voir un équivalent de ce que nous avions ci-dessus:
### Abstract Type Shape (Interface)type abstrait Shape endfunction area(self::Shape) end### Type Circle (Implémente l'interface)struct Circle <: Shape rayon::Float64endfunction area(circle::Circle) return 3.14 * circle.rayon^2end
Ici, nous avons défini un type abstrait “Shape”. Le fait qu’il soit abstrait implique qu’il ne peut pas être instancié; cependant, d’autres types (classes) peuvent en hériter. Ensuite, nous avons défini un type de cercle, en tant que sous-type du type abstrait Shape
et nous avons défini la méthode area
en spécifiant que l’entrée doit être de type Circle
. Par cela, nous pouvons faire
c = Circle(3.0)println(area(c))
Cela afficherait 28.26
. Bien que c
satisfasse les deux définitions de area
car c’est aussi une Shape
, la deuxième est plus spécifique donc c’est celle que le compilateur choisit pour l’appel.
De la même manière que l’OOP basée sur les classes, il est facile d’ajouter un autre type “rectangle” sans toucher au code existant:
struct Rectangle <: Shape longueur::Float64 largeur::Float64endfunction area(rect::Rectangle) return rect.longueur * rect.largeurend
Et maintenant quand nous faisons
rect = Rectangle(3.0, 6.0)println(area(rect))
Nous obtenons 18.0
. C’est le dispatch multiple en action; l’instance correcte de la méthode area
a été dynamiquement dispatchée en fonction du type d’exécution des arguments. Si vous venez d’un environnement C ou C++, cela doit vous rappeler la surcharge de fonctions. La différence est que la surcharge de fonctions n’est pas dynamique, elle repose sur les types trouvés lors de la compilation. Ainsi, vous pouvez concevoir des exemples où son comportement est différent.
Plus important encore, et contrairement à l’OOP basée sur les classes, nous pouvons ajouter des méthodes à n’importe quel type Shape
, Circle
ou Rectangle
sans avoir besoin de modifier leurs fichiers. Si tous les fichiers ci-dessus sont dans mon paquetage et que vous souhaitez ajouter un ensemble de méthodes qui produisent des animations et des visuels 3D des formes géométriques (ce qui ne m’intéresse pas), alors tout ce dont vous avez besoin est d’importer mon paquetage. Vous pouvez maintenant accéder aux types Shape
, Circle
et Rectangle
et vous pouvez écrire les nouvelles fonctions, puis les exporter dans votre propre paquetage “ShapeVisuals”.
### Définitions d'interfacesfunction animate(self::Shape) endfunction ThreeDify(self::Shape) end### Définitions de cerclesfunction animate(self::Circle) ...endfunction ThreeDify(self::Circle) ...end### Définitions de rectanglesfunction animate(self::Rectangle) ...endfunction ThreeDify(self::Rectangle) ...end
Quand on y réfléchit, la distinction majeure entre ceci et l’OOP que vous connaissez est qu’il suit le schéma func(obj, args)
au lieu de obj.func(args)
. En bonus, cela rend également des choses comme func(obj1, obj2, args)
un jeu d’enfant. L’autre distinction est qu’il n’encapsule pas les méthodes et les données ensemble ni n’impose aucune protection sur eux; peut-être une mesure sans rapport lorsque les développeurs sont suffisamment matures et que le code est examiné de toute façon.
Types Abstraits et Concrets
Le fait que vous sachiez maintenant qu’un type abstrait est simplement un type dont vous ne pouvez pas instancier de valeurs mais que d’autres types peuvent sous-types, ouvre la voie à la discussion sur le système de types de Julia. Rappelons que l’utilisation de la syntaxe var::type
pour annoter les types de variables lors de leur déclaration, en tant qu’arguments ou valeurs de retour de fonction, est facultative.
Tout type dans Julia est soit abstrait, comme nous l’avons défini précédemment, soit concret. Les types concrets sont ceux que vous pouvez instancier, comme les types personnalisés que nous avons définis ci-dessus.
Julia a le système de types hiérarchique suivant pour les nombres :
Si votre fonction prend un argument et opère sur n’importe quel nombre, vous utiliserez func(x::Number)
. Cela ne générera une erreur que si une valeur non numérique, telle qu’une chaîne de caractères, est transmise. Pendant ce temps, si cela ne fonctionne que pour un quelconque flottant, vous feriez func(x::AbstractFloat)
. Aucune erreur ne sera générée si l’entrée est de type BigFloat, Float64, Floar32 ou Floar16
. Étant donné qu’il existe une dispatch multiple, vous pouvez également définir une autre instance de la fonction func(x::Integer)
pour traiter le cas où le nombre donné est un entier.
Julia a également un système de types hiérarchique pour d’autres types abstraits tels que AbstractString
, mais ils sont beaucoup plus simples.
Julia est entièrement équipé
Si vous y réfléchissez, Python n’inclut que des fonctionnalités basiques dès le départ. Par exemple, vous pouvez faire très peu en matière de science des données et de calcul scientifique si vous n’utilisez que Python sans les packages populaires tels que Numpy. La grande majorité des autres packages du domaine dépendent également fortement de Numpy. Ils utilisent tous et supposent le type de tableau “Numpy” (au lieu du type de liste Python par défaut) comme s’il faisait partie du langage.
Julia n’est pas comme ça. Il inclut de nombreuses fonctionnalités importantes dès le départ, notamment :
Support de tableau
Julia inclut un support de tableau similaire à Numpy dès le départ, ce qui comprend le support de la diffusion et de la vectorisation. Par exemple, les opérations Numpy populaires sont comparées à la façon dont vous les écririez nativement en Julia :
#> 1. Création d'un tableau Numpy### Pythonarr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])### Juliaarr = [1 2 3 4 5 6 7 8 9]#> 2. Obtention de la forme d'un tableau### Pythonshape = arr.shape### Juliashape = size(arr)#> 3. Remodeler un tableau### Pythonreshaped_arr = arr.reshape(3, 3)### Juliareshaped_arr = reshape(arr, (3, 3))#> 4. Accéder aux éléments par indice### Pythonelement = arr[1, 2]### Juliaelement = arr[1, 2]#> 5. Effectuer des opérations arithmétiques élémentaires### Pythonmultiplication = arr * 3### Juliamultiplication = arr .* 3# 6. Concaténation de tableaux### Pythonarr1 = np.array([[1, 2, 3]])arr2 = np.array([[4, 5, 6]])concatenated_arr = np.concatenate((arr1, arr2), axis=0)### Juliaarr1 = [1 2 3]arr2 = [4 5 6]concatenated_arr = vcat(arr1, arr2)#> 7. Masquage booléen### Pythonmask = arr > 5masked_arr = arr[mask]### Juliamask = arr .> 5masked_arr = arr[mask]#> 8. Calcul de la somme des éléments d'un tableau### Pythonmean_value = arr.sum()### Juliamean_value = sum(arr)
Soutien des chaînes de caractères
Julia offre également une prise en charge étendue des chaînes de caractères et des expressions régulières intégrée :
name = "Alice"age = 13## concaténationgreeting = "Bonjour, " * name * "!"## interpolationmessage2 = "L'année prochaine, tu auras $(age + 1) ans."## regextext = "Voici quelques adresses e-mail : [email protected]"# Définir une regex pour les adresse e-mailemail_pattern = r"[\w.-]+@[\w.-]+\.\w+"# faire correspondre les adresse e-mailemail_addresses = match(email_pattern, text)"aby" > "abc" # vrai
Lorsque des chaînes de caractères sont comparées, celles qui sont plus tard dans l’ordre lexicographique (ordre alphabétique général) sont considérées comme étant supérieures à celles qui apparaissent plus tôt dans l’ordre. Il peut être démontré que la plupart de ce que vous pouvez faire avec des chaînes de caractères dans des langages avancés de traitement de chaînes comme Perl, peut également être réalisé en Julia.
Multi-threading
Le fait que Python ne supporte pas le multi-threading parallèle réel est justifié par le verrouillage global de l’interpréteur (GIL). Cela empêche l’exécution simultanée de plusieurs threads par l’interpréteur, ce qui constitue une solution simpliste pour garantir la sécurité des threads. Il est seulement possible de passer d’un thread à un autre (par exemple, si un thread serveur attend une requête réseau, l’interpréteur peut passer à un autre thread).
Heureusement, il est facile de libérer ce verrou dans les programmes C appelés par Python, ce qui explique pourquoi Numpy est possible. Cependant, si vous avez une boucle de calcul massive, vous ne pouvez pas écrire de code Python qui l’exécute en parallèle pour accélérer le calcul. La triste réalité pour Python est que la grande majorité des opérations mathématiques appliquées à de grandes structures de données telles que des matrices sont parallélisables.
Pendant ce temps, Julia prend en charge nativement le multi-threading parallèle réel et c’est aussi simple que cela :
# Avant le multi-threadingfor i in eachindex(x) y[i] = a * x[i] + y[i]end# Après le multi-threadingThreads.@threads for i in eachindex(x) y[i] = a * x[i] + y[i]end
Lorsque vous exécutez le code, vous pouvez spécifier combien de threads vous souhaitez utiliser parmi ceux disponibles sur votre système.
Intégration facile avec du code C
La procédure d’appel de code C depuis Julia est officiellement prise en charge et peut être réalisée de manière plus efficace et plus facile que dans Python. Si vous voulez appeler
#include <stdio.h>int add(int a, int b) { return a + b;}
alors l’étape principale (après une petite configuration) pour appeler cette fonction en Julia consiste à écrire
# spécifier la fonction, le type de retour, les types d'arguments et l'entrée. Préfixez les types avec "C"result = ccall(add, Cint, (Cint, Cint), 5, 3)
C’est beaucoup plus difficile à faire en Python et peut être moins efficace. Surtout parce qu’il est beaucoup plus facile de faire correspondre les types et les structures de Julia à ceux de C.
Une conséquence majeure de cela est qu’il est possible d’exécuter la grande majorité des langages pouvant produire du code C objet ici en Julia. En général, des packages externes bien connus existent à cette fin. Par exemple, pour appeler du code Python, vous pouvez utiliser le package PyCall.jl
comme suit :
using PyCallnp = pyimport("numpy")# Créez un tableau NumPy en Pythonpy_array = np.array([1, 2, 3, 4, 5])# Effectuez des opérations sur le tableau NumPypy_mean = np.mean(py_array)py_sum = np.sum(py_array)py_max = np.max(py_array)
Presque aucune configuration préalable n’est nécessaire pour cela, à part l’installation du package. Il est également possible d’appeler des fonctions écrites en Fortran, C++, R, Java, Mathematica, Matlab, Node.js, et plus encore en utilisant des packages similaires.
En revanche, il est possible d’appeler Julia depuis Python, bien que pas de manière aussi élégante. Cela a probablement déjà été utilisé pour accélérer les fonctions sans avoir recours à leur implémentation en C.
La bibliothèque standard
Un ensemble de packages est préinstallé (mais doit être explicitement chargé) avec Julia. Cela inclut les packages Statistics et LinearAlgebra, le package Downloads pour accéder à Internet, et plus important encore le package Distribued pour le calcul distribué (comme Hadoop), ainsi que le package Profile pour le profilage (pour optimiser le code) et notamment le package Tests pour les tests unitaires et le package Pkg pour la gestion des packages, ainsi que de nombreux autres.
Je dois dire que je suis un utilisateur passionné de Python qui a développé plusieurs packages en Python. Il n’y a aucune comparaison entre le package tiers “Setuptools” en Python et Pkg en Julia qui est vraiment beaucoup plus propre et plus facile à utiliser. Je n’ai jamais pu comprendre pourquoi Python n’a pas ses propres outils de gestion de packages et de tests. Ce sont des besoins vraiment basiques dans un langage de programmation.
Julia est polyvalent
Introduction
Si vous avez déjà rencontré Julia dans le passé, il serait naturel d’entendre dire que vous pensez que Julia est un langage spécifique à un domaine où le calcul scientifique est le domaine. Il est vrai que Julia a été soigneusement conçu pour être expressif et efficace pour le calcul scientifique, mais cela ne l’empêche pas d’être un langage polyvalent. C’est juste un langage conçu en gardant à l’esprit le calcul scientifique. Il existe cependant différents degrés de généralité d’un langage. Par exemple, Julia peut être utilisé pour la science des données et l’apprentissage automatique, le développement web, l’automatisation et le scripting, la robotique en plus du calcul scientifique, mais il n’existe toujours pas de packages matures qui aident les développeurs à utiliser Julia pour des choses comme le développement de jeux similaires à Pygame en Python. Même si le package Julia Genie.jl
est très proche d’être au même niveau que Flask
, il peut ne pas être aussi complet que des frameworks plus complets comme Django
. En bref, même si Julia n’est pas aussi polyvalent que vous le souhaitez pour le moment, il a été conçu en gardant cela à l’esprit et devrait l’être à terme.
Automatisation et scripting
Après avoir mentionné que Julia peut être utilisé pour l’automatisation et le scripting, il convient de souligner qu’il permet de le faire avec une syntaxe élégante similaire à celle d’un shell.
Par exemple, voici un ensemble d’opérations sur les systèmes de fichiers et les processus que vous pouvez effectuer en Julia:
# Créer un répertoiredir("my_directory")# Changer de répertoire de travailcd("my_directory")# Lister les fichiers du répertoire actuelprintln(readdir())# Supprimer le répertoirerm("my_directory"; recursive=true)# Vérifier si un fichier existeif isfile("my_file.txt") println("Le fichier existe.")else println("Le fichier n'existe pas.")end# Exécuter une simple commande shell depuis Julianrun(`echo "Bonjour, Julia!"`)# Capturer la sortie d'une commande shellresult = read(`ls`, String)println("Contenu du répertoire actuel: $result")
Remarquez la similitude avec ce que vous écrivez réellement dans le terminal.
Julia est largement extensible
Introduction
Une belle caractéristique du langage de programmation LISP est qu’il est homoïconique. Cela signifie que le code peut être traité comme des données et donc, de nouvelles fonctionnalités et sémantiques peuvent être ajoutées au langage par des développeurs ordinaires. Julia a également été conçu pour être homoïconique. Par exemple, rappelez-vous que j’ai dit que Julia ne prend en charge que la répartition multiple. Eh bien, il semble que quelqu’un ait créé un package ObjectOriented.jl
qui permet aux développeurs d’écrire de la POO en Julia. Autre exemple, si vous créez un nouveau type, il est facile de surcharger des fonctions de base et des opérateurs (qui ne sont que des fonctions) pour travailler avec votre nouveau type.
Macros
Le support de Julia pour les macros est une raison majeure pour laquelle cela est possible. Vous pouvez considérer une macro comme une fonction qui renvoie le code à exécuter pendant le temps d’analyse du programme. Supposons que vous définissiez la macro suivante:
macro add_seven(x) quote $x + 7 endend
Similaire à une fonction, cela vous permet de l’appeler de cette manière :
x = 5@add_seven x
ce qui renvoie 12. Ce qui se passe en coulisse, c’est qu’au moment de l’analyse (avant la compilation), la macro s’exécute, renvoyant le code 5 + 7
qui, au moment de la compilation, s’évalue à 12
. Vous pouvez penser aux macros comme un moyen de réaliser dynamiquement des opérations de recherche et de remplacement (CTRL+H
).
Prenons un autre cas d’utilisation pratique. Supposons que vous ayez un package avec 10 méthodes utiles, et que vous souhaitiez ajouter une nouvelle interface au package, ce qui signifie que vous devez écrire 10 structs, une pour chaque méthode. Supposons que vous puissiez écrire n’importe laquelle des structs en fonction de la fonction correspondante, alors vous pouvez simplement écrire une seule macro où vous bouclez sur les 10 fonctions pour générer du code pour les 10 structs. En pratique, le code que vous écrivez sera équivalent à l’écriture d’une seule struct de manière générique, ce qui vous fait gagner du temps.
Le fait que les macros soient possibles permet beaucoup plus de magie. Par exemple, si vous vous souvenez ci-dessus, nous avons pu paralléliser une boucle for en utilisant la macro Threads.@threads
. Pour mesurer le temps d’exécution d’un appel de fonction, il vous suffit de faire @time func()
. Si vous utilisez le package BenchmarkTools
, alors @benchmark func()
appellera la fonction plusieurs fois pour renvoyer des statistiques sur le temps et même un petit graphe. Si vous savez ce qu’est la mémorisation, même cela peut être appliqué à n’importe quelle fonction avec la simple macro @memoize
. Il n’est pas nécessaire de la modifier de quelque manière que ce soit. Il y a même @code_native func()
qui vous montrerait le code natif généré par la fonction, et il y a d’autres macros qui vous montrent d’autres représentations du code pendant le processus de compilation.
Conclusion
Il s’avère que toutes les fonctionnalités du langage dont nous avons parlé faisaient initialement partie du plan pour Julia. Comme indiqué sur le site web de Julia, voici la vision du langage :
“Nous voulons un langage open source, avec une licence libérale. Nous voulons la rapidité du langage C avec la dynamique de Ruby. Nous voulons un langage homoïconique, avec de vraies macros comme Lisp, mais avec une notation mathématique évidente et familière comme Matlab. Nous voulons quelque chose d’aussi utilisable pour la programmation générale que Python, aussi facile pour les statistiques que R, aussi naturel pour le traitement des chaînes de caractères que Perl, aussi puissant pour l’algèbre linéaire que Matlab, aussi bon pour assembler des programmes que le shell. Quelque chose d’extrêmement simple à apprendre, mais qui rende les hackers les plus sérieux heureux. Nous le voulons interactif et nous le voulons compilé.”
Après avoir lu cette histoire, vous devriez être en mesure de réfléchir sur chaque mot mentionné dans la déclaration de vision à ce stade.
J’espère que la lecture de ceci vous a aidé à en savoir plus sur le langage Julia et que vous envisagerez d’apprendre le langage. À la prochaine, au revoir.
We will continue to update IPGirl; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles
- Maîtriser l’avenir évaluation des architectures de données générées par LLM en exploitant les technologies IaC
- Qu’est-ce que les modèles multimodaux?
- Comment les développeurs de logiciels peuvent-ils être utiles avec ChatGPT et Bard AI?
- Le futur de l’IA dans le développement logiciel tendances et innovations
- Maîtriser l’apprentissage profond L’art d’approximer les non-linéarités avec des estimations morcelées Partie-2
- Un arrêt pour la régression logistique
- Maîtriser l’art du nettoyage de données en Python