Articolo originale: How not to be afraid of Python anymore

Un'immersione nella documentazione di riferimento del linguaggio

Per il primo anno o due quando ho iniziato a programmare, pensavo che imparare un linguaggio significasse solo impararne la sintassi. Quindi è tutto quello che feci.

Inutile dire che non sono diventato un grande sviluppatore. Ero bloccato. Poi, un bel giorno, qualcosa è scattato. Mi sono reso conto che stavo sbagliando. Imparare la sintassi dovrebbe essere l'ultima delle mie preoccupazioni. Ciò che conta è tutto il resto del linguaggio. Cos'è esattamente tutto ciò? Continua a leggere.

Questo articolo è diviso in tre sottosezioni principali: il Modello Dati, il Modello di Esecuzione e l'Analisi Lessicale.

Questo articolo è più una panoramica su come funzionano le cose nel mondo Python, non su come imparare Python. Troverai molte risorse di apprendimento online.

Quello che non ho trovato online era un'unica fonte di "trucchi" comuni in Python. Una fonte che spiega come funziona il linguaggio. Questo articolo tenta di risolvere quel problema. Non penso di esserci riuscito pienamente, c'è così tanto da fare!

Tutto quanto espresso qui proviene dalla documentazione ufficiale. L'ho condensato, nei punti importanti, ho riordinato le cose e ho aggiunto i miei esempi. Tutti i collegamenti puntano alla documentazione (in lingua inglese).

Senza ulteriori indugi, iniziamo.

Il Modello Dati

Oggetti, valori e tipi

Gli oggetti sono l'astrazione di Python per i dati.

Ogni oggetto ha la sua unica identità fissa, un tipo fisso e un valore.

‘Fisso’ vuol dire che l'identità e il tipo di un oggetto non possono mai cambiare.

Il valore potrebbe cambiare. Gli oggetti il cui valore può cambiare vengono detti mutabili, quelli che non possono cambiare il proprio valore sono detti immutabili.

La mutabilità è determinata dal tipo:

  • Numeri, stringhe e tuple sono immutabili
  • Liste e dizionari sono mutabili

L'identità degli oggetti può essere confrontata tramite l'operatore is.

id() restituisce l'identità

type() restituisce il tipo

Nota: Il valore di un oggetto contenitore immutabile  che contiene un riferimento a un oggetto mutabile può cambiare quando il valore di quest'ultimo viene cambiato. Tuttavia il contenitore è sempre considerato immutabile, perché la collezione di oggetti che contiene non può essere modificata. Pertanto l'immutabilità non è esattamente lo stesso che avere un valore non modificabile.

Questa nota mi ha fatto girare la testa le prime due volte che l'ho letta.

In parole povere: l'immutabilità non è lo stesso di un valore non modificabile. Nell'esempio qui sotto, la tupla t è immutabile, mentre il suo valore continua a cambiare (al cambiare del contenuto della lista in posizione t[1] ).

Esempio:

>>> t = ("a", [1]) # una tupla composta da una stringa e una lista
>>> id(t)
4372661064
>>> t
('a', [1])
>>> type(t)
<class 'tuple'>
>>> t[1]
[1]
>>> t[1].append(2)
>>> t
('a', [1, 2])
>>> id(t)
4372661064
>>> type(t)
<class 'tuple'>

La tupla è immutabile, come si vede dal valore del suo id, anche se contiene un oggetto mutabile, una lista.

Viceversa per una stringa, cambiando l'array di caratteri esistente cambia l'oggetto (visto che le stringhe sono immutabili).

>>> x = "abc"
>>> id(x)
4371437472
>>> x += "d"
>>> x
'abcd'
>>> id(x)
4373053712

Qui la variabile  x è associata a un altro oggetto di tipo stringa, di conseguenza il suo id cambia.

L'oggetto originale, essendo immutabile, non cambia. L'associazione viene spiegata più dettagliatamente di seguito, il che dovrebbe chiarire le cose.

Tipi integrati

Python dispone di parecchi tipi integrati:

None

Il tipo è rappresentato da un singolo oggetto, quindi da un singolo valore. Questo è l'unico oggetto con tipo uguale a NoneType

>>> type(None)
<class 'NoneType'>

Numeri

Questa è una collezione di classi base astratte usate per rappresentare numeri. Non possono essere istanziate e int e float ereditano da numbers.Number.

Sono create tramite numeri letterali e operazioni aritmetiche. Gli oggetti ritornati sono immutabili, come abbiamo visto. Gli esempi che seguono chiariranno il concetto:

>>> a = 3 + 4
>>> type(a)
<class 'int'>
>>> isinstance(a, numbers.Number)
True
>>> isinstance(a, numbers.Integral)
True
>>> isinstance(3.14 + 2j, numbers.Real)
False
>>> isinstance(3.14 + 2j, numbers.Complex)
True

Sequenze

Rappresentano insiemi finiti e ordinati indicizzati da numeri interi non negativi. Proprio come gli array in altri linguaggi.

len() ritorna la lunghezza delle sequenze. Quando la lunghezza è n, l'indice dell'insieme ha elementi da 0...n-1 . Pertanto l'ennesimo elemento viene selezionato con seq[i-1].

Per una sequenza l, puoi selezionare un sottoinsieme di elementi al suo interno usando la tecnica di slicing : l[i:j] seleziona gli elementi nella sequenza che hanno indice da i a j (escluso).

Ci sono due tipi di sequenze: mutabili e immutabili.

  • Le sequenze immutabili comprendono stringhe, tuple e byte.
  • Le sequenze mutabili includono liste e array di byte

Insiemi (set)

Rappresentano insiemi finiti non ordinati di oggetti immutabili univoci. Non possono essere indicizzati, ma su di essi si può iterare. Anche qui len() ritorna il numero di elementi in un set.

Ci sono due tipi di set: mutabili e immutabili.

  • Un set mutabile viene creato con set().
  • Un set immutabile viene creato con frozenset().

Mappature

Dizionari

Rappresentano insiemi finiti di oggetti indicizzati con valori (chiavi) pressoché arbitrari. Le chiavi non possono essere oggetti mutabili. Questo include liste, altri dizionari e altri oggetti che possono essere confrontati per valore, non per identità dell'oggetto.

Il che significa che anche un frozenset può costituire una chiave di un dizionario!

Moduli

Un oggetto modulo è l'unità base organizzativa in Python. Il nome dello spazio viene implementato come un dizionario. I riferimenti agli attributi sono ricerche in questo dizionario.

Per un modulo m, il dizionario è di sola lettura, a cui si accede con m.__dict__.

È un normale dizionario, quindi puoi anche aggiungere delle chiavi!

Ecco un esempio, tratto dallo Zen di Python:

Ora aggiungiamo la nostra funzione personalizzata, figure() al modulo this.

>>> import this as t
>>> t.__dict__
{'__name__': 'this', '__doc__': None, '__package__': '',
.....
.....
's': "Gur Mra bs Clguba, ol Gvz Crgref\n\nOrnhgvshy vf orggre guna
vqrn.\nAnzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!",
'd': {'A': 'N', 'B': 'O', 'C': 'P', 'D': 'Q', 'E': 'R', 'F': 'S', 
'u': 'h', 'v': 'i', 'w': 'j', 'x': 'k', 'y': 'l', 'z': 'm'},
'c': 97,
'i': 25
}
>>> def figure():
...   print("Can you figure out the Zen of Python?")
... 
>>> t.fig = figure
>>> t.fig()
Can you figure out the Zen of Python?
>>> t.__dict__
{'__name__': 'this', '__doc__': None, '__package__': '',
.....
.....
's': "Gur Mra bs Clguba, ol Gvz Crgref\n\nOrnhgvshy vf orggre guna
vqrn.\nAnzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!",
'd': {'A': 'N', 'B': 'O', 'C': 'P', 'D': 'Q', 'E': 'R', 'F': 'S', 
'u': 'h', 'v': 'i', 'w': 'j', 'x': 'k', 'y': 'l', 'z': 'm'},
'c': 97,
'i': 25
'fig': <function figure at 0x109872620>
}
>>> print("".join([t.d.get(c, c) for c in t.s]))
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Non molto utile, ma buono a sapersi.

Sovraccarico degli Operatori (operator overloading)

Python consente il sovraccarico dell'operatore (operator overloading).

Le classi hanno nomi di funzione speciali, metodi che possono implementare per usare gli operatori definiti da Python. Questo include slicing, operazioni aritmetiche e di sottoscrizione.

Per esempio, __getitem__() fa riferimento alla sottoscrizione. Pertanto, x[i] equivale a type(x).__getitem__(x,i).

Quindi per usare l'operatore [] su una classe someClass: devi definire __getitem__() in someClass.

>>> class operatorTest(object):
...     vals = [1,2,3,4]
...     def __getitem__(self, i):
...         return self.vals[i]
... 
>>> x = operatorTest()
>>> x[2]
3
>>> x.__getitem__(2)
3
>>> type(x)
<class '__main__.OperatorTest'>
>>> type(x).__getitem__(x,2)
3
>>> OperatorTest.__getitem__(x,2)
3

Confuso sul motivo per cui sono tutti equivalenti? Questo è per la parte successiva, dove trattiamo le definizioni di classi e funzioni.

Alla stessa stregua, la funzione __str__() determina il risultato quando viene chiamato il metodo str() su un oggetto della tua classe.

Per gli operatori di confronto, i nomi delle funzioni speciali sono:

  • object.__lt__(self, other) per < (“minore di”)
  • object.__le__(self, other) per <= (“minore di o uguale a”)
  • object.__eq__(self, other) per == (“uguale a”)
  • object.__ne__(self, other) per != (“non uguale a ”)
  • object.__gt__(self, other) per > (“maggiore di”)
  • object.__ge__(self, other) per >= (“maggiore di o uguale a”)

Quindi per esempio, x < y viene chiamato come x.__lt__(y)

Ci sono anche funzioni speciali per le operazioni aritmetiche come l'addizione object.__add__(self, other).

Per esempio, x+y viene chiamato con x.__add__(y)

Un'altra funzione interessante è __iter__().

Chiami questo metodo quando ti serve un iteratore per un contenitore. Restituisce un nuovo oggetto di iterazione che può iterare su tutti gli oggetti nel contenitore.

Per le mappature, dovrebbe iterare sulle chiavi del contenitore.

L'oggetto iteratore stesso supporta due metodi:

  • iterator.__iter__(): restituisce l'oggetto stesso.

Il che rende gli iteratori e i contenitori equivalenti.

Questo consente a iteratori e contenitori di essere entrambi usati nelle istruzioni for e in.

  • iterator.__next__() : restituisce l'elemento successivo nel contenitore. Se non ci sono ulteriori elementi, solleva un'eccezione di interruzione di iterazione StopIteration.
class IterableObject(object):    # Rappresenta un oggetto iterabile
     vals = []
     it = 0
     def __init__(self, val):
         self.vals = val
         it = 0
 
     def __iter__(self):
         return self
 
     def __next__(self):
         if self.it < len(self.vals):
             index = self.it
             self.it += 1
             return self.vals[index]
         raise StopIteration
 
 class IterableClass(object):    # La classe contenitore
       vals = [1,2,3,4]
 
       def __iter__(self):
         return iterableObject(self.vals)
>>> iter_object_example = IterableObject([1,2,3])
>>> for val in iter_object_example:
...   print(val)
... 
1
2
3
>>> iter_container_example = IterableClass()
>>> for val in iter_container_example:
...  print(val)
... 
1
2
3
4

Roba interessante, vero? C'è anche un equivalente diretto in Javascript.

Anche i gestori di contesto (Context Manager) sono implementati tramite sovraccarico dell'operatore.

with open(filename, 'r') as f

open(filename, 'r') è un oggetto gestore di contesto che implementa

object.__enter__(self) e

object.__exit__(self, exc_type, exc_value, traceback)
Tutti e tre i parametri qui sopra sono nulli quando non ci sono errori.

class MyContextManager(object):
    def __init__(self, some_stuff):
        self.object_to_manage = some_stuff
    def __enter__(self):
        print("Entrata nella gestione del contesto")
        return self.object_to_manage # si possono fare anche delle trasformazioni
    
    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            print("Uscita con successo")
            # Altre cose da chiudere
>>> with MyContextManager("file") as f:
...     print(f)
... 
Entrata nella gestione del contesto
file
Uscita con successo

Questo non è utile, ma fa capire il punto. Questo lo rende comunque utile?

vJ7yWCli55L8Sn1i-cnzBNyjXJm8quKKcPRq
Philosoraptor

Modello di Esecuzione

Un blocco è un pezzo di codice eseguito come una unità in un frammento di esecuzione.

Esempi di blocchi includono:

  • Moduli, che sono blocchi a livello più alto
  • Corpi di funzione
  • Definizioni di classe
  • NON cicli for e altre strutture di controllo

Ricordi come tutto sia un oggetto in Python?

Bene, hai nomi associati a questi oggetti. Questi nomi sono ciò che consideri come variabili.

>>> x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

L'associazione al nome, o assegnazione, si verifica in un blocco.

Esempi di associazione di nomi – questi sono intuitivi:

  • I parametri delle funzioni sono associati ai nomi definiti nella funzione
  • Le istruzioni di importazione associano il nome del modulo
  • Definizioni di classi e funzioni associano il nome agli oggetti classe/funzione
  • Gestori di contesto: with ... as f : f è il nome associato all'oggetto ...

I nomi associati a un blocco sono locali a quel blocco. Ciò significa che le variabili globali sono semplicemente nomi associati al modulo.

Variabili usate in un blocco senza esservi definite sono variabili libere.

Gli ambiti definiscono la visibilità di un nome in un blocco. L'ambito di una variabile include il blocco nel quale è stata definita, così come tutti i blocchi contenuti all'interno del blocco di definizione.

Ricordi che i cicli for non sono blocchi? Ecco perché le variabili di iterazione definite nel ciclo sono accessibili al di fuori dello stesso, al contrario di C++ e JavaScript.

>>> for i in range(5):
...   x = 2*i
...   print(x, i)
... 
0 0
2 1
4 2
6 3
8 4
>>> print(x, i)    # al di fuori del ciclo! x era stato definito all'interno.
8 4

Quando un nome viene usato in un blocco, viene risolto usando l'ambito di inclusione più prossimo.

Nota: se un'operazione di associazione di un nome si verifica da qualunque parte all'interno di un blocco di codice, tutti gli utilizzi di quel nome all'interno del blocco sono considerati come riferimenti al blocco corrente. Questo può portare a errori quando un nome viene usato all'interno di un blocco prima della sua assegnazione.

Per esempio:

>>> name = "outer_scope"
>>> def foo():
...     name = "inner_function" if name == "outer_scope" \
                else "not_inner_function"
... 
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'name' referenced before assignment

Questo è un bellissimo traceback: la variabile locale 'name' è stata referenziata prima della sua assegnazione", il che ora dovrebbe avere un senso.

Abbiamo il blocco al livello superiore, il modulo, nel quale c'è un altro blocco, la funzione. Ogni associazione all'interno della funzione ha la funzione come ambito di livello più alto!

Quindi, quando stai associando il nome name all'oggetto "inner_function", stai verificando il suo valore prima dell'assegnazione. La regola dice che non puoi referenziare name prima dell'associazione. Esattamente la ragione per l'errore UnboundLocalError.

3vBeauEpskRngPsPSWn2ww0KsgA3mNCGGzjl
Not this kind of Execution Model? Source

Analisi Lessicale

Python ti consente di  unire le righe di codice. Per considerare esplicitamente la riga successiva la prosecuzione di quella corrente devi aggiungere una barra rovesciata alla fine della riga corrente.

Non sono consentiti commenti dopo l'unione di righe.

if a < 10 and b < 10 \ # Questo commento genera un errore SyntaxError
and c < 10: # Questo commento va bene
    return True
else:
    return False

Implicitamente vengono considerate righe unite quando i propri elementi sono racchiusi tra parentesi quadre. I commenti sono consentiti.

month_names = ['Januari', 'Februari', 'Maart',      # Questi sono i
               'April',   'Mei',      'Juni',       # nomi in olandese
               'Juli',    'Augustus', 'September',  # per i mesi
               'Oktober', 'November', 'December']   # dell'anno

Indentazione

Il numero di spazi/caratteri di tabulazione nell'indentazione non importa, fintato che sia crescente per il codice che deve essere indentato. La prima riga non dovrebbe essere indentata.

La regola dei quattro spazi per l'indentazione è una convenzione definita dalla guida di stile  PEP 8: Style Guide. È buona pratica seguirla.

# Calcola l'elenco di permutazioni per l.
def perm(l):
        # L'indentazione del commento viene ignorata
    if len(l) <= 1:
                  return [l]
    r = []
    for i in range(len(l)):
             s = l[:i] + l[i+1:]     # Il livello di indentazione scelto
             p = perm(s)             # deve essere allo stesso livello di quello sopra 
             for x in p:
              r.append(l[i:i+1] + x) # Uno spazio va bene
    return r

Ci sono anche alcuni identificatori riservati.

  • _ per le importazioni: le funzioni / variabili che iniziano per  _ non vengono importate.
  • __*__ per nomi definiti dal sistema, definiti dall'implementazione, ne abbiamo visti alcuni ( __str__(), __iter__(), __add__() )

Python offre anche la concatenazione implicita di stringhe letterali

>>> def name():
...   return "Neil" "Kakkar"
...
>>> name()
'Neil Kakkar'

Formattazione Stringhe

La formattazione delle stringhe è un utile strumento in Python.

Le stringhe possono avere { expr } in una stringa letterale dove expr è un'espressione. La valutazione dell'espressione viene sostituita sul posto.

Le conversioni possono essere specificate per convertire il risultato prima della formattazione.

!r chiama repr(), !s chiama str() e !a chiama ascii()

>>> name = "Fred"
>>> f"He said his name is {name!r}."
"He said his name is 'Fred'."
>>> f"He said his name is {repr(name)}."  # repr() equivale a !r
"He said his name is 'Fred'."
>>> width = 10
>>> precision = 4
>>> value = decimal.Decimal("12.34567")
>>> f"result: {value:{width}.{precision}}"  # campi annidati
'result:      12.35'
# Uguale a "{decf:10.4f}".format(decf=float(value))
>>> today = datetime(year=2017, month=1, day=27)
>>> f"{today:%B %d, %Y}"  # uso di uno specificatore di formato data
'January 27, 2017'
>>> number = 1024
>>> f"{number:#0x}"  # uso di uno specificatore di formato esadecimale
'0x400'

È una sintassi più pulita rispetto all'uso di str.format()

Con questo, abbiamo coperto i principali pilastri di Python. Il modello di dati, il modello di esecuzione con i suoi ambiti e blocchi e alcuni concetti sulle stringhe. Sapere tutto questo ti mette davanti a ogni sviluppatore che conosca solo la sintassi. È molto più di quanto pensi.

Altri articoli in questa serie:

Hai apprezzato? Non perdere ancora un altro post, iscriviti alla mia mailing list!