-Until recently I believed that classes were 'molds' that were used to create objects; however, this question ( Why can't you delete methods in python? ) made me question a few things. What is said in the most voted answer is that, in short, you can delete methods since, just like anything else, methods (without parentheses) are basically pointers that store the reference ('point') to the method code that loads into RAM. In other words: they are attributes of all life except that, in this case, they point to code and not to normal data, therefore when using the reserved worddel
what we do is eliminate the references that pointer saves, which translates into 'cutting' the 'link' that pointer has with the variable(s) to which it points (from what I also understand can save multiple references, it would be the same as what would happen with lists for example, at least that's how it works in C
and I guess it will be the same as what happens in python (if not, please comment) therefore we can no longer access nothing through it, since it no longer points anywhere...
- So far so good, the question begins when it is mentioned that objects inherit from the classes from which they are instantiated, it is for the same reason that a method cannot be eliminated through an object, since that method is not yours but of your class , this makes a lot of sense, since (as you can see in the post) starting from that premise everything works.
-With which, the question would be the following: Are the classes really ' molds '? What actually happens when we create and instantiate an object from a class? (removing all the abstraction layers, which is what actually happens in RAM)
PLOT
-I understood the following...
class persona:
def __init__(self, nombre, apellido, edad, sexo) -> None:
self.nombre = nombre
self.apellido = apellido
self.edad = edad
self.sexo = sexo
persona_nueva = persona('Jhon','Doe', 33, 'h')
-In this case, it persona_nueva
would then be a pointer that would store the reference to an object (in this case we can understand an object as a set of pointers), therefore, execute ...
print(persona_nueva.nombre)
-It would be like saying to persona_nueva
(apart from the participation of _ _ str _ _): 'pass print
the value of the place where the pointer you are pointing to points to, that nombre
' (this would be more or less the structure that is shape in my head)
____-nombre -> 'Jhon'
/ -apellido -> 'Doe'
persona_nueva -edad -> 33
-sexo -> 'h'
already persona_nueva
agreed to nombre
...
persona_nueva.nombre -> print
It's for the same reason that it del persona_nueva.nombre
works (at least on my machine), as it would be equivalent to...
____-nombre -> 'Jhon'
\ -apellido -> 'Doe'
persona_nueva -edad -> 33
-sexo -> 'h'
and therefore ...
-apellido -> 'Doe'
persona_nueva -edad -> 33
-sexo -> 'h'
-Well, the case is that if it were a method (that is, if the name, for example, were a method) and therefore...
-nombre -> nombre()
-apellido -> 'Doe'
persona_nueva -edad -> 33
-sexo -> 'h'
In this case, it could not be executed anymore del persona_nueva.nombre
... This is basically what I asked in (Why can't you delete methods in python?), in response I was told that it is because technically, the attributes are not of persona_nueva
(object ) but of its class persona
, but if this is the case, why can I remove attributes (use del
) from persona_nueva
it and not methods, these having the same behavior. Most likely it's because I don't know what a class is, hence the question...
wow! Interesting question, complex answer. Although it could be answered at different levels (it's the nice thing about python, you can stay at one level for years without needing anything from the lower level).
Highest level. Classes are templates
A class is a template that allows you to create objects. This explanation is wrong in the sense that the created objects are not mere "copies" of the class, but rather contain references to the class, to avoid for example duplicating the code of the methods, as @jachguate mentions in his answer . However, you can work with that mental model without finding cracks for quite some time.
However, that explanation does not clarify why you have to put a parameter
self
in each of the class methods. Or the difference between a class attribute and an object attribute. Or between a staticmethod and a normal method.So let's dig a little deeper.
Medium level. classes are objects
Everything in python is objects. A function is an object. A class is also an object, but it has the ability, when called, to create new objects. They are in fact the only way to create user-defined objects.
Then perhaps it is necessary to clarify before what is an object? .
Deep down we could say that an object is a dictionary. In fact, it is implemented as such, but it offers other ways to interact with it. Within every object there is a predefined named attribute called
__dict__
and that is the dictionary that contains all the object's attributes and their values.For example, as stated before, a function is an object. When you write:
An object of type is created
function
, and a referencef
pointing to that object is createdfunction
. The reference is nothing more than a name, and you can think of it as a pointer. The function itself is notf
, but the objectfunction
itf
points to. Being an object, it has its__dict__
that we can consult:It is empty by default because functions, although they are objects, by default have no attributes. Nothing would prevent us from creating them (although it's not common practice, but they can sometimes be used to store data in them that persists between different invocations of the function):
But there are not only attributes in objects. They actually contain more things, like a documentation (stored in your
.__doc__
), or a reference to their type (the class they are an instance of), stored in your.__class__
. In this example:So we see that it
f
is actually an instance of<class 'function'>
, but that class is itself an object, so it will also have its.__doc__
,.__dict__
and even.__class__
:This already allows us to differentiate a little better between a class and an instance of the class. They are both objects, but they contain different information.
When you write a class of your own, such as:
We create an object of type
class
, which has its own__dict__
, inside which there are among other things the keys"x"
,"__init__"
and"metodo"
. The first contains simply an integer (1) and the following contain a function-object (calledC.__init__
andC.metodo
, respectively)When you instantiate that class to create an object, for example with
its initializer is executed which does the assignment
self.n=3
. At that momentself
it is the newly created object, therefore a new object. The attributen
will be part ofo.__dict__
, but not part ofC.__dict__
.As we can see, in the dictionary
o
there is only the key'n'
. However that does not prevent us from trying to accesso.x
:It so happens that python will first look for an attribute called
"x"
withino.__dict__
. If you find it, it will be over there. If it doesn't find it, it will useo.__class__
to know the class of which it is an instance, and then it will useo.__class__.__dict__
to search there"x"
, where it will find it with value 1. That is why all the instances of that class share that attribute, because it is not in each object but in the class.By the way, this behavior applies only to reading the attribute. If we assign it instead, that will always create a new entry in the
__dict__
object's:The methods operate in a similar way (at least at the level we are explaining now, which is not the deepest yet). When we do
o.metodo()
, we look"metodo"
foro.__dict__
it and when we don't find it, we look foro.__class__.__dict__
where we find it, which is a reference to a function. At that point Python transforms it into what it calls an "instance method" (but does not altero.__dict__
), which is a special function that will receive as its first parameter the concrete object through which it was called.In other words, it
o.metodo()
is basically equivalent to:And there are even more levels
Actually, looking for an attribute on an object is something that can be redefined. What I explained before is how it's done by default , but if the class
C
defined a method called__getattribute__()
, then python would use it to look up the given attribute ino
. So thato.foo
would translate toC.__getattribute__(o, "foo")
, and that function could decide to return anything, thereby skipping the role ofo.__dict__
andC.__dict__
.This in turn can be made more complex by taking inheritance (and the possibility of multiple inheritance) into account, which would cause recursive calls "up" the inheritance tree, according to a well-defined order (MRO). And we should talk about the protocol descriptor
And I didn't want to go into how the object itself is created, which is something that python normally takes care of, but which we have control over as well if we want, by implementing the special method
__new__()
in the class. That method is the one that must return the newly created object on which it will be applied later__init__()
.All the details are of course in the data model documentation
Leaving aside the conceptual part of the class as a template , if you look at it from another point of view, at runtime each instance of
Persona
has to have all its attributes (nombre
,apellido
, etc). If you have 1,000 people (and ignoring other optimizations), there will be 1,000 first names, 1,000 last names, etc. in memory.However, a method, say
saludar()
, will exist in memory only once (both the code to execute it and the reference to this code).Since they only have to be there once, these attributes are stored in the class itself, and not in each instance, where it wouldn't make much sense.
When you call the method on an instance, say
juan.saludar()
, what happens behind the scenes is that itjuan
has a pointer to the classPersona
(its class) and this class is the one with the pointer to the method codesaludar()
. The call is constructed by passing to said function, as its first parameter, the pointer to the instance on which it is going to operate (the self-parameter self), in this case, a reference tojuan
.In other words, calling
juan.decir('hola')
is the equivalent of callingPersona.decir(juan, 'hola')
, which I don't know if it can be done that way, but that's what the call translates into .The underlying model that python operates on is quite different from c++ and other compiled languages... in that it is at compile time that many of these references are resolved and memory addresses of attributes are computed, making it less flexible to the model.
In python, since everything is resolved at run time, you can more freely manipulate the structure of a class or its instances, even after having copies in memory, in a way that you couldn't do in c++ or Pascal, for example. mention some languages.
Complementing the given answer a bit, what happens when a class is created and it is instantiated, an empty object is created, which is often filled with a constructor
__init__
. Let's take an example:In your case, you define a constructor
__init__
, this makes the class immediately call this method in which the attributes are assigned, these are created when the class is instantiated (calling an object__call__
). What will happen here will be, effectively, what will be in memory the same number of instances and attributes you have (#a*#i) as explained in the other answer.On the other hand, the methods are functions, which are executed when the function is called and therefore create and assign the variables when they are called, a function when called from an instance generates a pointer to such a class, this is already I explain why I will leave it here.
When a class is created, it is executed in a new execution frame using newly created local names, this has to do with the scope of a class, but what happens when it ends? When a new object has already been created, its execution frame execution is discarded, however it saves the local namespace.
Inheritance
Inheritance is something fundamental in object-oriented programming, when the class object is constructed, the base class is taken into account, this helps to resolve references of requested attributes and methods that are not found, to which python proceeds to search in the base class, All the classes that you create inherit by default from a base class
object
this class has all the methods that are common to all the instances, this class does not have a method__dict__
which does not make it possible to assign attributes to the instance of this class.What is scope?
Earlier we talked about the scope of a class. The scope is the visibility of a name in a block, something similar to
var, let
javascript, the first indicates that the variable will be accessed from anywhere and the second that it only works in a specific block. When a name is not found an exception is thrownNameError
. You have to remember that python binds the variables, here is a small example, it is not on the same topic but it serves to explain it.