[Français] Premiers pas avec Rust pour les programmeurs Python

Cet article est la traduction d'un article écrit par Armin Ronacher (@mitsuhiko) le mercredi 27 mai 2015.

Puisque le traducteur ne connaît pas du tout Rust, il peut y avoir des malentendus et des erreurs de traduction (en particulier les termes). Si vous trouvez une telle erreur, il serait utile que vous puissiez nous envoyer une demande de modification.

Premiers pas avec Rust pour les programmeurs Python

Maintenant que Rust 1.0 est sorti et très stable, j'ai pensé qu'il serait intéressant d'écrire un article d'introduction sur Rust pour les programmeurs Python. Ce guide explore les bases du langage Rust et compare différentes constructions et leur comportement.

Le langage Rust est une bête complètement différente de Python. Non seulement l'un est un langage compilé et l'autre un langage interprété, mais ils sont également très différents dans leurs principales caractéristiques linguistiques. En tant que tel, les parties fondamentales du langage sont assez différentes, mais ces langages ont beaucoup en commun concernant l'idée du fonctionnement de l'API. En tant que programmeur Python, de nombreux concepts vous sont très familiers.

syntaxe

La première différence que les programmeurs Python remarquent est la syntaxe. Contrairement à Python, Rust est un langage avec de nombreux crochets intermédiaires. Mais il y a de bonnes raisons à cela. C'est parce que Rust a des fonctions anonymes, des fermetures et de nombreuses fonctionnalités de chaînage que Python ne prend pas bien en charge. Dans les langages sans indentation, ces fonctionnalités facilitent l'écriture et la compréhension du code. Regardons maintenant le même exemple dans les deux langues.

Tout d'abord, voici un exemple de Python qui affiche "Hello World" trois fois.

    def main():
        for count in range(3):
            print "{}. Hello World!".format(count)

Le même exemple dans Rust.

    fn main() {
        for count in 0..3 {
            println!("{}. Hello World!", count);
        }
    }

Comme vous pouvez le voir, ils sont très similaires. «def» devient «fn» et le signe deux-points devient une parenthèse du milieu. L'autre différence syntaxique majeure est que Rust nécessite des informations de type pour les paramètres de fonction. Vous ne faites pas cela en Python. Les annotations de type sont disponibles dans Python 3, vous pouvez donc utiliser la même syntaxe que celle trouvée dans Rust.

L'un des nouveaux concepts par rapport à Python est la fonction avec un point d'exclamation à la fin. Ce sont des macros. Les macros sont développées au moment de la compilation. Par exemple, cette macro est utilisée pour le format de chaîne et sa sortie. Cette macro est un moyen de forcer le compilateur à avoir la chaîne de format appropriée au moment de la compilation. Ce faisant, vous ne ferez pas d'erreur dans le nombre ou le type d'arguments transmis à la fonction de sortie.

Trate contre protocole

On peut dire que le comportement d'un objet est le plus familier mais présente des caractéristiques différentes. En Python, les classes peuvent choisir un comportement spécifique en implémentant des méthodes spéciales. Ceci est communément appelé «suivre le protocole». Par exemple, implémentez la méthode __iter__ qui renvoie un itérateur pour créer un objet itérable. Ces méthodes doivent être implémentées dans la classe elle-même. Et plus tard _en fait _ ne peut pas être changé (monkeypatch ignore).

Le concept de Rust est très similaire, mais utilise des traits au lieu de méthodes spéciales. Les traits ont une manière légèrement différente d'atteindre le même objectif, mais leur implémentation est placée dans la portée locale et vous pouvez implémenter plus de traits pour le type à partir d'un autre module. Par exemple, si vous voulez qu'un entier ait un comportement spécial, vous pouvez le faire sans rien changer sur le type entier.

Pour comparer ce concept, voyons comment implémenter un type qui est ajouté au type lui-même. D'abord de Python.

    class MyType(object):
    
        def __init__(self, value):
            self.value = value
    
        def __add__(self, other):
            if not isinstance(other, MyType):
                return NotImplemented
            return self.__class__(self.value + other.value)    

Le même exemple dans Rust.

    use std::ops::Add;
    
    struct MyType {
        value: i32,
    }
    
    impl MyType {
        fn new(value: i32) -> MyType {
            MyType { value: value }
        }
    }
    
    impl Add for MyType {
        type Output = MyType;
    
        fn add(self, other: MyType) -> MyType {
            MyType { value: self.value + other.value }
        }
    }

Le code de Rust ici est un peu plus long, mais il gère également les types automatiques que le code Python n'a pas. La première chose à noter est qu'en Python les méthodes appartiennent à des classes, alors qu'en Rust les données et ses opérations sont indépendantes. struct définit la disposition des données. ʻImpl MyType définit les méthodes dont dispose le type lui-même, tandis que ʻimpl Add for My Type implémente le trait ʻAdd pour ce type. Pour l'implémentation de ʻAdd, nous devons également définir le type de résultat de l'opération d'ajout implémentée ici, mais cela élimine la complexité supplémentaire de la vérification de type à l'exécution que nous devons faire en Python.

Une autre différence est que le constructeur est explicite dans Rust, alors que c'est assez magique en Python. En Python, lorsque vous créez une instance d'un objet, vous appelez __init__ à la fin pour initialiser l'objet. Rust, d'autre part, définit simplement une méthode statique (par convention appelée «new») qui assemble et attribue des objets.

La gestion des erreurs

La gestion des erreurs dans Python et Rust est assez différente. Les erreurs Python sont levées comme exceptions, tandis que les erreurs Rust sont renvoyées comme valeur de retour. Cela peut sembler étrange au début, mais c'est un très bon concept. Il est facile de voir quel type d'erreur est renvoyé lorsque vous regardez une fonction.

Cela signifie que les fonctions de Rust peuvent renvoyer Result. Le résultat est un type paramétré qui a deux aspects: le succès et l'échec. Par exemple, «Result <i32, MyError>» signifie que si cette fonction réussit, elle renvoie un entier 32 bits, et si une erreur se produit, elle renvoie «MyError». Que faire si je dois renvoyer une ou plusieurs erreurs? C'est là que la perspective philosophique est différente.

En Python, les fonctions peuvent échouer avec des erreurs arbitraires et vous ne pouvez rien y faire. Si vous avez utilisé la bibliothèque de "requêtes" de Python pour intercepter toutes les exceptions de requête, vous comprendrez l'essence du problème après avoir été frustré que les erreurs SSL ne soient pas détectées. Si une bibliothèque ne documente pas ce qu'elle renvoie, un utilisateur de cette bibliothèque ne peut pas faire grand-chose.

Cette situation est très différente à Rust. La signature de la fonction contient l'erreur qu'elle renvoie. Si vous devez renvoyer deux erreurs, il existe un moyen de créer un type d'erreur personnalisé pour convertir l'erreur interne en une erreur meilleure. Par exemple, en supposant que vous disposez d'une bibliothèque HTTP, si cette bibliothèque peut échouer en interne avec des erreurs telles que des erreurs Unicode, des erreurs IO, des erreurs SSL, vous devez la convertir en un type d'erreur spécifique à la bibliothèque. , L'utilisateur de cette bibliothèque ne doit gérer que cette erreur. Si nécessaire, Rust fournit une chaîne d'erreur qui remonte à l'endroit où l'erreur a été créée et pointe vers l'erreur d'origine.

Vous pouvez également utiliser le type Boîte <Erreur> n'importe où. Si vous ne souhaitez pas créer votre propre type d'erreur personnalisé, ce type le convertira en n'importe quelle erreur.

Alors que Python propage implicitement les erreurs, Rust propage explicitement les erreurs. Cela signifie que vous pouvez voir immédiatement que la fonction renvoie une erreur, même si la fonction n'a pas de contrôle d'erreur. Ceci est réalisé par la macro «essayez!». Voici un exemple:

    use std::fs::File;
    
    fn read_file(path: &Path) -> Result<String, io::Error> {
        let mut f = try!(File::open(path));
        let mut rv = String::new();
        try!(f.read_to_string(&mut rv));
        Ok(rv)
    }

«File :: open» et «read_to_string» peuvent échouer avec des erreurs d'E / S. La macro try! Retourne immédiatement de la fonction pour propager l'erreur vers le haut en cas d'erreur et décompresser en cas de succès. Lors du retour du résultat d'une fonction, il doit être encapsulé dans «Ok» pour indiquer le succès ou «Err» pour indiquer l'échec.

La macro try! Appelle le trait From, ce qui rend l'erreur traduisible. Par exemple, implémentez la conversion de ʻio :: ErrorenMyError en changeant la valeur de retour de ʻio :: Error en MyError et en implémentant le trait From. Et cette conversion y est appelée automatiquement.

Vous pouvez également changer la valeur de retour de ʻio :: ErrorenBox ` pour renvoyer n'importe quelle erreur. Cependant, ce n'est pas une erreur de compilation, il suffit de penser aux erreurs d'exécution.

Vous pouvez également ʻunwrap () `le résultat si vous voulez interrompre son exécution sans contrôler l'erreur. De cette façon, s'il réussit, il obtient la valeur, et si le résultat est une erreur, le programme s'arrête.

Mutabilité et propriété

La partie du langage qui est complètement différente entre Rust et Python est le concept de variabilité et de propriété. Python est un langage avec un garbage collection. En conséquence, presque tout se passe avec l'objet au moment de l'exécution. Vous êtes libre de remettre de tels objets et ils "se déplaceront normalement". Vous pouvez explicitement provoquer une fuite de mémoire, mais de nombreux problèmes sont résolus automatiquement lors de l'exécution.

Rust n'a pas de ramasse-miettes, mais la gestion de la mémoire est toujours automatique. Ceci est rendu possible grâce à un concept appelé suivi de propriété. Tout ce que vous créez appartient à un autre. Si vous comparez cela en Python, imaginez que tous les objets Python appartiennent à l'interpréteur. La propriété de Rust est beaucoup plus locale. Supposons que vous ayez une liste d'objets qui ont des appels de fonction. Dans ce cas, la liste possède l'objet et la portée de la fonction possède la liste.

Des scénarios de propriété plus complexes peuvent être représentés par des annotations de durée de vie de propriété et des signatures de fonction. Par exemple, dans l'exemple de l'implémentation ʻAddci-dessus, nous avons nommé le récepteurself`, tout comme Python. Cependant, contrairement à Python, la valeur est "déplacée" dans une fonction, alors qu'en Python, la méthode est appelée avec une référence de variable. Cela signifie qu'avec Python, vous pouvez:

    leaks = []
    
    class MyType(object):
        def __add__(self, other):
            leaks.append(self)
            return self
    
    a = MyType() + MyType()

Se fuit dans la liste globale lors de l'ajout d'instances MyType à d'autres objets. L'exécution du code ci-dessus aura deux références à la première instance de MyType. Un pour «fuites» et l'autre pour «a». Vous ne pouvez pas faire ça avec Rust. Parce qu'il n'y a qu'un seul propriétaire. Si vous essayez d'ajouter «self» aux «fuites», le compilateur «déplacera» la valeur là-bas et la fonction ne pourra pas la renvoyer car elle s'est déplacée quelque part. Pour renvoyer cette valeur de la fonction, vous devez d'abord la déplacer en arrière (par exemple, la supprimer de la liste).

Et si vous avez besoin de deux références à un objet? Vous pouvez en fait emprunter cette valeur. Il n'y a pas de limite au nombre d'emprunts immuables, mais un seul emprunt modifiable (et seulement s'il n'y a pas d'emprunt immuable).

Les fonctions qui fonctionnent sur des emprunts invariants sont étiquetées avec «& self», et les fonctions qui nécessitent un emprunt variable sont étiquetées avec «& mut self». Les références ne peuvent être louées que par le propriétaire. Si vous souhaitez retirer cette valeur de cette fonction (par exemple, la renvoyer à partir de la fonction), vous ne pouvez pas avoir de prêt en cours et vous prêtez cette valeur après avoir transféré la propriété quelque part. Je ne peux pas faire ça non plus.

C'est un grand changement dans votre façon de penser les programmes, mais vous vous y habituerez bientôt.

Emprunts d'exécution et plusieurs propriétaires [^ 1]

[^ 1]: Le texte original est "Mutible Owners", mais je suppose que c'est une erreur dans Multiple.

Jusqu'à présent, presque tous les suivis de propriété ont été vérifiés au moment de la compilation. Mais que faire si vous ne pouvez pas vérifier la propriété au moment de la compilation? Il existe plusieurs options pour une utilisation gratuite. Un exemple est l'utilisation de mutex. Le mutex garantit qu'une seule personne aura des emprunts de variables sur l'objet lors de l'exécution, mais le mutex lui-même possède l'objet. De cette façon, j'écris du code qui accède au même objet, mais à un moment donné, un seul thread peut accéder à cet objet.

En conséquence, cela signifie également que vous n'oublierez pas d'utiliser le mutex et de provoquer des conflits de données. C'est parce que ce code ne se compile pas.

Mais si vous voulez faire cela en Python, comment pouvez-vous trouver le propriétaire de la mémoire? Dans un tel cas, définissez un objet dans l'encapsuleur de comptage de références et prêtez la valeur de ce côté au moment de l'exécution. C'est très proche du comportement de Python simplement parce qu'il peut faire un cycle. Python divise le cycle avec son garbage collector, et Rust n'en a pas.

Pour mieux illustrer cela, examinons un exemple Python complexe et l'équivalent de Rust.

    from threading import Lock, Thread
    
    def fib(num):
        if num < 2:
            return 1
        return fib(num - 2) + fib(num - 1)
    
    def thread_prog(mutex, results, i):
        rv = fib(i)
        with mutex:
            results[i] = rv
    
    def main():
        mutex = Lock()
        results = {}
    
        threads = []
        for i in xrange(35):
            thread = Thread(target=thread_prog, args=(mutex, results, i))
            threads.append(thread)
            thread.start()
    
        for thread in threads:
            thread.join()
    
        for i, rv in sorted(results.items()):
            print "fib({}) = {}".format(i, rv)

Ce que nous faisons ici est de générer 35 threads et de faire de terribles calculs pour augmenter le nombre de Fibonacci. Rejoignez ensuite les discussions pour voir les résultats triés. Une chose que vous remarquerez bientôt ici est qu'il n'y a pas de relation essentielle entre le mutex (verrou) et le tableau résultant.

Vient ensuite un exemple de Rust.

    use std::sync::{Arc, Mutex};
    use std::collections::BTreeMap;
    use std::thread;
    
    fn fib(num: u64) -> u64 {
        if num < 2 { 1 } else { fib(num - 2) + fib(num - 1) }
    }
    
    fn main() {
        let locked_results = Arc::new(Mutex::new(BTreeMap::new()));
        let threads : Vec<_> = (0..35).map(|i| {
            let locked_results = locked_results.clone();
            thread::spawn(move || {
                let rv = fib(i);
                locked_results.lock().unwrap().insert(i, rv);
            })
        }).collect();
        for thread in threads { thread.join().unwrap(); }
        for (i, rv) in locked_results.lock().unwrap().iter() {
            println!("fib({}) = {}", i, rv);
        }
    }

La grande différence ici avec le code Python est qu'il utilise une carte B-tree au lieu d'une table de hachage et ajoute cette carte au mutex Arc'ed. Qu'est-ce que c'est? Tout d'abord, j'utilise l'arbre B car il trie automatiquement, ce dont j'avais besoin ici. Ajoutez-le ensuite au mutex afin de pouvoir verrouiller la carte lors de l'exécution. La relation a été établie ici. Enfin, ajoutez ce mutex à Arc. La référence Arc compte ce qu'elle renferme. Dans ce cas, mutex. Cela signifie que nous garantissons que le mutex ne peut être supprimé qu'après la fin de l'exécution du dernier thread. C'est un mécanisme intelligent.

Voyons maintenant comment ce code fonctionne. Comptez jusqu'à 35 [^ 2] comme Python et exécutez des fonctions locales pour chaque nombre. Contrairement à Python, vous pouvez utiliser des fermetures ici. Faites ensuite une copie d'Arc dans votre thread local. Cela signifie que chaque méthode verra son propre Arc individuellement (en interne, cela augmentera automatiquement le nombre de références et le diminuera lorsque le thread meurt). Ensuite, créez ce thread avec une fonction locale. Ce move vous indique de déplacer la fermeture à l'intérieur du thread. Exécutez ensuite la fonction Fibonacci dans chaque thread. Déballez puis ajoutez la valeur lors du verrouillage de l'arc qui renvoie le résultat. Ignorez ce déballage pendant un moment. Cela confond simplement le résultat explicite. Mais le fait est que vous ne pouvez obtenir la carte résultante que lorsque vous déverrouillez le mutex. Vous ne pouvez jamais oublier de le verrouiller par inadvertance!

[^ 2]: Le texte original dit "nous comptons jusqu'à 20 comme en Python", mais je pense que c'est probablement une faute de frappe de 35.

Ensuite, rassemblez tous les threads dans un tableau à une dimension (vecteur). Enfin, rejoignez tous les threads de manière itérative et affichez le résultat.

Il y a deux choses à noter ici. Il y a très peu de moisissures visibles. Bien sûr, les types Arc et les fonctions Fibonacci gèrent les types entiers 64 bits non signés, mais il n'y a pas d'autres types explicites. J'ai également utilisé l'arborescence B au lieu de l'objet hachable car Rust fournit un tel type.

L'itération fonctionne exactement comme Python. La seule différence est que dans cet exemple, Rust doit obtenir un mutex. La raison en est que le compilateur ne sait pas que vous n'avez pas besoin du thread fini ou de son mutex. Cependant, il existe des API qui ne nécessitent pas ce mutex, et ces API ne sont pas encore stables dans Rust 1.0.

En termes de performances, exactement ce que vous attendiez se produira. (Cet exemple a écrit intentionnellement un code terrible pour illustrer le comportement du thread.)

Unicode

Le sujet d'Unicode est mon préféré :) Rust et Python sont assez différents. Python (2 et 3) est très similaire au modèle Unicode, qui mappe les données Unicode à un tableau de caractères. Rust, en revanche, est une chaîne Unicode qui est toujours stockée en UTF-8. J'ai expliqué plus tôt pourquoi c'est une bien meilleure solution que ce que Python et C # essayent de faire (UCS vs UTF-8 as Internal String Encoding. Voir 2014/1/9 / ucs-vs-utf8 /)). Une chose très intéressante à propos de Rust est de savoir comment gérer la triste réalité du monde autour de l'encodage.

Tout d'abord, Rust est parfaitement conscient que les API du système d'exploitation (Windows Unicode et Linux non Unicode) sont assez terribles. Contrairement à Python, nous ne forçons pas Unicode dans ces zones. Au lieu de cela, ils ont différents types de chaînes qui peuvent être (raisonnablement) convertis les uns aux autres à faible coût. Cela fonctionne très bien en pratique et rend la manipulation des cordes très rapide.

Accepter UTF-8 pour la plupart des programmes élimine le besoin d'encodage / décodage. Vous n'avez qu'à effectuer des contrôles de validation à faible coût et le traitement sur des chaînes UTF-8 n'a pas besoin d'être codé au milieu. Si vous avez besoin d'intégrer l'API Windows Unicode, vous pouvez convertir en interne en UCS2 comme UTF-16 à un coût assez bas encodage WTF-8 -8 /) est utilisé.

Vous pouvez convertir entre Unicode et octets n'importe où et gérer les octets selon vos besoins. Vous pouvez ensuite effectuer les étapes de validation ultérieurement pour vous assurer que tout est comme prévu. Cela vous permet d'écrire un protocole à la fois très rapide et très pratique. Comparez cela au simple fait de devoir constamment encoder et décoder en Python pour prendre en charge l'indexation de chaînes pour ʻO (1) `.

Outre le très bon modèle de stockage d'Unicode, il existe également de nombreuses API pour travailler avec Unicode. Il fait partie du langage ou de l'index Great crates.io (https://crates.io/search?q=unicode). Cela inclut le pliage de cas, la catégorisation, les expressions canoniques Unicode, la normalisation Unicode, les API URI / IRI / URL bien connues, le fractionnement et le mappage de noms simple.

Quels sont les inconvénients? Vous ne pouvez pas annuler 'ö' comme vous le vouliez avec une chaîne comme" föo "[1]. Mais en tout cas ce n'est pas une bonne idée.

Pour illustrer comment interagir avec le comportement du système d'exploitation, nous présenterons une application qui ouvre un fichier dans le répertoire courant et affiche son contenu et son nom de fichier.

    use std::env;
    use std::fs;
    
    fn example() -> Result<(), Box<Error>> {
        let here = try!(env::current_dir());
        println!("Contents in: {}", here.display());
        for entry in try!(fs::read_dir(&here)) {
            let path = try!(entry).path();
            let md = try!(fs::metadata(&path));
            println!("  {} ({} bytes)", path.display(), md.len());
        }
        Ok(())
    }
    
    fn main() {
        example().unwrap();
    }

Toutes les opérations d'E / S utilisent l'objet Path introduit précédemment. Il encapsule correctement le chemin interne du système d'exploitation. Cela peut être des octets ou unicode, ou autre chose que le système d'exploitation utilise. Cependant, il est correctement formaté en appelant .display (). Cette méthode retourne un objet dans la chaîne qui peut se formater. C'est utile. La raison en est que vous ne fuirez jamais par inadvertance une chaîne illégale, comme vous le feriez dans Python 3, par exemple. Une séparation nette des intérêts.

Distributions et bibliothèques

Rust a une «cargaison» qui combine virtualenv + pip + setuptools. Eh bien, ce n'est pas exactement l'équivalent de virtualenv car il ne fonctionne qu'avec une seule version de Rust par défaut, mais sinon, cela fonctionne comme prévu. L'avantage de Python est que vous pouvez créer des dépendances pour différentes versions de la bibliothèque dans le référentiel git ou dans l'index crates.io. Si vous avez téléchargé rust depuis le site Web, il est livré avec la commande cargo et tout devrait fonctionner comme prévu.

Rust remplacera-t-il Python?

Je ne pense pas qu'il existe une relation directe entre Python et Rust. Par exemple, Python a réussi dans le domaine de l'informatique, et je ne pense pas que Rust travaillera dans ce domaine dans un proche avenir simplement en raison de la quantité de travail qu'il nécessitera. De même, cela n'a aucun sens d'écrire un script shell en Python mais de l'écrire en Rust. Cela dit, je pense que cela commence à se pencher sur Rust dans des domaines où il s'agissait auparavant de Python, tout comme de nombreux programmeurs Python ont commencé à utiliser Go.

Rust est un langage très puissant, a une base solide et fonctionne sous une licence libre avec une communauté amicale et une attitude démocratique envers l'évolution de la langue.

Le support d'exécution de Rust est si petit qu'il est très facile à utiliser depuis Python via ctypes et CFFI. Je pouvais imaginer très clairement l'avenir. Il créera un package Python qui inclut une distribution de modules binaires écrits en Rust, qui appellera le module Rust à partir de Python sans l'effort requis par le développeur.

© Copyright 2015 by Armin Ronacher. Content licensed under the Creative Commons attribution-noncommercial-sharealike License.

Recommended Posts

[Français] Premiers pas avec Rust pour les programmeurs Python
Premiers pas avec Python pour les classes PHPer
Premiers pas avec Python pour les fonctions PHPer
1.1 Premiers pas avec Python
Premiers pas avec Python
Premiers pas avec Python pour PHPer-Super Basics
Premiers pas avec Python
Paramètres pour démarrer avec MongoDB avec python
Introduction aux fonctions Python
Premiers pas avec Python Django (1)
Premiers pas avec Python Django (4)
Premiers pas avec Python Django (3)
Introduction à Python Django (6)
Premiers pas avec Python Django (5)
Premiers pas avec Google App Engine pour Python et PHP
Premiers pas avec Python responder v2
Premiers pas avec les applications Web Python
Premiers pas avec Julia pour Pythonista
Premiers pas avec Python Bases de Python
Premiers pas avec les algorithmes génétiques Python
Premiers pas avec Python 3.8 sous Windows
Premiers pas avec python3 # 1 Apprenez les connaissances de base
Utiliser DeepL avec python (pour la traduction d'articles)
Premiers pas avec Python Web Scraping Practice
Premiers pas avec Python Web Scraping Practice
Premiers pas avec Dynamo de Python boto
Premiers pas avec Lisp pour Pythonista: Supplément
Démarrer avec Python avec 100 coups sur le traitement du langage
Django 1.11 a démarré avec Python3.6
Premiers pas avec la traduction japonaise du modèle séquentiel Keras
Premiers pas avec apache2
Premiers pas avec Django 1
Introduction à l'optimisation
Premiers pas avec AWS IoT facilement en Python
Premiers pas avec Numpy
Premiers pas avec Spark
Premiers pas avec Pydantic
Premiers pas avec Jython
Matériel à lire lors de la mise en route de Python
Premiers pas avec Django 2
Premiers pas avec python3 # 2 En savoir plus sur les types et les variables
Traduire Premiers pas avec TensorFlow
Introduction à Tkinter 2: Button
Premiers pas avec Go Assembly
Python lancé par des programmeurs C
Commencez avec Python! ~ ② Grammaire ~
Premiers pas avec Django avec PyCharm
Premiers pas avec python3 # 3 Essayez des calculs avancés à l'aide de l'instruction d'importation
Initiation aux mathématiques Démarrage avec la programmation Python Challenge Notes personnelles - Problème 1-1
Orientation des objets Prolog pour les programmeurs Python
Commencez avec Python! ~ ① Construction de l'environnement ~
Lien pour commencer avec python
Introduction à Git (1) Stockage d'historique
Premiers pas avec Sphinx. Générer docstring avec Sphinx
Premiers pas avec Sparse Matrix avec scipy.sparse
Comment démarrer avec Python