Python avanzato: operatore punto.  L’operatore che abilita il… |  di Ilija Lazarevic |  Ottobre 2023

 | Intelligenza-Artificiale

Inizierò con una domanda banale: “Cos’è un “operatore punto?”

Ecco un esempio:

hello = 'Hello world!'

print(hello.upper())
# HELLO WORLD!

Bene, questo è sicuramente un esempio di “Hello World”, ma difficilmente riesco a immaginare qualcuno che inizi a insegnarti Python esattamente in questo modo. In ogni caso, l’“operatore punto” è il “.” parte dihello.upper(). Proviamo a fare un esempio più dettagliato:

class Person:

num_of_persons = 0

def __init__(self, name):
self.name = name

def shout(self):
print(f"Hey! I'm {self.name}")

p = Person('John')
p.shout()
# Hey I'm John.

p.num_of_persons
# 0

p.name
# 'John'

Ci sono alcuni posti in cui usi l ‘”operatore punto”. Per rendere più semplice la visione d’insieme, riassumiamo il modo in cui lo utilizzi in due casi:

  • usarlo per accedere agli attributi di un oggetto o di una classe,
  • usarlo per accedere alle funzioni definite nella definizione della classe.

Ovviamente, abbiamo tutto questo nel nostro esempio e sembra intuitivo e come previsto. Ma c’è di più in questo di quanto sembri! Dai un’occhiata più da vicino a questo esempio:

p.shout
# <bound method Person.shout of <__main__.Person object at 0x1037d3a60>>

id(p.shout)
# 4363645248

Person.shout
# <function __main__.Person.shout(self)>

id(Person.shout)
# 4364388816

In qualche modo, p.shout non fa riferimento alla stessa funzione di Person.shout anche se dovrebbe. Almeno te lo aspetteresti, vero? E p.shout non è nemmeno una funzione! Esaminiamo il prossimo esempio prima di iniziare a discutere cosa sta succedendo:

class Person:

num_of_persons = 0

def __init__(self, name):
self.name = name

def shout(self):
print(f"Hey! I'm {self.name}.")

p = Person('John')

vars(p)
# {'name': 'John'}

def shout_v2(self):
print("Hey, what's up?")

p.shout_v2 = shout_v2

vars(p)
# {'name': 'John', 'shout_v2': <function __main__.shout_v2(self)>}

p.shout()
# Hey, I'm John.

p.shout_v2()
# TypeError: shout_v2() missing 1 required positional argument: 'self'

Per chi non ne fosse a conoscenza vars funzione, restituisce il dizionario che contiene gli attributi di un’istanza. Se corri vars(Person) otterrai una risposta leggermente diversa, ma otterrai l’immagine. Ci saranno entrambi gli attributi con i loro valori e le variabili che contengono le definizioni delle funzioni di classe. C’è ovviamente una differenza tra un oggetto che è un’istanza di una classe e l’oggetto della classe stessa, quindi ci sarà una differenza in vars risposta della funzione per questi due.

Ora è perfettamente valido definire ulteriormente una funzione dopo la creazione di un oggetto. Questa è la linea p.shout_v2 = shout_v2. Ciò introduce un’altra coppia chiave-valore nel dizionario dell’istanza. Apparentemente tutto va bene e potremo correre senza intoppi, come se shout_v2 sono stati specificati nella definizione della classe. Ma ahimè! Qualcosa è veramente sbagliato. Non possiamo chiamarlo nello stesso modo in cui lo abbiamo chiamato shout metodo.

I lettori più astuti dovrebbero ormai aver notato con quanta attenzione utilizzo i termini funzione E metodo. Dopotutto, c’è una differenza anche nel modo in cui Python li stampa. Dai un’occhiata agli esempi precedenti. shout è un metodo, shout_v2 è una funzione. Almeno se li guardiamo dal punto di vista dell’oggetto p. Se li consideriamo dal punto di vista del Person classe, shout è una funzione e shout_v2 non esiste. È definito solo nel dizionario dell’oggetto (spazio dei nomi). Quindi, se intendi davvero fare affidamento su paradigmi e meccanismi orientati agli oggetti come incapsulamento, ereditarietà, astrazione e polimorfismo, non definirai funzioni sugli oggetti, come p è nel nostro esempio. Ti assicurerai di definire le funzioni in una definizione di classe (corpo).

Allora perché questi due sono diversi e perché riceviamo l’errore? Bene, la risposta più veloce è dovuta al modo in cui funziona l ‘”operatore punto”. La risposta più lunga è che esiste un meccanismo dietro le quinte che esegue la risoluzione del nome (dell’attributo) per te. Questo meccanismo è costituito da __getattribute__ E __getattr__ metodi più stupidi.

All’inizio, questo probabilmente sembrerà poco intuitivo e piuttosto inutilmente complicato, ma abbi pazienza. Essenzialmente, ci sono due scenari che possono verificarsi quando si tenta di accedere a un attributo di un oggetto in Python: o c’è un attributo oppure non c’è. Semplicemente. In entrambi i casi, __getattribute__ si chiama, o per semplificarti le cose, lo è essere chiamato sempre. Questo metodo:

  • restituisce il valore dell’attributo calcolato,
  • chiama esplicitamente __getattr__O
  • rilanci AttributeError in quale caso __getattr__ viene chiamato per impostazione predefinita.

Se vuoi intercettare il meccanismo che risolve i nomi degli attributi, questo è il posto dove dirottarlo. Bisogna solo fare attenzione, perché è davvero facile finire in un loop infinito o mandare in tilt l’intero meccanismo di risoluzione dei nomi, soprattutto nello scenario dell’ereditarietà orientata agli oggetti. Non è così semplice come può sembrare.

Se vuoi gestire i casi in cui non è presente alcun attributo nel dizionario dell’oggetto, puoi immediatamente implementare il file __getattr__ metodo. Questo viene chiamato quando __getattribute__ non riesce ad accedere al nome dell’attributo. Se dopotutto questo metodo non riesce a trovare un attributo o a gestirne uno mancante, solleva un AttributeError anche eccezione. Ecco come puoi giocare con questi:

class Person:

num_of_persons = 0

def __init__(self, name):
self.name = name

def shout(self):
print(f"Hey! I'm {self.name}.")

def __getattribute__(self, name):
print(f'getting the attribute name: {name}')
return super().__getattribute__(name)

def __getattr__(self, name):
print(f'this attribute doesn\'t exist: {name}')
raise AttributeError()

p = Person('John')

p.name
# getting the attribute name: name
# 'John'

p.name1
# getting the attribute name: name1
# this attribute doesn't exist: name1
#
# ... exception stack trace
# AttributeError:

È molto importante chiamare super().__getattribute__(...) nella tua implementazione di __getattribute__e il motivo, come ho scritto prima, è che c’è molto da fare nell’implementazione predefinita di Python. E questo è esattamente il luogo da cui “dot operator” trae la sua magia. Ebbene, almeno metà della magia è lì. L’altra parte riguarda il modo in cui viene creato un oggetto di classe dopo aver interpretato la definizione di classe.

Il termine che uso qui è intenzionale. La classe contiene solo funzionie lo abbiamo visto in uno dei primi esempi:

p.shout
# <bound method Person.shout of <__main__.Person object at 0x1037d3a60>>

Person.shout
# <function __main__.Person.shout(self)>

Quando si guarda dalla prospettiva dell’oggetto, questi sono chiamati metodi. Il processo di trasformazione della funzione di una classe in un metodo di un oggetto è chiamato bounding, e il risultato è quello che vedi nell’esempio precedente, un metodo vincolato. Cosa lo rende limitee a cosa? Bene, una volta che hai un’istanza di una classe e inizi a chiamare i suoi metodi, in sostanza stai passando il riferimento all’oggetto a ciascuno dei suoi metodi. Ricorda il self discussione? Quindi, come avviene questo e chi lo fa?

Ebbene, la prima parte avviene quando viene interpretato il corpo della classe. Ci sono alcune cose che accadono in questo processo, come definire uno spazio dei nomi di classe, aggiungervi valori di attributi, definire funzioni (di classe) e associarle ai loro nomi. Ora, mentre queste funzioni vengono definite, vengono racchiuse in un certo senso. Avvolto in un oggetto concettualmente chiamato descrittore. Questo descrittore sta consentendo questo cambiamento nell’identificazione e nel comportamento delle funzioni di classe che abbiamo visto in precedenza. Mi assicurerò di scrivere un post sul blog separato a riguardo descrittorima per ora, sappi che questo oggetto è un’istanza di una classe che implementa un insieme predefinito di metodi dunder. Questo è anche chiamato a Protocollo. Una volta implementati questi si dice che gli oggetti di questa classe seguire il protocollo specifico e quindi comportarsi nel modo previsto. C’è una differenza tra il dati E non datato descrittori. Ex attrezzi __get__, __set__e/o __delete__ metodi più stupidi. Successivamente, implementa solo il file __get__ metodo. Ad ogni modo, ogni funzione in una classe finisce per essere racchiusa in un cosiddetto non datato descrittore.

Una volta avviata la ricerca degli attributi utilizzando l’operatore punto, il file __getattribute__ viene chiamato il metodo e viene avviato l’intero processo di risoluzione dei nomi. Questo processo si interrompe quando la risoluzione ha esito positivo e funziona in questo modo:

  1. restituire il descrittore di dati che ha il nome desiderato (livello di classe), oppure
  2. restituire l’attributo dell’istanza con il nome desiderato (livello di istanza) oppure
  3. restituisce un descrittore non di dati con il nome desiderato (livello di classe), oppure
  4. restituire l’attributo della classe con il nome desiderato (livello della classe), oppure
  5. aumentare AttributeError che essenzialmente chiama il __getattr__ metodo.

La mia idea iniziale era di lasciarvi con un riferimento alla documentazione ufficiale su come viene implementato questo meccanismo, almeno un mockup Python, a scopo didattico, ma ho deciso di aiutarvi anche con quella parte. Ti consiglio comunque vivamente di andare a leggere tutta la pagina della documentazione ufficiale.

Quindi, nel prossimo frammento di codice, inserirò alcune descrizioni nei commenti, in modo che sia più facile leggere e comprendere il codice. Ecco qui:

def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
# Create vanilla object for later use.
null = object()

"""
obj is an object instantiated from our custom class. Here we try
to find the name of the class it was instantiated from.
"""
objtype = type(obj)

"""
name represents the name of the class function, instance attribute,
or any class attribute. Here, we try to find it and keep a
reference to it. MRO is short for Method Resolution Order, and it
has to do with class inheritance. Not really that important at
this point. Let's say that this mechanism optimally finds name
through all parent classes.
"""
cls_var = find_name_in_mro(objtype, name, null)

"""
Here we check if this class attribute is an object that has the
__get__ method implemented. If it does, it is a non-data
descriptor. This is important for further steps.
"""
descr_get = getattr(type(cls_var), '__get__', null)

"""
So now it's either our class attribute references a descriptor, in
which case we test to see if it is a data descriptor and we
return reference to the descriptor's __get__ method, or we go to
the next if code block.
"""
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype) # data descriptor

"""
In cases where the name doesn't reference a data descriptor, we
check to see if it references the variable in the object's
dictionary, and if so, we return its value.
"""
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)(name) # instance variable

"""
In cases where the name does not reference the variable in the
object's dictionary, we try to see if it references a non-data
descriptor and return a reference to it.
"""
if descr_get is not null:
return descr_get(cls_var, obj, objtype) # non-data descriptor

"""
In case name did not reference anything from above, we try to see
if it references a class attribute and return its value.
"""
if cls_var is not null:
return cls_var # class variable

"""
If name resolution was unsuccessful, we throw an AttriuteError
exception, and __getattr__ is being invoked.
"""
raise AttributeError(name)

Tieni presente che questa implementazione è in Python allo scopo di documentare e descrivere la logica implementata nel file __getattribute__ metodo. In realtà, è implementato in C. Solo guardandolo, puoi immaginare che sia meglio non giocare a reimplementare il tutto. Il modo migliore è provare a eseguire parte della risoluzione da soli e poi ricorrere all’implementazione CPython con return super().__getattribute__(name) come mostrato nell’esempio sopra.

La cosa importante qui è che ogni funzione di classe (che è un oggetto) viene racchiusa in un descrittore non di dati (che è un function oggetto class), e questo significa che questo oggetto wrapper ha l’ __get__ metodo dunder definito. Ciò che fa questo metodo dunder è restituire un nuovo chiamabile (consideralo come una nuova funzione), dove il primo argomento è il riferimento all’oggetto su cui stiamo eseguendo l’“operatore punto”. Ho detto di pensarla come una nuova funzione poiché è un file richiamabile. In sostanza, è un altro oggetto chiamato MethodType. Controlla:

type(p.shout)
# getting the attribute name: shout
# method

type(Person.shout)
# function

Una cosa interessante sicuramente è questa function classe. Questo è esattamente l’oggetto wrapper che definisce il file __get__ metodo. Tuttavia, una volta provato ad accedervi come metodo shout da “operatore punto”, __getattribute__ scorre l’elenco e si ferma al terzo caso (restituisce un descrittore non di dati). Questo __get__ Il metodo contiene logica aggiuntiva che prende il riferimento dell’oggetto e crea MethodType con riferimento al function e oggetto.

Ecco il mockup della documentazione ufficiale:

class Function:
...

def __get__(self, obj, objtype=None):
if obj is None:
return self
return MethodType(self, obj)

Ignorare la differenza nel nome della classe. Ho usato function invece di Function per renderlo più facile da afferrare, ma userò il file Function nome d’ora in poi in modo che segua la spiegazione della documentazione ufficiale.

Ad ogni modo, solo guardando questo mockup, potrebbe essere sufficiente per capire come funziona function class si adatta al quadro, ma permettetemi di aggiungere un paio di righe di codice mancanti, che probabilmente renderanno le cose ancora più chiare. Aggiungerò altre due funzioni di classe in questo esempio, vale a dire:

class Function:
...

def __init__(self, fun, *args, **kwargs):
...
self.fun = fun

def __get__(self, obj, objtype=None):
if obj is None:
return self
return MethodType(self, obj)

def __call__(self, *args, **kwargs):
...
return self.fun(*args, **kwargs)

Perché ho aggiunto queste funzioni? Bene, ora puoi facilmente immaginare come Function L’oggetto gioca il suo ruolo in tutto questo scenario di delimitazione del metodo. Questo nuovo Function L’oggetto memorizza la funzione originale come attributo. Anche questo oggetto lo è richiamabile il che significa che possiamo invocarlo come una funzione. In tal caso, funziona proprio come la funzione che racchiude. Ricorda, tutto in Python è un oggetto, anche le funzioni. E MethodType ‘avvolge’ Function object insieme al riferimento all’oggetto su cui stiamo chiamando il metodo (nel nostro caso shout).

Come fa MethodType Fai questo? Bene, mantiene questi riferimenti e implementa un protocollo richiamabile. Ecco il mockup della documentazione ufficiale per MethodType classe:

class MethodType:

def __init__(self, func, obj):
self.__func__ = func
self.__self__ = obj

def __call__(self, *args, **kwargs):
func = self.__func__
obj = self.__self__
return func(obj, *args, **kwargs)

Ancora una volta, per brevità, func finisce per fare riferimento alla nostra funzione di classe iniziale (shout), obj istanza di riferimenti (p), quindi abbiamo argomenti e argomenti di parole chiave che vengono passati. self nel shout dichiarazione finisce per fare riferimento a questo “obj”, che è essenzialmente p nel nostro esempio.

Alla fine, dovrebbe essere chiaro il motivo per cui facciamo una distinzione tra funzioni e metodi e come le funzioni vengono vincolate una volta accedute tramite oggetti utilizzando l ‘”operatore punto”. Se ci pensi, saremmo perfettamente d’accordo con l’invocazione delle funzioni di classe nel modo seguente:

class Person:

num_of_persons = 0

def __init__(self, name):
self.name = name

def shout(self):
print(f"Hey! I'm {self.name}.")

p = Person('John')

Person.shout(p)
# Hey! I'm John.

Tuttavia, questo non è proprio il modo consigliato ed è semplicemente brutto. Di solito non è necessario eseguire questa operazione nel codice.

Quindi, prima di concludere, voglio esaminare un paio di esempi di risoluzione degli attributi solo per renderlo più facile da comprendere. Usiamo l’esempio precedente e scopriamo come funziona l’operatore punto.

p.name
"""
1. __getattribute__ is invoked with p and "name" arguments.

2. objtype is Person.

3. descr_get is null because the Person class doesn't have
"name" in its dictionary (namespace).

4. Since there is no descr_get at all, we skip the first if block.

5. "name" does exist in the object's dictionary so we get the value.
"""

p.shout('Hey')
"""
Before we go into name resolution steps, keep in mind that
Person.shout is an instance of a function class. Essentially, it gets
wrapped in it. And this object is callable, so you can invoke it with
Person.shout(...). From a developer perspective, everything works just
as if it were defined in the class body. But in the background, it
most certainly is not.

1. __getattribute__ is invoked with p and "shout" arguments.

2. objtype is Person.

3. Person.shout is actually wrapped and is a non-data descriptor.
So this wrapper does have the __get__ method implemented, and it
gets referenced by descr_get.

4. The wrapper object is a non-data descriptor, so the first if block
is skipped.

5. "shout" doesn't exist in the object's dictionary because it is part
of class definition. Second if block is skipped.

6. "shout" is a non-data descriptor, and its __get__ method is returned
from the third if code block.

Now, here we tried accessing p.shout('Hey'), but what we did get is
p.shout.__get__ method. This one returns a MethodType object. Because
of this p.shout(...) works, but what ends up being called is an
instance of the MethodType class. This object is essentially a wrapper
around the `Function` wrapper, and it holds reference to the `Function`
wrapper and our object p. In the end, when you invoke p.shout('Hey'),
what ends up being invoked is `Function` wrapper with p object, and
'Hey' as one of the positional arguments.
"""

Person.shout(p)
"""
Before we go into name resolution steps, keep in mind that
Person.shout is an instance of a function class. Essentially, it gets
wrapped in it. And this object is callable, so you can invoke it with
Person.shout(...). From a developer perspective, everything works just
as if it were defined in the class body. But in the background, it
most certainly is not.

This part is the same. The following steps are different. Check
it out.

1. __getattribute__ is invoked with Person and "shout" arguments.

2. objtype is a type. This mechanism is described in my post on
metaclasses.

3. Person.shout is actually wrapped and is a non-data descriptor,
so this wrapper does have the __get__ method implemented, and it
gets referenced by descr_get.

4. The wrapper object is a non-data descriptor, so first if block is
skipped.

5. "shout" does exist in an object's dictionary because Person is
object after all. So the "shout" function is returned.

When Person.shout is invoked, what actually gets invoked is an instance
of the `Function` class, which is also callable and wrapper around the
original function defined in the class body. This way, the original
function gets called with all positional and keyword arguments.
"""

Se leggere questo articolo tutto d’un fiato non è stata un’impresa facile, non preoccuparti! L’intero meccanismo dietro l’“operatore punto” non è qualcosa che si capisce così facilmente. Ci sono almeno due ragioni, una delle quali è come __getattribute__ fa la risoluzione dei nomi e l’altro è il modo in cui le funzioni della classe vengono racchiuse nell’interpretazione del corpo della classe. Quindi, assicurati di rileggere l’articolo un paio di volte e di giocare con gli esempi. Sperimentare è davvero ciò che mi ha spinto a iniziare una serie chiamata Advanced Python.

Un’altra cosa! Se ti piace il modo in cui spiego le cose e c’è qualcosa di avanzato nel mondo di Python di cui vorresti leggere, mandaci un messaggio!

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *