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 iterazioneStopIteration
.
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?
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
.
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()
Riepilogo
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!