Archivi Categorie: Python

Messaggio colorato nel terminale – 1

kk2

Se un messaggio è importante deve attrarre l’attenzione. Argomento già trattato in passato ma questa è la versione 2018.

Ci sono due modi facili: un suono e un colore diverso dal solito.

Produrre un beep è semplice, così:

#!/bin/bash
echo -en "\a"

o così:

#!/usr/bin/python3
print("\a", end="")

Visualizzare il testo colorato è risultato immediato con Python, più impegnativo con Bash. I codici delle sequenze di escape ANSI si trovano (come farei senza la Wiki) qui: ANSI escape code – Colors.

La versione Python:

#!/usr/bin/python3

import sys

RED='\033[01;31m'
GREEN='\033[01;32m'
YELLOW='\033[01;33m'
BLUE='\033[01;34m'
ENDC='\033[00m'
BEEP='\a'

dis = str.join(' ', sys.argv[1:])

dis = dis.replace('%R', RED)
dis = dis.replace('%G', GREEN)
dis = dis.replace('%Y', YELLOW)
dis = dis.replace('%B', BLUE)
dis = dis.replace('%Z', ENDC)
dis = dis.replace('%_', BEEP)

print(dis)

 

I codici sono nella forma %L dove %L è una lettera (o anche più di una, o _ per il beep) che genera una sequenza improbabile: di solito % finisce la parola, terminando un numero.

Più impegnativo produrre l’equivalente Bash: abbiamo, PLG e io, provato con sed ma con pessimi risultati. Poi la dritta, da Stack Overflow ed ecco:

#!/bin/bash

st=$*
st=$(printf '%s' "$st" | sed -e 's/:R/\\033[01;31m/g')
st=$(printf '%s' "$st" | sed -e 's/:G/\\033[01;32m/g')
st=$(printf '%s' "$st" | sed -e 's/:Y/\\033[01;33m/g')
st=$(printf '%s' "$st" | sed -e 's/:B/\\033[01;34m/g')
st=$(printf '%s' "$st" | sed -e 's/:Z/\\033[00m/g')

echo -e $st

sh
Rispetto alla versione Python cambia il codice, uso : al posto di %. Probabilmente da uniformare. Prossimamente, forse, chissà 😐

Convertire codice Fortran in Python – 2

Continuo da qui.

Questione preliminare: perché non usare le liste?
Risposta: le liste sono per elementi eterogenei e sono lente e avide di memoria. Se i dati sono omogenei (p.es. tutti int o tutti float o casi simili) ci sono gli array mono- e pluri-dimensionali (matrici).

Python ha un modulo per la gestione degli arrays, ma c’è di meglio: NumPy.

Ogni trattazione di questo argomento inizia con un avvertimento: attenzione alla gestione degli indici, e continua con l’esempio Fortran “per colonna” invece del più comune “per riga”. OK, da verificare. C’è poi un altro aspetto cui prestare attenzione, anzi due. In Fortran il primo indice è 1; in Pthon 0; lo sanno tutti, è banale ma è facile fare confusione, esempio:

$ py3
>>> import numpy as np
>>> ar = np.arange(10)
>>> ar
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> ar.shape
(10,)
>>> ar.size
10
>>> ar[0]
0
>>> ar[5]
5
>>> ar[ar.size]
Traceback (most recent call last):
  File "", line 1, in 
IndexError: index 10 is out of bounds for axis 0 with size 10
>>> ar[ar.size - 1]
9

OK? Quando si ha una matrice attenzione ala gestione degli indici, dicono tutti. Da verificare in Fortran (ebbene sì! ma è semplice e c’è una piacevole sorpresa).

Gestione indici per per riga, file row_col.f

      program row_col

      integer row, col
      real m(1000,1000)

      do row = 1, 1000
          do col = 1, 1000
              m(row, col) = col + 10 * row
          end do
      end do

      print*, m(1, 1), m(2, 200), m(1000, 1)

      do row = 1, 1000
          do col = 1, 1000
              m(row, col) = m(row, col) / 2
          end do
      end do

      print*, m(1, 1), m(2, 200), m(1000, 1)

      end

Compilo ed eseguo:

$ gfortran -o row-col row-col.f
$ time ./row-col
   11.0000000       220.000000       10001.0000
   5.50000000       110.000000       5000.50000

real	0m0,016s
user	0m0,012s
sys	0m0,004s
$ time ./row-col
   11.0000000       220.000000       10001.0000
   5.50000000       110.000000       5000.50000

real	0m0,018s
user	0m0,014s
sys	0m0,004s
$ time ./row-col
   11.0000000       220.000000       10001.0000
   5.50000000       110.000000       5000.50000

real	0m0,019s
user	0m0,012s
sys	0m0,008s

Uh! per un milione di dati float, real*4 in Fortran 77, il tempo di esecuzione è troppo piccolo per essere significativo. E questa è la versione lenta, ecco quella corretta, col-row.f

      program col_row

      integer row, col
      real m(1000,1000)

      do row = 1, 1000
          do col = 1, 1000
              m(col, row) = col + 10 * row
          end do
      end do

      print*, m(1, 1), m(2, 200), m(1000, 1)

      do row = 1, 1000
          do col = 1, 1000
              m(col, row) = m(col, row) / 2
          end do
      end do

      print*, m(1, 1), m(2, 200), m(1000, 1)

      end

Compilo ed eseguo:

$ gfortran -o col-row col-row.f
$ time ./col-row
   11.0000000       2002.00000       1010.00000
   5.50000000       1001.00000       505.000000

real	0m0,018s
user	0m0,018s
sys	0m0,000s
$ time ./col-row
   11.0000000       2002.00000       1010.00000
   5.50000000       1001.00000       505.000000

real	0m0,017s
user	0m0,005s
sys	0m0,013s
$ time ./col-row
   11.0000000       2002.00000       1010.00000
   5.50000000       1001.00000       505.000000

real	0m0,017s
user	0m0,013s
sys	0m0,004s

sì, migliore! di quanto? pochissimo!

Non siamo più ai tempi del PDP-11, nemmeno a quelli del VAX suo successore e la RAM oggi è chip e abbondante. Ecco perché il Web è pieno di foto di gatti, mica come ao tempi di Lenna!

OK, questo per il Fortran, entri Python, con Numpy.

Prima versione col-row.py

#!/usr/bin/python3

import numpy as np

M = np.arange(1000*1000, dtype=float).reshape(1000,1000)
for row in range(1000):
    for col in range(1000):
        M[col, row] = col + 1 + 10 * (row + 1)

print(M[0, 0], M[1, 199], M[999, 0])

for row in range(1000):
    for col in range(1000):
        M[col, row] = M[col, row] / 2

print(M[0, 0], M[1, 199], M[999, 0])

Eseguo:

$ time py3 col-row.py
11.0 2002.0 1010.0
5.5 1001.0 505.0

real	0m0,811s
user	0m0,798s
sys	0m0,012s
$ time py3 col-row.py
11.0 2002.0 1010.0
5.5 1001.0 505.0

real	0m0,807s
user	0m0,781s
sys	0m0,020s
$ time py3 col-row.py
11.0 2002.0 1010.0
5.5 1001.0 505.0

real	0m0,798s
user	0m0,776s
sys	0m0,016s

Seconda versione row-col.py

#!/usr/bin/python3

import numpy as np

M = np.arange(1000*1000, dtype=float).reshape(1000,1000)
for row in range(1000):
    for col in range(1000):
        M[row, col] = col + 1 + 10 * (row + 1)

print(M[0, 0], M[1, 199], M[999, 0])

for row in range(1000):
    for col in range(1000):
        M[row, col] = M[row, col] / 2

print(M[0, 0], M[1, 199], M[999, 0])
$ time py3 row-col.py
11.0 220.0 10001.0
5.5 110.0 5000.5

real	0m0,771s
user	0m0,755s
sys	0m0,016s
$ time py3 row-col.py
11.0 220.0 10001.0
5.5 110.0 5000.5

real	0m0,823s
user	0m0,800s
sys	0m0,024s
$ time py3 row-col.py
11.0 220.0 10001.0
5.5 110.0 5000.5

real	0m0,794s
user	0m0,774s
sys	0m0,020s

sì, Python è lento ma ampiamente accettabile.

OK, ho tutto quello che serve per la conversione; con Python sarà semplice aggiornare e aggiungere nuove funzionalità, i grafici, ma non solo. Per esempio capita (ahemm, sempre) di dover fare la matrice trasposta, quella con righe e colonne scambiate. A me che sono vecchio vedere come fa NumPy vengono le vertigini:

$ py3
>>> import numpy as np
>>> M = np.arange(12).reshape(3,4)
>>> M
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> M.T
array([[ 0,  4,  8],
       [ 1,  5,  9],
       [ 2,  6, 10],
       [ 3,  7, 11]])

Il manuale di NumPy è qui.

OK? vero che Python è (quasi) l’unico linguaggio in città. O almeno quello più sexy 🤩

Convertire codice Fortran in Python – 1

Si può fare ma c’è qualche aspetto da considerare.

Intanto le abitudini di una volta, complice una possibilità del Fortran non presente in Python. Nel Fortran (parlo delle versioni vecchie 77 e precedenti; il 77 è tuttora usato per quel che ne so) ci sono due tipi di sottoprogrammi: le funzioni e le subroutines. Queste ultime, affini alle funzioni C di tipo void erano di gran lunga le preferite. C’è una giustificazione: le variabili sono passate per indirizzo (reference, non so se i due termini sono rigorosamente equivalenti, questi erano quelli correnti). Esempio:

N = 8
CALL DOPPIO (N)
PRINT* N

SUBROUTINE DOPPIO (N)
N = 2 * N
END

Il risultato che ottengo è 16, cioè il valore di N modificato nella subroutine viene conservato nel main, è cambiato il valore memorizzato all’indirizzo della variabile e questo è quello che ho passato al sottoprogramma. Con Python (per quel che ne so) questo non è possibile, ecco qua:

$ py3
>>> def doppio (n):
...   n = 2 * n
...   print('in doppio:', n)
...
>>> n = 8
>>> doppio (n)
in doppio: 16
>>> print (n)
8

alla funzione doppio ho passato il valore 8 e al di fuori di questa il valore di n resta 8 indipendentemente da quanto avvenuto nel sottoprogramma. Ovvia la soluzione, la stesa da sempre, in Fortran:

N = 8
N = IDOPPIO (N)
PRINT* N

FUNTION IDOPPIO (N)
N = 2 * N
RETURN (N)
END

Notare che ho cambiato il nome alla funzione, variabili e funzioni sono di default REAL*4 tranne quelle il cui nome inizia con le lettere dell’intervallo [I..N] che sono INTEGER*2 o INTEGER*4 a seconda della versione e delle opzioni di compilazione. Caratteristico del Fortran è che la precisione viene espressa in byte e non in bit.

In Python  la funzione può ritornare una lista, torna comoda per le subroutines:

$ py3
>>> def pot(a, b, p):
...   a = a ** p
...   b = b ** p
...   return a, b
...
>>> r = pot(3, 4, 2)
>>> print(r)
(9, 16)
>>> # ovviamente
... a, b = pot(3, 4, 2)
>>> print(a)
9
>>> print(b)
16

Quindi una prima regola pratica: scrivere funzioni che ritornano i valori delle variabili che ci servono. Ma ci sono dei casi in cui questa pratica non basta, il caso di arrays e matrici di dimensioni rilevanti.

In Fortran c’è l’istruzione COMMON:

COMMON /DATI/ A, B(10), C, D
COMMON /MAT/ P(20, 20), R(10, 10, 10)

Ogni funzione o subroutine che usi A, B, C o D dovrà dichiarare il COMMON /DATI/, idem per il caso di /MAT/.

In Python si ottiene un risultato simile (meno granulare, meno raffinato ma forse meno soggetto a bug) con le variabili globali:

$ py3
>>> #a = 0
>>> b = []
>>>
>>> def popola_dati():
...   global a
...   a = 42
...   for c in range(10):
...     b.append(c ** 2)
...
>>>
>>> popola_dati()
>>> print('a =', a)
a = 42
>>> print('b =', b)
b = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

OK, notare che a devo dichiararla global nella funzione a differenza di b che dev’essere inizializzato nel main e diventa automaticamente globale. b è una lista, non esattamente l’equivalente degli arrays e matrici del Fortran. Le liste sono versatili ma onerose, da modificare, in un post successivo, pausa 😎

Il caso di case

Ogni tanto capita di dover aggiornare un programma vecchio, adeguarlo, ampliarlo. Capita anche che deve funzionare anche con Linux e allora il dilemma se modificarlo o rifarlo ex-novo non si pone.

Usiamo cosa? Python, ovvio (ahemmm… quasi ma questa volta sì). E salta fuori la solita storia che adesso provo ancora una volta a dire che secondo me non ha ragione d’essere.

Il programma vecchio, in Visual Basic, quindi solo Windows contiene (semplifico, riduco all’essenziale) questo codice:

Select Case N
  case 0 To 1
    Print "piccolo"
  Case 2
    Print "giusto"
  Case 3, 4
    Print "grosso"
  Case Else
    Print "valori tra 0 e 4"
End Select

Select/Case è la versione Basic di switch/case del C e linguaggi da esso derivati. Per esempio JavaScript:

n = parseInt(process.argv[2]);
switch(n) {
  case 0 : case 1:
    console.log("troppo piccolo");
    break;
  case 2:
    console.log("giusto!");
    break;
  case 3: case 4:
    console.log("troppo grande");
    break;
  default:
    console.log("valori validi da 0 a 4");
}
* case $ node case.js 0
troppo piccolo
* case $ node case.js 1
troppo piccolo
* case $ node case.js 2
giusto!
* case $ node case.js 4
troppo grande
* case $ node case.js 100
valori validi da 0 a 4
* case $ node case.js -100
valori validi da 0 a 4

Con Bash è anche più sinttetico, ma non gira su Windows (cioè non su tutti i ‘puters, non su quello del Boss).

N=$1
echo "caso" $N
case $N in
  0 | 1) echo "piccolo" ;;
  2) echo "giusto" ;;
  3 | 4) echo "grande" ;;
  *) echo "NO! valori [0 .. 4]" ;;
esac

Python non ha un’istruzione simile, perché non serve:

from sys import argv
n = int(argv[1])

if n in [0, 1]   : print("troppo piccolo")
elif n == 2      : print("giusto!")
elif n in [3, 4] : print("troppo grande")
else             : print("valori validi da 0 a 4")

if/elif/else. Secondo me è anche molto leggibile (OK, dipende dai gusti). Una soluzione simile è immediata con JavaScript:

n = parseInt(process.argv[2]);

if ([0, 1].indexOf(n) >= 0)      { console.log("troppo piccolo"); }
else if (n == 2)                 { console.log("giusto!"); }
else if ([3, 4].indexOf(n) >= 0) { console.log("troppo grande"); }
else                             { console.log("valori validi da 0 a 4"); }

Vero che è più bello della versione case?

Le graffe …, direbbe un C-ista o un programmatore Java e/o JS. Ma anche un pythonista potrebbe andare a capo dopo il :. E il codice vero sarebbe più complesso, su più righe. Certo; questo è solo un esempio per dire che **case non serve quasi mai.

Ma sai che il Lisp… OK, ma il Lisp è diverso.

Un po’ di quirks con Python

Recentemente un tweet intercettato via un retweet di qualche tweep mi ha coinvolto in discussioni senza troppo senso ma –credo– da riportare.

KDnuggets visualizza una serie di tricks dichiarando che Most of the tricks and tips are taken from PyTricks and a few blogs.

Ecco ‏PyTricks di Max, brennerm. L’unico Max Brenner trovato su Twitter è una catena di bar, non credo sia chi sto cercando.

Ma mai disperare, una rapida googlata ed eccolo, qui. C’è –c’era– anche su Twitter ma sembra sia stata solo una prova, 42 tweets, l’ultimo vecchio di un anno.

OK, torno ai suggerimenti Python. L’inizio è stato l’inversione della stringa:

>>> st = '0123456789'
>>> rst = st[::-1]
>>> st
'0123456789'
>>> rst
'9876543210'

Con due giovani promesse della CS (ragazzi dovreste bloggare, imho) abbiamo cercato di mettere i valori appropriati nei camp i(start e stop) lasciati ai valori di default senza riuscirci. Perché pare che davvero non si può, vedi qui.

In Python c’è il metodo reverse() per le sequenze mutabili (ma le stringhe sono immutabili).

>>> ls = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> sl = ls.reverse()
>>> sl
>>> ls
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

uh! non quello che mi aspettavo (RTFM!): The reverse() method modifies the sequence in place for economy of space when reversing a large sequence. To remind users that it operates by side effect, it does not return the reversed sequence.

Altro trick intrigante è la trasposizione di una matrice. Max riporta:

>>> original = [['a', 'b'], ['c', 'd'], ['e', 'f']]
>>> transposed = zip(*original)
>>> print(list(transposed))
[('a', 'c', 'e'), ('b', 'd', 'f')]
>>> transposed
<zip object at 0x7f150fbd4448>

che, ovviamente, funziona. Il guaio è che che il codice non dice tutto quel che vorrei.

>>> orig = [['a', 'b'], ['c', 'd'], ['e', 'f']]
>>> trans = [*zip(*orig)]
>>> trans
[('a', 'c', 'e'), ('b', 'd', 'f')]

Ecco mancava un *: An asterisk * denotes iterable unpacking. Its operand must be an iterable. The iterable is expanded into a sequence of items, which are included in the new tuple, list, or set, at the site of the unpacking. [qui]. Conviene anche ripassare qui.

Mi sa che è da vedere anche zip(). Qui, qui, qui e qui.
Ovviamente dopo la documentazione ufficiale (già detto RTFM?), qui.

Ci sono altri suggerimenti, interessanti quelli sui dizionari; ma anche altri. Tutti.

Un quiz & considerazioni personali relative

Cosa non si trova ontehtoobz! (auto-cit.) 😯 Ieri questo: I was just shown this. Guess what this does in Python? powers = [lambda x: x**i for i in range(10)] powers[3](2).

Sukant è conosciuto, in diversi socials, rockz. E, confesso, ho provato a risolvere il suo quiz, nel senso di correggere la funzione, senza riuscirci. Poi ho letto i commenti, già David A Roberts 💥 doveva mettermi sulla retta via:

* quiz $ py3
>>> def f(i): return lambda x: x**i
...
>>> powers = [f(i) for i in range(10)]
>>> powers[3](2)
8

ma ci sono altri come me che non sono così pronti: How is it Alex Jones got banned, and yet Sukant still has a twitter account after posting this?? dice Warner e Leftmost Grief Node: Haha the Pythonistas responding to this in my org are like, “Well that code is not Pythonic so…”.

das portal ci spiega il perché, Dan P ci dice che Scala…

Ma la risposta migliore è quella di jeeger che si potrebbe tradurre con *RTFM!*, in questo caso The Hitchhiker’s Guide to Python, in particolare –chiaro come il sole– Late Binding Closures.

Non copio è là.
Da leggere attentamente, anzi studiare che poi interrogo, nèh! 🧐

Ci ho pensato su. Anzi ci ho dormito su. E ho maturato una mia opinione che potrò cambiare se e solo se fatti o circostanze me lo consiglieranno.

In questo caso –e tanti altri casi simili– il problema non esiste:

>>> pow(2, 3)
8
>>> pow(2, 0.5)
1.4142135623730951
>>> pow(-2, 0.5)
(8.659560562354934e-17+1.4142135623730951j)

Non ce l’ho con lambda e le funzioni e quant’altro ma con il loro (ab)uso. E usare solo quello che si conosce davvero. Detto in altri termini non ho ancora digerito il fatto che non ho trovato il bug 👿

Comunque…

Siate semplici (cit.). Anzi, qui. È talmente autoevidente che non si sa di chi è; è Patrimonio dell’Umanità (se non lo è dovrebbe esserlo).

Formattare le stringhe in Python

Python lo uso da sempre (cioè no, ho solo dimenticato quando l’ho scoperto (o meglio il numero della versione che mi sembra di ricordar è troppo basso per essere credibile) e capita che ogni tanto scopro di non essere aggiornato, sapete l’evilussione 🧐 Per fortuna c’è Twitter che mi suggerisce, via prof Corno ecco: Python 3’s f-Strings: An Improved String Formatting Syntax (Guide).

La documentazione ufficiale corrente di Python (3.6 o mi sto perdendo qualcosa anche qui? sì. c’è la 3.7 nuova di pakka, non ancora installata) è aggiornata e dice tutto, anche se in modo un po’ troppo sintetico per quelli come me.

A formatted string literal or f-string is a string literal that is prefixed with ‘f‘ or ‘F‘.

Conviene quindi seguire la guida di RealPython, anzi RealPython è da followare su Twitter, fatto.

Anzi, la guida è talmente OK che non continuo a scrivere, c’è tutto là. A me resterebbe di aggiornare gazillioni di script che usano non immaginate cosa (l’evilussione) ma poi mi viene da pensare che (cit.)

anche perché capita troppo spesso che parto con l’idea di aggiornare e mi scopro a riscrivere ⭕

Perché il ciclo for di Python è speciale

Il titolo avrebbe dovuto essere “perché range(start, end) non comprende end?” ma si sarebbe perso il riferimento a for.

Forse lo sanno tutti, forse qualcuno l’ha scoperto da poco, altri ancora forse no. E perché?  Sto dicendo del ciclo for in Python.

Nella riscrittura di in Python di un programmino Basic c’è stata un po’ di confusione sugli indici di arrays e contatori di cicli.

Utilizzo MY-BASIC di Wang Renxin per visualizzare il ciclo for come lo intende il Basic, in tutte le sue varianti e incarnazioni, in particolare nel mio caso si tratta di VB, Visual Basic.

run e bye sono estensioni di MY-BASIC, il comando mb invece è un alias mio ma l’esempio –minimo– illustra come il Basic gestisce il ciclo, in particolare il ciclo comprende il limite finale, 5 in questo caso. Lo stesso comportamento lo troviamo nel linguaggio da cui il Basic deriva, il Fortran (dove il ciclo si chiama do). L’istruzione può prevedere un passo per l’indice, così:

OK? Ma questa è un’eccezione, dal C in poi il ciclo for ha una sintassi diversa, per esempio con nodejs (un sapore di JavaScript) posso scrivere:

Ovviamente si può scrivere <= al posto di < comprendendo così il limite superiore. Con C e C++ la sintassi è la stessa, migliora solo l’istruzione di scrittura, printf() e/o cout <<.

Il for per Python è diverso, funziona con le liste:

Potrei usare una lista di numeri:

Per creare una lista di numeri, come quelle che si trovano di solito nei cicli for esiste la funzione range().

The range type represents an immutable sequence of numbers and is commonly used for looping a specific number of times in for loops.

Come si vede il limite finale non è mai compreso. Per cui il ciclo iniziale (quello dei numeri da 1 a 5) sarà:

Visto 6 e non 5.

C’è un potivo per tutto ciò, il BDFL (emerito da due giorni) Guido la sa lunga:

in questo modo la lunghezza (len()) della lista è uguale al limite superiore del range(). Altre cosa da ricordare (già usata implicitamente ma è bene ricordarla per quelli come me che vengono dal Fortran) è che gli indici partono da 0, zero.

Questo è spiegato più in dettaglio qui.

E per chi vuole saperne di più passo la parola a EDW, qui: Why numbering should start at zero.

Sostituire i TABs e i caratteri non-ASCII

A volte il testo preso dal Web o da altri posti ancora non è come lo vorremmo. Capita che contenga caratteri strani, per esempio TABs, caratteri Unicode (a volte sono OK, a volte no, per esempio nel codice apici e virgolette devono essere quelli classici anche se meno belli), λ (OK ma non nella REPL di Racket (sì, questo solo per me)). Ecco il terminale è uno di quelli che certe cose proprio non le accetta.

Vero che in (quasi) qualsiasi editor si può fare ma se del caso…

Comincio con TAB, serve come base poi generalizzabile (file stab):

#!/bin/bash

if [ -z $1 ]; then
  echo 'uso:' $0 '[sost-char] file'
  exit 2
elif [ -z $2 ]; then
  SOST=' '
  PROC=$1
else
  SOST=$1
  PROC=$2
fi

sed "s/\t/$SOST/g" $PROC

Lanciato senza opzioni esce con un messaggio:

È richiesto il nome del file preceduto opzionalmente con il carattere (o i caratteri) che sostituiranno il TAB.

OK? C’è (ancora, pare che adesso Windows 10 sia sulla buona strada) chi preferisce Python. Si può fare; anzi lo script risulta simile (stab.py):

#!/usr/bin/python3

import sys, re

if len(sys.argv) == 1:
  print ('uso:', sys.argv[0], '[sost-char] file')
  exit(2)
elif len(sys.argv) == 2:
  sost = ' '
  proc = sys.argv[1]
else:
  sost = sys.argv[1]
  proc = sys.argv[2]

with open(proc, 'r') as f:
  txt = f.readlines()

for st in txt:
  print(re.sub('\t', sost, st), end='')

È possibile evitare l’uso delle espressioni regolari (modulo re che ho preferito per uniformità con lo script seguente): basta sostiture l’ultimo ciclo for con

for st in txt:
  print(st.replace('\t', sost), end='')

Generalizzando arrivo ad andare alla ricerca di tutti i caratteri fuori dalla sequenza ASCII. Ho copiato in un file un paio di tweets (dimenticando di salvare il nome degli autori) contenenti citazioni quotate. Nel primo si usano le virgolette che si trovano nella stampa tedesca, nel secondo no, roba corrente ontehtoobz. Ho aggiunto un paio di emoji, ormai hanno invaso il Web. Poi passo a evidenziali:

Questo è lo script fuori.py:

#!/usr/bin/python3

import sys, re

if len(sys.argv) == 1:
  print ('uso:', sys.argv[0], '[sost-char] file')
  exit(2)
elif len(sys.argv) == 2:
  sost = '_'
  proc = sys.argv[1]
else:
  sost = sys.argv[1]
  proc = sys.argv[2]

with open(proc, 'r') as f:
  txt = f.readlines()

for st in txt:
  print(re.sub(r'[^\x00-\x7f]', sost, st), end='')

Procedo come per il TAB ma con sed ci sono problemi, dovuti alla tabella caratteri caricata se abbiamo capito bene. Ancora da indagare (e risolvere), materia per un prossimo post.

OK, 😁⭕

Globale o locale?

Jake 💥 rockz e oggi ci racconta di un caso che neanche Dirk Gentlyquesto.

Lo riscrivo con una piccola modifica (l’istruzione print per vedere il risultato), file e0.py:

def g(x):
  def f():
    x += 1
  f()

print(g(0))

Err! 😡, e come dice Jake: That was a fun one to explain 😀. Dico subito che da solo non ci sono arrivato; poi mi sono detto che dovevo pensarci ma…

Provo a fare come avrei dovuto. Invece di operare sulla variabile (globale) x nella f() ritorno il valore di x modificato, così (file e1.py):

def g(x):
  def f():
    return x + 1
  f()

print(g(0))

OOPS! per Python è OK ma non per me, correggo (e2.py):

def g(x):
  def f():
    return x + 1
  return f()

print(g(0))

Chiaro? Torno al tweet, nel thread c’è la dritta, il link a –indovina– la documentazione ufficiale di Python, qui: Why am I getting an UnboundLocalError when the variable has a value?

La spiegazione è diversa (più esaustiva) della mia, in sintesi:

This is because when you make an assignment to a variable in a scope, that variable becomes local to that scope and shadows any similarly named variable in the outer scope. Since the last statement in foo assigns a new value to x, the compiler recognizes it as a local variable. Consequently when the earlier print(x) attempts to print the uninitialized local variable and an error results.

Logico. E con certi linguaggi –nuovi, di moda– le variabili, quando ci sono, sono quelle, restano tali, niente modifiche. Per esempio Haskell –lasciato come esercizio. È da tanto che volevo dire questa frase 😊.

Invece in un’altra famiglia di linguaggi –sexy, i miei preferiti– non serve scrivere “return“, la funzione è un valore (file e3.rkt):

(define (g x)
  (define (f)
    (+ 1 x))
    (f))

(displayln (g 0))

Ma nessun lisper userebbe la funzione f(), viene chiamata una volta sola, per questo c’è lambda (e4.rkt):

(define (g x)
  ((lambda (t) (+ 1 t)) x))

(displayln (g 0))

Non mi piace, inutilmente contorta, basta (e5.rkt):

(define (g x)
  (+ 1 x))

(displayln (g 0))

Dove ho semplificato (troppo? f e g in un programma reale conterranno altre istruzioni) il codice. Ma era ridondante, lambda si usa quando serve.

Poi ci ho ripensato ancora (tutta la notte) e sono giunto a una conclusione che forse vale solo per me ma penso che il codice del tweet sia sbagliato come progettazione. Se x dev’essere globale si deve dichiarare, così (e6.py):

def g(x):
  def f():
    global x
    x += 1
  f()

# main
x = 0
g(0)
print(x)

Ovviamente il parametro in g() è inutile e dannoso come si scoprirà nel debug della prima revisione (e7.py):

def g(x):
  def f():
    global x
    x += 1
  f()

# main
x = 0
g(42) # <- ====
print(x)