Nombres décimaux et Python

31 décembre 2022 5 commentaires

Python, comme de nombreux autres langages ainsi que des implémentations matérielles, suit la norme IEEE 754 pour manipuler les nombres à virgule (le type float en Python). Cette norme définit les tailles possibles de mémoire allouée pour contenir le nombre. La taille étant fixe, certains nombres ne sont pas représentables et la valeur enregistrée peut être légèrement erronée.

Cette situation n’est donc pas spécifique à Python. L’écart entre la valeur saisie et la valeur en mémoire est visible avec un interpréteur Python :

$ python3 -q
>>> 1.9999999999999943e+71
1.9999999999999942e+71

ou un calcul qui devrait valoir 0 si les mathématiques étaient une science exacte :

$ python3 -q
>>> 0.1 + 0.1 + 0.1 - 0.3
5.551115123125783e-17

Ce type d’erreur ne se rencontre pas uniquement dans les domaines spatial ou scientifique. Par exemple, des calculs de TVA et TTC peuvent produire des erreurs visibles pour l’utilisateur.

Pour éviter ces erreurs, il est possible d’utiliser la bibliothèque decimal incluse dans la bibliothèque standard :

$ python3 -q
>>> from decimal import Decimal
>>> decimal.Decimal('1.9999999999999943e+71')
Decimal('1.9999999999999943E+71')
>>> Decimal(1) / Decimal(10) + \
... Decimal(1) / Decimal(10) + \
... Decimal(1) / Decimal(10) - \
... Decimal(3) / Decimal(10)
Decimal('0.0')

Un autre moyen est de faire des calculs en n’utilisant que des entiers et faire des conversions au dernier moment. Dans le cas de la TVA, cela signifie de ne travailler qu’en centimes et de ne convertir en euro que lors de l’affichage à l’utilisateur (avec l’arrondi adapté, limité à deux décimales).

Références

Publicité
Catégories :Python

Vérification de la syntaxe de certains fichiers de configuration

Certains logiciels fournissent aussi la possibilité de vérifier la syntaxe des fichiers de configuration qu’ils utilisent. Cela permet d’éviter des erreurs ou interruptions de service dûes à une erreur dans le fichier. Voici trois exemples :

1. Apache

Apache2 fournit apachectl. Si la syntaxe des sites actifs est correcte, la sortie sera :

# apachectl -t
Syntax OK

Avec un fichier de configuration incorrect nommé conte.conf contenant

<VirtualHost a.oree.du.bois:80>
    ServerName le.grand.mechant.loup.example
    CustomLog /il/etait/une/fois combined
    RencontreChaperonRouge on
</VirtualHost>

la sortie sera 

# apachectl -t
[Sun Sep 18 22:18:32.305781 2022] [core:error] [pid 14382:tid 139846731306112] (EAI 2)Name or service not known: AH00547: Could not resolve host name a.oree.du.bois -- ignoring!
AH00526: Syntax error on line 4 of /etc/apache2/sites-enabled/conte.conf:
Invalid command 'RencontreChaperonRouge', perhaps misspelled or defined by a module not included in the server configuration
Action '-t' failed.
The Apache error log may have more information.

Attention, les vérifications d’apachectl ne sont pas exhaustives et une erreur peut encore survenir lors du redémarrage du serveur Apache. Ici le chemin vers le fichier n’existe pas mais n’a pas été détecté. Si apachectl -t détecte une erreur, il y a un problème. S’il n’en détecte pas, il n’y a peut-être pas de problème.

(test réalisé avec Apache/2.4.38)

2. OpenSSH

La commande sshd -t exécutée avec des droits root permet de vérifier la validité de la configuration du serveur openSSH (le fichier /etc/ssh/sshd_config sous Debian).
Si le fichier est correct, alors rien n’est affiché et la valeur de sortie est 0.
Avec un fichier sshd_config commençant par :

PetitPotDeBeurre on
Tartiflette off

La sortie sera :

# sshd -t
[sudo] password for stephane: 
/etc/ssh/sshd_config: line 1: Bad configuration option: PetitPotDeBeurre
/etc/ssh/sshd_config: line 2: Bad configuration option: Tartiflette
/etc/ssh/sshd_config: terminating, 2 bad configuration options

avec une valeur de sortie de 255.

(test réalisé avec OpenSSH_7.9p1, OpenSSL 1.1.1d)

3. Sudo

Si une erreur est faite dans le fichier /etc/sudoers qui empêche sa relecture par l’exécutable sudo, il devient impossible d’utiliser la commande sudo. visudo permet d’éviter ce désagrément.
Supposons que l’utilisateur ait ajouté à la ligne 12,

Hello           MereGrand

puis enregistre le fichier :

% sudo visudo
/etc/sudoers:12:25: erreur de syntaxe
Hello           MereGrand
^
Et maintenant ?

Lorsque le fichier est incorrect, trois choix sont possibles :

  • remodifier le fichier
  • quitter sans enregistrer
  • quitter en enregistrant (une déception pourrait arriver peu de temps après)

L’éditeur par défaut utilisé par visudo est vi. Cela est modifiable en paramétrant des variables d’environnement comme $EDITOR. (En réalité, c’est plus compliqué: il y a deux autres variables d’environnement possibles et deux variables de configuration de sudo permettent de modifier de comportement des éditeurs par défaut. man sudo si vous pensez que cette complexité a un intérêt dans votre cas.)

(testé avec visudo version 1.9.5p2, version de la grammaire de visudo : 48)

Faim de loup, fin d’article

Ces outils sont pratiques pour éviter de mettre un service en panne ou s’enfermer dehors. Ils sont complémentaires de vérificateur générique de syntaxe JSON, YAML, etc.

Catégories :Autre

Hello Debian en Brainfuck

screenshots.debian.net est un service qui permet d’afficher des captures d’écran de logiciels. C’est assez pratique pour se faire une idée d’une interface par exemple. Une capture d’écran montrait déjà l’interpréteur Brainfuck beef affichant un classique
Hello Word!. Mais on peut aussi personnaliser en affichant un
Hello Debian! :

Utilisation de beef

Brainfuck

Brainfuck est un langage dont l’intérêt principal est d’être difficilement compréhensible par un humain. Pas la peine de s’étendre sur ses spécificités, wikipedia le fait très bien. Il ressemble à une machine de Turing: le programme déplace un curseur dans un tableau et modifie les valeurs contenues dans les cellules du tableau.
Voici une version commentée du programme utilisé (le début est quasi-identique au hello world fourni sur la page wikipedia puisqu’on veut écrire la même chose) :

++++++++++          affecte 10 à la case 0
[                   boucle initialisant des valeurs au tableau 
   >                avance à la case 1 
   +++++++          affecte 7 à la case 1
   >                avance à la case 2
   ++++++++++       affecte 10 à la case 2 
   >                avance à la case 3
   +++              affecte 3 à la case 3
   >                avance à la case 4
   +                affecte 1 à la case 4
   >                avance à la case 5
   +++++++++++      affecte 11 à la case 5
   <<<<<            retourne à la case 0
   -                enlève 1 à la case 0
]                   jusqu'à ce que la case 0 soit = à 0

La boucle initialise le tableau en 10 itérations et son état est alors :

Case 0 1 2 3 4 5
Valeur 0 70 100 30 10 110

Suite du programme :

>++                 ajoute 2 à la case 1 (70 plus 2 = 72)
.                   imprime le caractère 'H' (72)
>+                  ajoute 1 à la case 2 (100 plus 1 = 101)
.                   imprime le caractère 'e' (101)
+++++++             ajoute 7 à la case 2 (101 plus 7 = 108)
.                   imprime le caractère 'l'  (108)
.                   imprime le caractère 'l'  (108)
+++                 ajoute 3 à la case 2 (108 plus 3 = 111)
.                   imprime le caractère 'o' (111)
>++                 ajoute 2 à la case 3 (30 plus 2 = 32)
.                   imprime le caractère ' '(espace) (32)

<<<                 revient à la case 0
++                  ajoute 2 à la case 0 (0 plus 2 = 2)
[                   une boucle
   >                avance à la case 1 
   --               enlève 4 à la case 1 (72 moins 4 = 68)
   >                avance à la case 2
   -----            enlève 10 à la case 2 (111 moins 10 = 101)
   <<               retourne à la case 0
   -                enlève 1 à la case 0
]                   jusqu'à ce que la case 0 soit = à 0

>                   va case 1
.                   affiche 'D'
>                   va case 2
.                   affiche 'e'
---                 enlève 3 à la case 2 (101 moins 3 = 98)
.                   affiche 'b'
>>>                 va case 5
-----               enlève 5 à la case 5
.                   affiche 'i'
<<<                 va case 2
-                   enlève 1 à la case 2
.                   affiche 'a'
>>>                 va case 5
+++++               ajoute 5 à la case 5
.                   affiche 'n'
<<                  va à la case 3
+                   ajoute 1 à la case 3
.                   affiche un point d'exclamation

>                   va à la case 4
.                   imprime le caractère 'nouvelle ligne' (10)

screenshots.debian.net

Une capture de l’exécution du programme est disponible pour les interpréteurs beef et hsbrainfuck sur screenshot.debian.net.
Les images disponibles sur screenshots.debian.net sont aussi réutilisées par le service packages.debian.org (par exemple packages.debian.org) et par certains gestionnaires de paquets.
Si vous avez envie d’ajouter des captures d’écran à des paquets qui n’en auraient pas (les plus courants sont déjà faits), sachez que l’affichage n’est pas direct car il y a une validation manuelle des images envoyées. Le délai reste limité à quelques jours (voire à la journée).

Catégories :Debian

Évolution de la difficulté du minage de Bitcoin

Le Bitcoin est une monnaie cryptographique dont la validation des transactions est faite par une preuve de travail. La preuve de travail consiste à trouver un condensat (hash) commençant par un certain nombre de zéros.

Nombre de zéro au début du hash d’une transaction

Un hash de transaction (Transaction Hash ID) ressemble, par exemple, à 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f. Dans ce cas, 10 zéros débutent le hash.

Plus le nombre de zéro exigé est élevé, plus il est difficile de trouver une solution. Ce nombre évolue au fil du temps : si une solution est trouvée rapidement, le nombre de zéro augmente pour les transactions suivantes (et inversement).

Évolution jusqu’à aujourd’hui

Croissance du nombre de zéros requis au fil du temps

En bleu, les statistiques de l’ensemble des transactions contenues dans la chaîne. En orange, la moyenne des transactions de chaque journée. Les points noirs représentent les moyennes mensuelles.

La difficulté est croissante, avec parfois des plateaux. Cela montre indirectement l’augmentation de la puissance de calcul au fil du temps (le nombre de machines minant augmente, les processeurs sont plus performants, etc.).

Cela mesure encore plus imparfaitement la pollution générée par l’utilisation :

  • d’énergies renouvelables : à une période où le prix du Bitcoin était faible, une stratégie de minimisation des coûts a poussé certains à se limiter à la consommation d’excédents d’énergie hydroélectrique ou éolienne.
  • d’énergie fossile : le prix actuel pousse à la relance de centrales à gaz ou à charbon.

Production du graphique

Il est possible de voir les informations d’une transaction sur des services web mais autant extraire les informations à partir de la chaîne Bitcoin avec le client grâce à un script shell suivant :

#! /bin/sh
MAX_BLOCK=$(./bin/bitcoin-cli getblockcount)

for i in $(seq 1 $MAX_BLOCK); do {
HASH=$(./bin/bitcoin-cli getblockhash $i)
./bin/bitcoin-cli getblock $HASH | jq '[.time, .hash] | @csv' | sed 's/[\"]//g' >> stats_brutes.csv
}
done

Le script nécessite que la chaîne complète soit disponible sur la machine. La chaîne consomme presque 400 Go d’espace disque. La version du client et serveur était la 0.21.1.
Les commandes du binaire bitcoin-cli sont documentées sur https://developer.bitcoin.org/reference/rpc/index.html.
La production des statistiques au format .csv est très loooongue car le client bitcoin met du temps à répondre.

Une fois les données extraites au format .csv, un script python utilise la bibliothèque matplotlib pour produire le graphique :

import csv
import datetime
import time

import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np

def draw_graph(csv_path):
    SIZE = 0.5
    stats_completes, stats_quotidiennes, stats_mensuelles = stats(csv_path)
    fig, ax = plt.subplots()
    ax.plot_date(stats_completes["x"], stats_completes["y"], markersize=SIZE, label="Tous", color="blue")
    ax.plot_date(stats_quotidiennes["x"], stats_quotidiennes["y"], markersize=SIZE*2, label="Quotidien", color="orangered")
    ax.plot_date(stats_mensuelles["x"], stats_mensuelles["y"], markersize=SIZE*2, label="Mensuel", color="black")
    fmt_year = mdates.YearLocator()
    ax.xaxis.set_major_locator(fmt_year)
    fmt_month = mdates.MonthLocator()
    ax.xaxis.set_minor_locator(fmt_month)
    ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
    ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d %H:%M:%S')
    fig.autofmt_xdate()

    ax.grid(True)
    plt.xlabel("Temps")    
    plt.ylabel("Nombre de zéros")
    plt.legend(loc="upper left", markerscale=4)
    plt.savefig("./graph_bitcoin_zeros.png", dpi=150)

def stats(path):
    x_toutes, y_toutes = [], []
    with open(path, newline='') as csv_file:
        stats_reader = csv.reader(csv_file)
        for ligne in stats_reader:
            date = _from_timestamp(ligne[0])
            nb_zeros = _nb_zeros(ligne[1])
            x_toutes.append(date)
            y_toutes.append(nb_zeros)
    stats_quotidiennes = _stats_quotidiennes(x_toutes, y_toutes)
    stats_mensuelles = _stats_mensuelles(x_toutes, y_toutes)
    return {"x": x_toutes, "y": y_toutes}, stats_quotidiennes, stats_mensuelles

def _stats_quotidiennes(x_toutes, y_toutes):
    dates_possibles = set([(date.year, date.month, date.day) for date in x_toutes])
    stats_quot = {date: [] for date in dates_possibles}
    for index, x in enumerate(x_toutes):
        stats_quot[x.year, x.month, x.day].append(y_toutes[index])
    x, y = [], []
    for mois in sorted(dates_possibles):
        x.append(datetime.datetime(mois[0], mois[1], mois[2]))
        y.append(_moyenne(stats_quot[mois]))
    return {"x": x, "y": y}

def _stats_mensuelles(x_toutes, y_toutes):
    dates_possibles = set([(date.year, date.month) for date in x_toutes])
    stats_mois = {date: [] for date in dates_possibles}
    for index, x in enumerate(x_toutes):
        stats_mois[x.year, x.month].append(y_toutes[index])
    x, y = [], []
    for date in sorted(dates_possibles):
        x.append(datetime.datetime(date[0], date[1], 15))
        y.append(_moyenne(stats_mois[date]))
    return {"x": x, "y": y}

def _moyenne(valeurs):
    return sum(valeurs) / len(valeurs)

def _from_timestamp(data):
    return datetime.datetime.fromtimestamp(int(data))

def _nb_zeros(data):
    count = 0
    for lettre in data:
        if lettre == "0":
            count += 1
        else:
            return count

        
if __name__ == "__main__":
    draw_graph("stats_brutes.csv")

Le script python utilise un type de graphique idéal pour le besoin, déjà inclus dans Matplotlib. 🙂

Catégories :Autre

Démontage WD Elements Play

WD Elements Play était un boîtier multimédia vendu par Western Digital à partir des années 2010. Apparemment c’était un système assez limité dès sa sortie. Il semble que Western Digital ait simplement utilisé une conception faite chez Amlogic, en y incluant sa propre production de disque dur.

Composants

Le démontage du boîtier est aisé : il faut retourner le boîtier et déclipser le fond à l’aide d’un tournevis plat. Le disque dur se retire directement en le reculant pour le déconnecter du connecteur SATA, puis un deuxième cran pour pouvoir le sortir du boîtier. Une vidéo youtube faite par quelqu’un d’autre montre ce processus.

La carte qui fait l’intermédiaire entre les Entrées/Sorties et le disque dur est basée sur une puce Amlogic AML8626-H aux spécifications mystérieuses.

Le disque dur est au format 3,5 pouces et d’une taille de 1To. Sur le côté du disque, on peut voir les vis, chacune avec une pièce en caoutchouc pour retenir le disque dans le boîtier.

Quel format pour le disque ?

Étant donné qu’il s’agit d’un disque standard, il suffit de le brancher sur une machine quelconque pour le détecter (ici sur /dev/sdb) :

# fdisk -l
[...]
Disk /dev/sdb: 931,51 GiB, 1000204886016 bytes, 1953525168 sectors
Disk model: WDC WD10EADS-11M
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disklabel type: dos
Disk identifier: 0x9a6027cc

Device Boot Start End Sectors Size Id Type
/dev/sdb1 2048 1953521663 1953519616 931,5G 27 Hidden NTFS WinRE

Parted est d’accord avec fdisk :

# parted -l
[...]
Model: ATA WDC WD10EADS-11M (scsi)
Disk /dev/sdb: 1000GB
Sector size (logical/physical): 512B/4096B
Partition Table: msdos
Disk Flags: 

Number  Start   End     Size    Type     File system  Flags
 1      1049kB  1000GB  1000GB  primary  ntfs         msftres

L’équivalent graphique avec gparted

Il y a donc une partition unique, au format Microsoft NTFS. L’intérêt est probablement d’avoir un format reconnu nativement par des machines Windows car il est possible de brancher le boîtier en USB pour le relier à un ordinateur.

Regarder le contenu du disque est trivial :

# mkdir /tmp/ntfs
# mount -t ntfs /dev/sdb1 /tmp/ntfs

ls /tmp/ntfs montre des fichiers comme la corbeille (RECYCLER), un répertoire autorun et un fichier autorun.inf (probablement utilisé lors du branchement en USB du boîtier sur un système Windows), un répertoire System Volume Information avec un point de restauration (peut-être pour une réinitialisation usine?) et un répertoire fourni par WD contenant un peu de documentation au format pdf. Bien évidemment, on y retrouve aussi les fichiers déposés par l’utilisateur.

Les versions des logiciels qui n’auront pas été maltraités malgré une partition NTFS:

  • fdisk et mount(util-linux 2.36.1)
  • parted (GNU parted) 3.4
  • GParted 1.2.0
Catégories :Autre Étiquettes :