Comenzaré con una pregunta trivial: “¿Qué es un “operador de punto”?
Aquí hay un ejemplo:
hello = 'Hello world!'print(hello.upper())
# HELLO WORLD!
Bueno, este es seguramente un ejemplo de “Hola mundo”, pero no puedo imaginarme a alguien empezando a enseñarte Python exactamente así. De todos modos, el “operador de punto” es el “.” parte dehello.upper(). Intentemos dar un ejemplo más detallado:
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'
Hay algunos lugares donde se utiliza el “operador de punto”. Para que sea más fácil ver el panorama general, resumamos la forma en que lo usa en dos casos:
- Úselo para acceder a los atributos de un objeto o clase,
- Úselo para acceder a funciones definidas en la definición de clase.
Obviamente, tenemos todo esto en nuestro ejemplo, y parece intuitivo y esperado. ¡Pero hay más en esto de lo que parece! Mire más de cerca este ejemplo:
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
De alguna manera, p.shout no hace referencia a la misma función que Person.shout aunque debería. Al menos lo esperarías, ¿verdad? Y p.shout ¡Ni siquiera es una función! Repasemos el siguiente ejemplo antes de comenzar a discutir lo que está sucediendo:
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'
Para aquellos que desconocen la vars función, devuelve el diccionario que contiene los atributos de una instancia. Si tu corres vars(Person) Obtendrá una respuesta un poco diferente, pero se hará una idea. Habrá atributos con sus valores y variables que contienen definiciones de funciones de clase. Obviamente hay una diferencia entre un objeto que es una instancia de una clase y el objeto de clase en sí, por lo que habrá una diferencia en vars respuesta de función para estos dos.
Ahora bien, es perfectamente válido definir adicionalmente una función después de crear un objeto. esta es la linea p.shout_v2 = shout_v2. Esto introduce otro par clave-valor en el diccionario de instancias. Aparentemente todo está bien y podremos funcionar sin problemas, como si shout_v2 fueron especificados en la definición de clase. ¡Pero Ay! Algo está realmente mal. No podemos llamarlo de la misma manera que lo hicimos shout método.
Los lectores astutos ya deberían haber notado con qué cuidado uso los términos función y método. Después de todo, también hay una diferencia en cómo Python los imprime. Eche un vistazo a los ejemplos anteriores. shout es un método, shout_v2 es una función. Al menos si los miramos desde la perspectiva del objeto. p. Si los miramos desde la perspectiva del Person clase, shout es una función y shout_v2 no existe. Se define únicamente en el diccionario del objeto (espacio de nombres). Entonces, si realmente vas a confiar en paradigmas y mecanismos orientados a objetos como la encapsulación, la herencia, la abstracción y el polimorfismo, no definirás funciones en objetos, como p está en nuestro ejemplo. Se asegurará de definir funciones en una definición de clase (cuerpo).
Entonces, ¿por qué son diferentes estos dos y por qué aparece el error? Bueno, la respuesta más rápida es por cómo funciona el “operador de punto”. La respuesta más larga es que existe un mecanismo detrás de escena que resuelve el nombre (atributo) por usted. Este mecanismo consiste en __getattribute__ y __getattr__ métodos más tontos.
Al principio esto probablemente parezca poco intuitivo e innecesariamente complicado, pero tengan paciencia. Esencialmente, hay dos escenarios que pueden suceder cuando intentas acceder a un atributo de un objeto en Python: hay un atributo o no lo hay. Simplemente. En ambos casos, __getattribute__ se llama, o para ponértelo más fácil, es siendo llamado siempre. Este método:
- devuelve el valor del atributo calculado,
- llama explícitamente
__getattr__o - eleva
AttributeErroren ese caso__getattr__se llama por defecto.
Si desea interceptar el mecanismo que resuelve los nombres de los atributos, este es el lugar para secuestrarlo. Sólo hay que tener cuidado, porque es muy fácil terminar en un bucle infinito o estropear todo el mecanismo de resolución de nombres, especialmente en el escenario de herencia orientada a objetos. No es tan simple como parece.
Si desea manejar casos en los que no hay ningún atributo en el diccionario del objeto, puede implementar de inmediato el __getattr__ método. Este se llama cuando __getattribute__ no puede acceder al nombre del atributo. Si este método no puede encontrar un atributo o solucionar uno que falta después de todo, genera un AttributeError excepción también. Así es como puedes jugar con estos:
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:
Es muy importante llamar super().__getattribute__(...) en su implementación de __getattribute__, y la razón, como escribí antes, es que están sucediendo muchas cosas en la implementación predeterminada de Python. Y aquí es exactamente de donde el “operador punto” obtiene su magia. Bueno, al menos la mitad de la magia está ahí. La otra parte es cómo se crea un objeto de clase después de interpretar la definición de clase.
El término que uso aquí tiene un propósito. La clase contiene sólo funcionesy vimos esto en uno de los primeros ejemplos:
p.shout
# <bound method Person.shout of <__main__.Person object at 0x1037d3a60>>Person.shout
# <function __main__.Person.shout(self)>
Cuando se mira desde la perspectiva del objeto, estos se denominan métodos. El proceso de transformar la función de una clase en un método de un objeto se llama delimitación, y el resultado es lo que ves en el ejemplo anterior, un método enlazado. ¿Qué lo hace? atado¿Y a qué? Bueno, una vez que tienes una instancia de una clase y comienzas a llamar a sus métodos, estás, en esencia, pasando la referencia del objeto a cada uno de sus métodos. Recuerda el self ¿argumento? Entonces, ¿cómo sucede esto y quién lo hace?
Bueno, la primera parte ocurre cuando se interpreta el cuerpo de la clase. Hay bastantes cosas que suceden en este proceso, como definir un espacio de nombres de clase, agregarle valores de atributos, definir funciones (de clase) y vincularlas a sus nombres. Ahora, a medida que se definen estas funciones, se están envolviendo de alguna manera. Envuelto en un objeto llamado conceptualmente descriptor. Este descriptor está permitiendo este cambio en la identificación y el comportamiento de las funciones de clase que vimos anteriormente. Me aseguraré de escribir una publicación de blog separada sobre descriptores, pero por ahora, sepa que este objeto es una instancia de una clase que implementa un conjunto predefinido de métodos dunder. A esto también se le llama Protocolo. Una vez implementados estos, se dice que los objetos de esta clase seguir el protocolo específico y por lo tanto comportarse de la manera esperada. Hay una diferencia entre el datos y sin datos descriptores. Antiguos implementos __get__, __set__y/o __delete__ métodos más tontos. Posteriormente, implementar sólo el __get__ método. De todos modos, cada función en una clase termina envuelta en lo que se llama sin datos descriptor.
Una vez que inicie la búsqueda de atributos utilizando el “operador de punto”, el __getattribute__ Se llama al método y comienza todo el proceso de resolución de nombres. Este proceso se detiene cuando la resolución es exitosa y es algo como esto:
- devolver el descriptor de datos que tiene el nombre deseado (nivel de clase), o
- devolver el atributo de instancia con el nombre deseado (nivel de instancia), o
- devolver un descriptor sin datos con el nombre deseado (nivel de clase), o
- devolver atributo de clase con el nombre deseado (nivel de clase), o
- aumentar
AttributeErrorque esencialmente llama a la__getattr__método.
Mi idea inicial era dejarles una referencia a la documentación oficial sobre cómo se implementa este mecanismo, al menos una maqueta de Python, para fines de aprendizaje, pero he decidido ayudarlos con esa parte también. Sin embargo, le recomiendo encarecidamente que lea la página completa de la documentación oficial.
Entonces, en el siguiente fragmento de código, pondré algunas de las descripciones en los comentarios, para que sea más fácil leer y comprender el código. Aquí lo tienes:
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)
Tenga en cuenta que esta implementación está en Python con el fin de documentar y describir la lógica implementada en el __getattribute__ método. En realidad, está implementado en C. Con solo mirarlo, puedes imaginar que es mejor no perder el tiempo reimplementando todo. La mejor manera es intentar hacer parte de la resolución usted mismo y luego recurrir a la implementación de CPython con return super().__getattribute__(name) como se muestra en el ejemplo anterior.
Lo importante aquí es que cada función de clase (que es un objeto) está envuelta en un descriptor que no es de datos (que es un function objeto de clase), y esto significa que este objeto contenedor tiene la __get__ Método Dunder definido. Lo que hace este método dunder es devolver un nuevo invocable (considérelo como una nueva función), donde el primer argumento es la referencia al objeto en el que estamos realizando el “operador de punto”. Dije que lo pensáramos como una nueva función ya que es una invocable. En esencia, es otro objeto llamado MethodType. Échale un vistazo:
type(p.shout)
# getting the attribute name: shout
# methodtype(Person.shout)
# function
Una cosa interesante sin duda es esto. function clase. Este es exactamente el objeto contenedor que define el __get__ método. Sin embargo, una vez que intentamos acceder a él como método shout por “operador de punto”, __getattribute__ itera a través de la lista y se detiene en el tercer caso (devuelve un descriptor que no es de datos). Este __get__ El método contiene lógica adicional que toma la referencia del objeto y crea MethodType Con referencia a function y objeto.
Aquí está la maqueta de la documentación oficial:
class Function:
...def __get__(self, obj, objtype=None):
if obj is None:
return self
return MethodType(self, obj)
Ignore la diferencia en el nombre de la clase. he estado usando function en lugar de Function para que sea más fácil de agarrar, pero usaré el Function nombre de ahora en adelante para que siga la explicación de la documentación oficial.
De todos modos, con sólo mirar esta maqueta, puede ser suficiente para entender cómo funciona esto. function La clase se ajusta a la imagen, pero permítanme agregar un par de líneas de código que faltan, lo que probablemente aclarará aún más las cosas. Agregaré dos funciones de clase más en este ejemplo, a saber:
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)
¿Por qué agregué estas funciones? Bueno, ahora puedes imaginar fácilmente cómo Function El objeto juega su papel en todo este escenario de limitación de métodos. esta nueva Function El objeto almacena la función original como un atributo. Este objeto también es invocable lo que significa que podemos invocarlo como una función. En ese caso, funciona igual que la función que envuelve. Recuerde, todo en Python es un objeto, incluso las funciones. Y MethodType ‘envolturas’ Function objeto junto con la referencia al objeto en el que estamos llamando al método (en nuestro caso shout).
Cómo MethodType ¿hacer esto? Bueno, mantiene estas referencias e implementa un protocolo invocable. Aquí está la maqueta de la documentación oficial para el MethodType clase:
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)
De nuevo, en aras de la brevedad, func termina haciendo referencia a nuestra función de clase inicial (shout), obj instancia de referencias (p), y luego tenemos argumentos y argumentos de palabras clave que se pasan. self en el shout La declaración termina haciendo referencia a este ‘obj’, que es esencialmente p en nuestro ejemplo.
Al final, debería quedar claro por qué hacemos una distinción entre funciones y métodos y cómo se vinculan las funciones una vez que se accede a ellas a través de objetos utilizando el “operador punto”. Si lo piensas bien, estaríamos perfectamente de acuerdo con invocar funciones de clase de la siguiente manera:
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.
Sin embargo, esta no es realmente la forma recomendada y es sencillamente fea. Normalmente, no tendrás que hacer esto en tu código.
Entonces, antes de concluir, quiero repasar un par de ejemplos de resolución de atributos solo para que esto sea más fácil de entender. Usemos el ejemplo anterior y descubramos cómo funciona el operador de 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.
"""
Si leer este artículo de una vez no fue tarea fácil, ¡no te preocupes! Todo el mecanismo detrás del “operador de punto” no es algo que se entienda tan fácilmente. Hay al menos dos razones, una es cómo __getattribute__ hace la resolución de nombres, y el otro es cómo las funciones de clase se ajustan a la interpretación del cuerpo de la clase. Así que asegúrate de repasar el artículo un par de veces y jugar con los ejemplos. Experimentar es realmente lo que me impulsó a comenzar una serie llamada Advanced Python.
¡Una cosa más! Si te gusta la forma en que explico las cosas y hay algo avanzado en el mundo de Python sobre lo que te gustaría leer, ¡agradece!