Python Avanzato Funzioni

Python Avanzato Funzioni' Il risultato condensato è

Come ti ingarbuglierai con Python. Foto di iam_os su Unsplash

Dopo aver letto il titolo, probabilmente ti chiedi qualcosa del tipo: “Le funzioni in Python sono un concetto avanzato? Come? Tutti i corsi introducono le funzioni come il blocco di base del linguaggio”. E hai ragione e torto allo stesso tempo.

La maggior parte dei corsi su Python introduce le funzioni come concetto di base e mattoncino perché, senza di esse, non saresti in grado di scrivere codice funzionale affatto. Questo è completamente diverso dal paradigma di programmazione funzionale, che è un concetto separato, ma ne parlerò anche in seguito.

Prima di addentrarci nelle intricatissime funzioni avanzate di Python, facciamo una breve panoramica di alcuni concetti di base e cose che probabilmente già conosci.

Nozioni di base

Quindi inizi a scrivere il tuo programma e ad un certo punto finisci per scrivere la stessa sequenza di codice. Inizi a ripeterti e a ripetere i blocchi di codice. Questo si rivela un buon momento e un buon posto per introdurre le funzioni. Almeno, così sembra. In Python, si definisce una funzione come:

def shout(nome):    print(f'Ehi! Il mio nome è {nome}.')

Nel mondo dell’ingegneria del software, facciamo una distinzione tra le parti della definizione di una funzione:

  • def – parola chiave di Python utilizzata per definire una funzione.
  • shout – nome della funzione.
  • shout(nome) – dichiarazione della funzione.
  • nome – argomento della funzione.
  • print(...) è parte del corpo della funzione o come lo chiamiamo definizione della funzione.

Una funzione può restituire un valore o non avere alcun valore di ritorno, come quella che abbiamo definito in precedenza. Quando una funzione restituisce un valore, può restituirne uno o più:

def break_sentence(frase):    return frase.split(' ')

Quello che ottieni come risultato è una tupla che puoi scomporre o scegliere uno qualsiasi degli elementi della tupla per procedere.

Per coloro che non sono informati, le funzioni in Python sono cittadini di prima classe. Cosa significa? Significa che puoi lavorare con le funzioni come faresti con qualsiasi altra variabile. Puoi passarle come argomenti ad altre funzioni, restituirle da funzioni e persino memorizzarle in variabili. Ecco uno degli esempi:

def shout(nome):    return f'Ehi! Il mio nome è {nome}.'# useremo break_sentence definita sopra# assegna la funzione ad un'altra variabileanother_breaker = break_sentence another_breaker(shout('John'))# ['Ehi!', 'Il', 'mio', 'nome', 'è', 'John.']# Wow! Sì, questo è un modo valido per definire una funzionename_decorator = lambda x: '-'.join(list(nome))name_decorator('John')# 'J-o-h-n'

Aspetta, cos’era questo lambda? Questo è un altro modo in cui puoi definire le funzioni in Python. Questa è la cosiddetta funzione anonima o senza nome. Bene, in questo esempio, la assegniamo ad una variabile chiamata name_decorator, ma puoi passare l’espressione lambda come argomento di un’altra funzione senza bisogno di darle un nome. Ne parlerò presto.

Ciò che rimane è dare un esempio di come le funzioni possono essere passate come argomenti o restituite come valori da un’altra funzione. Questa è la parte in cui ci stiamo avvicinando a concetti avanzati, quindi sii paziente.

def dash_decorator(nome):    return '-'.join(list(nome))def no_decorator(nome):    return nomedef shout(nome, decorator=no_decorator):    nome_decorato = decorator(nome)    return f'Ehi! Il mio nome è {nome_decorato}'shout('John')# 'Ehi! Il mio nome è John'shout('John', decorator=dash_decorator)# 'Ehi! Il mio nome è J-o-h-n'

Quindi così sembra quando si passano le funzioni come argomenti ad un’altra funzione. E la funzione lambda? Beh, guarda l’esempio successivo:

def shout(nome, decorator=lambda x: x):    nome_decorato = decorator(nome)    return f'Ehi! Il mio nome è {nome_decorato}'print(shout('John'))# Ehi! Il mio nome è Johnprint(shout('John', decorator=dash_decorator))# Ehi! Il mio nome è J-o-h-n

Ora la funzione di decorazione predefinita è lambda e restituisce il valore dell’argomento così com’è (idempotente). Qui, è anonima perché non ha un nome associato ad essa.

Nota che print è anche una funzione, e stiamo passando una funzione shout al suo interno come argomento. In sostanza, stiamo concatenando le funzioni. E questo può condurci ad un paradigma di programmazione funzionale, che è un percorso che puoi scegliere in Python. Cercherò di scrivere un altro post sul blog specificamente su questo argomento perché mi interessa molto. Per ora, continueremo con il paradigma di programmazione procedurale; cioè, continueremo con quello che abbiamo fatto finora.

Come già detto, una funzione può essere assegnata ad una variabile, passata come argomento ad un’altra funzione e restituita da quella funzione. Ti ho mostrato alcuni esempi semplici per i primi due casi, ma cosa succede se restituiamo una funzione da una funzione? All’inizio volevo mantenerlo davvero semplice, ma poi di nuovo, questo è un Python avanzato!

Parti intermedie o avanzate

Questo non sarà in alcun modo LA guida alle funzioni e ai concetti avanzati attorno alle funzioni in Python. Ci sono molti ottimi materiali, che lascerò alla fine di questo post. Tuttavia, voglio parlare di un paio di aspetti interessanti che ho trovato molto intriganti.

Le funzioni in Python sono oggetti. Come possiamo scoprirlo? Beh, ogni oggetto in Python è un’istanza di una classe che alla fine eredita da una classe specifica chiamata type. I dettagli di questo sono complessi, ma per poter vedere cosa c’entra con le funzioni, ecco un esempio:

type(shout)# functiontype(type(shout))# type

Quando si definisce una classe in Python, eredita automaticamente la classe object. E quale classe eredita object?

type(object)# type

E dovrei dirti che le classi in Python sono anche oggetti? Infatti, questo è sconvolgente per i principianti. Ma come direbbe Andrew Ng, questo non è così importante; non preoccuparti.

Ok, quindi le funzioni sono oggetti. Certamente le funzioni dovrebbero avere alcuni metodi magici, giusto?

shout.__class__# functionshout.__name__# shoutshout.__call__# <method-wrapper '__call__' dell'oggetto funzione a 0x10d8b69e0># Oh snap!

Il metodo magico __call__ è definito per gli oggetti che possono essere chiamati. Quindi il nostro oggetto shout (funzione) è chiamabile. Possiamo chiamarlo con o senza argomenti. Ma questo è interessante. Quello che abbiamo fatto in precedenza è stato definire una funzione shout e ottenere un oggetto che è chiamabile con il metodo magico __call__ che è una funzione. Hai mai visto il film Inception?

Quindi, la nostra funzione non è realmente una funzione ma un oggetto. Gli oggetti sono istanze di classi e contengono metodi e attributi, giusto? Questo è qualcosa che dovresti conoscere dalla programmazione orientata agli oggetti. Come possiamo scoprire quali sono gli attributi del nostro oggetto? C’è questa funzione di Python chiamata vars che restituisce un dizionario degli attributi dell’oggetto con i loro valori. Vediamo cosa succede nel prossimo esempio:

vars(shout)# {}shout.name = 'Jimmy'vars(shout)# {'name': 'Jimmy'}

Questo è interessante. Non che tu possa capire subito l’utilizzo di questo. E anche se riuscissi a trovarlo, ti sconsiglierei vivamente di fare questa magia nera. Non è facile da seguire, anche se è una flessione interessante. Il motivo per cui ti ho mostrato questo è perché volevamo la prova che le funzioni siano effettivamente oggetti. Ricorda, tutto in Python è un oggetto. Questo è come lo facciamo in Python.

Ora, finalmente, torniamo alle funzioni. Questo concetto è anche molto interessante perché ti offre molta utilità. Con un po’ di zucchero sintattico, diventi molto espressivo. Approfondiamo.

Prima di tutto, la definizione di una funzione può contenere la definizione di un’altra funzione. Anzi, più di una. Ecco un esempio perfettamente valido:

def shout(name):    def _upper_case(s):        return s.upper()    return _upper_case(name)

Se stai pensando che questa sia solo una versione complicata di name.upper() hai ragione. Ma aspetta, stiamo arrivando lì.

Quindi, dato l’esempio precedente, che è codice Python completamente funzionante, puoi sperimentare con più funzioni definite all’interno della tua funzione. Qual è il valore di questo trucco carino? Beh, potresti trovarti in una situazione in cui la tua funzione è enorme, con blocchi di codice ripetuti. In questo modo, definire una sottofunzione aumenterebbe la leggibilità. In pratica, le funzioni enormi sono un segnale di “code smell” ed è altamente consigliato scomporle in alcune più piccole. Quindi, seguendo questo consiglio, raramente avrai bisogno di definire più funzioni l’una dentro l’altra. Una cosa da notare è che la funzione _upper_case è nascosta e fuori dalla portata dello scope in cui la funzione shout viene definita e disponibile per essere chiamata. In questo modo, non puoi testarla facilmente, che è un altro problema di questo approccio.

Tuttavia, c’è un caso specifico in cui definire una funzione all’interno di un’altra è la strada da seguire. Questo accade quando si implementa il decoratore di una funzione. Questo non ha nulla a che fare con la funzione che abbiamo usato per decorare la stringa name in uno degli esempi precedenti.

Funzioni decorator in Python

Cosa è una funzione decorator? Pensaci come una funzione che avvolge la tua funzione. Lo scopo di farlo è introdurre funzionalità aggiuntive a una funzione già esistente. Ad esempio, diciamo che vuoi registrare ogni volta che la tua funzione viene chiamata:

def my_function():    return sum(range(10))def my_logger(fun):    print(f'{fun.__name__} viene chiamata!')    return funmy_function()# 45my_logger(my_function)# my_function viene chiamata!# <function my_function at 0x105afbeb0>my_logger(my_function)()# my_function viene chiamata!# 45

Presta attenzione a come decoriamo la nostra funzione; la passiamo come argomento a quella decorativa. Ma questo non è sufficiente! Ricorda, il decoratore restituisce una funzione e questa funzione deve essere invocata (chiamata). Questo è ciò che fa l’ultima chiamata.

Ora, nella pratica, ciò che vuoi veramente è che la decorazione persista sotto il nome della funzione originale. Nel nostro caso, vorremmo che dopo che l’interprete ha analizzato il nostro codice, my_function sia il nome della funzione decorata. In questo modo, manteniamo le cose semplici da seguire e ci assicuriamo che ogni parte del nostro codice non sarà in grado di chiamare una versione non decorata della nostra funzione. Esempio:

def my_function():    return sum(range(10))def my_logger(fun):    print(f'{fun.__name__} viene chiamata!')    return funmy_function = my_logger(my_function)my_function(10)# my_function viene chiamata!# 45

Ammetterai che la parte in cui riassegniamo il nome della funzione a una decorata è problematica. Devi tenerlo presente. Se ci sono molte chiamate di funzione che vuoi registrare, ci sarà molto codice ripetuto. Qui entra in gioco lo “syntactic sugar”. Dopo che la funzione decoratore è definita, puoi usarla per decorare un’altra funzione precedendo la definizione della funzione con un @ e il nome della funzione decoratore. Esempio:

def my_logger(fun):    print(f'{fun.__name__} viene chiamata!')    return fun@my_loggerdef my_function():    return sum(range(10))my_function()# my_function viene chiamata!# 45

Questo è lo Zen di Python. Guarda l’espressività del codice e la sua semplicità.

Una cosa importante da notare qui! Anche se l’output ha senso, non è quello che ti aspetteresti! Al momento del caricamento del tuo codice Python, l’interprete chiamerà la funzione my_logger ed effettivamente la eseguirà! Otterrai l’output del log, ma questo non sarà ciò che volevamo in primo luogo. Guarda il codice ora:

def my_logger(fun):    print(f'{fun.__name__} viene chiamata!')    return fun@my_loggerdef my_function():    return sum(range(10))my_function()# my_function viene chiamata!# 45my_function()# 45

Per poter eseguire il codice del decoratore una volta chiamata la funzione originale, dobbiamo avvolgerlo in un’altra funzione. Qui le cose possono diventare disordinate. Ecco un esempio:

def my_logger(fun):    
    def _inner_decorator(*args, **kwargs):        
        print(f'{fun.__name__} viene chiamata!')        
        return fun(*args, **kwargs)    
    return _inner_decorator

@my_logger
def my_function(n):    
    return sum(range(n))

print(my_function(5))
# my_function viene chiamata!
# 10

In questo esempio, ci sono anche degli aggiornamenti, quindi passiamoli in rassegna:

  1. Vogliamo essere in grado di passare l’argomento a my_function.
  2. Vogliamo essere in grado di decorare qualsiasi funzione, non solo my_function. Poiché non conosciamo il numero esatto di argomenti per le future funzioni, dobbiamo mantenere le cose il più generali possibile, ecco perché usiamo *args e **kwargs.
  3. Ma soprattutto, abbiamo definito _inner_decorator che viene chiamato ogni volta che chiamiamo my_function nel codice. Accetta argomenti posizionali e argomenti con nome e li passa come argomenti alla funzione decorata.

Ricorda sempre che la funzione decoratrice deve restituire una funzione che accetta gli stessi argomenti (numero e rispettivi tipi) e restituisce lo stesso output (nuovamente, numero e rispettivi tipi). Cioè, se vuoi evitare che l’utente si confonda e che il lettore del codice non debba cercare di capire cosa sta succedendo.

Ad esempio, supponiamo di avere due funzioni che differiscono nei risultati ma richiedono anche argomenti:

@my_logger
def my_function(n):    
    return sum(range(n))

@my_logger
def my_unordinary_function(n, m):    
    return sum(range(n)) + m

print(my_function(5))
# my_function viene chiamata!
# 10

print(my_unordinary_function(5, 1))
# my_unordinary_function viene chiamata!
# 11

Nel nostro esempio, la funzione decoratrice accetta solo la funzione che decora. Ma cosa succede se si vogliono passare parametri aggiuntivi e modificare dinamicamente il comportamento del decoratore? Supponiamo si voglia regolare la verbosità del decoratore di log. Finora, la nostra funzione decoratrice ha accettato un argomento: la funzione che decora. Tuttavia, quando la funzione decoratrice ha i propri argomenti, questi vengono passati prima ad essa. Quindi, la funzione decoratrice deve restituire una funzione che accetta quella decorata. Fondamentalmente, le cose si stanno complicando. Ricordi il riferimento al film Inception?

Ecco un esempio:

from enum import IntEnum, auto
from datetime import datetime
from functools import wraps

class LogVerbosity(IntEnum):
    ZERO = auto()
    LOW = auto()
    MEDIUM = auto()
    HIGH = auto()

def my_logger(verbosity: LogVerbosity):
    def _inner_logger(fun):
        def _inner_decorator(*args, **kwargs):
            if verbosity >= LogVerbosity.LOW:
                print(f'LOG: Livello di verbosità: {verbosity}')
                print(f'LOG: {fun.__name__} viene chiamata!')
            if verbosity >= LogVerbosity.MEDIUM:
                print(f'LOG: Data e ora della chiamata: {datetime.utcnow()}.')
            if verbosity == LogVerbosity.HIGH:
                print(f'LOG: Ambito del chiamante: {__name__}.')
                print(f'LOG: Argomenti sono {args}, {kwargs}')
            return fun(*args, **kwargs)
        return _inner_decorator
    return _inner_logger

@my_logger(verbosity=LogVerbosity.LOW)
def my_function(n):
    return sum(range(n))

@my_logger(verbosity=LogVerbosity.HIGH)
def my_unordinary_function(n, m):
    return sum(range(n)) + m

print(my_function(10))
# LOG: Livello di verbosità: LOW
# LOG: my_function viene chiamata!
# 45

print(my_unordinary_function(5, 1))
# LOG: Livello di verbosità: HIGH
# LOG: my_unordinary_function viene chiamata!
# LOG: Data e ora della chiamata: 2023-07-25 19:09:15.954603.
# LOG: Ambito del chiamante: __main__.
# LOG: Argomenti sono (5, 1), {}
# 11

Non entrerò nella descrizione del codice non correlato al decoratore, ma ti incoraggio a cercarlo e imparare. Qui abbiamo un decoratore che registra le chiamate alle funzioni con diverse verbosità. Come già descritto, il decoratore my_logger ora accetta argomenti che cambiano dinamicamente il suo comportamento. Dopo aver passato gli argomenti, la funzione risultante che restituisce dovrebbe accettare una funzione da decorare. Questa è la funzione _inner_logger. A questo punto, dovresti capire cosa fa il resto del codice del decoratore.

Conclusion

La mia prima idea per questo post era di scrivere su argomenti avanzati come i decorators in Python. Tuttavia, come probabilmente saprai ora, ho menzionato e utilizzato molti altri argomenti avanzati anche. Nei prossimi post, affronterò alcuni di questi fino a un certo punto. Tuttavia, il mio consiglio per te è di andare e imparare anche dalle altre fonti le cose menzionate qui. Comprendere le funzioni è indispensabile se stai sviluppando in qualsiasi linguaggio di programmazione, ma comprendere tutti gli aspetti del linguaggio di programmazione scelto può darti un grande vantaggio nel modo in cui scrivi il tuo codice.

Spero di averti introdotto qualcosa di nuovo e che tu sia ora sicuro di scrivere funzioni come un programmatore Python avanzato.

Riferimenti

  • Primer su decorators in Python
  • Python Inner Functions: A cosa servono?
  • Enum HOWTO