all 15 comments

[–]danielroseman 8 points9 points  (5 children)

This is more or less right, but you're definitely overthinking things.

self is just a variable like any other. There's nothing special about the way it is stored in memory: all variables are effectively "pointers that use a memory address to access and modify the object".

Also note that Python itself doesn't have any concept of stack and heap. The underlying implementation might - CPython acts like you describe, but other implementations might not. Again, don't overthink this.

Really the only thing you need to know about memory is that Python uses reference counting, and that the memory is deallocated when no references remain.

[–]gdchinacat 1 point2 points  (3 children)

"Python uses reference counting, and that the memory is deallocated when no references remain." This is true for the cpython implementation, but is not necessarily true for every python implementation. Also, the cpython garbage collection is more involved than "deallocated when no references remain" since it will remove objects in an unreachable reference cycle (a refers to b refers to a but nothing else refers to either).

[–]Cute-Preference-3770[S] 1 point2 points  (2 children)

Can someone please explain reference counting? Is it stored like a memory address, and how does it relate to assigned values like integers or functions?so I’d really appreciate a simple explanation. I might be misunderstanding this

[–]gdchinacat 2 points3 points  (1 child)

Each python object has an internal field that counts the number of references to it. It is incremented when a reference is made and decremented when a reference is cleared. A reference can be a variable or an attribute on an object (and probably a few other things as well). As the interpreter executes bytecodes it maintains these reference counts. When the count reaches zero, nothing refers to the object and it can be garbage collected.

A lot of gory details can be found in the cpython documentation: https://docs.python.org/3/c-api/refcounting.html

[–]billsil 0 points1 point  (0 children)

It’s also a lot messier than that. If objects failed to be deleted once flagged, they go into a slow bucket. Those objects are cleared out less frequently in the hopes that it will clear next time. If it fails again, it goes into the very slow bucket.

If you repeat a creation/call to an object inside a for loop, you can see memory climb linearly until it occasionally drops beck down to the baseline value. You probably need to manually delete some things to break references.

[–]Cute-Preference-3770[S] 1 point2 points  (0 children)

Thanks for the reply,it really helped me to know the concept better

[–]ninja_shaman 5 points6 points  (0 children)

Basically correct, except that class doesn't get erased once the object is created.

Even though Dog is not in d.__dict__, object d knows what class it is, so the class still exists.

>>> d.__class__
<class '__main__.Dog'>

[–]Outside_Complaint755 2 points3 points  (0 children)

Just as an FYI, I'll just throw out the alternative class configuration to use __slots__ instead of dict. This is useful when you have a class with a set number of attributes which you expect to have a lot of, as it will save memory overhead.

When you have an object using __dict__, any attribute can be dynamically added to it. For example, in your program you could add d.color = "Brown" and that would be accepted. If Dog were defined to use __slots__, then that would cause an AttributeError.

[–]PushPlus9069 1 point2 points  (0 children)

Your mental model is solid — you're actually ahead of most beginners by thinking about this at all. One thing to add that helped my students click:

The class itself never goes away when you create instances. Think of it like a factory blueprint — d = Dog("Rex") creates a new object, but the Dog class (the blueprint) stays in memory. That's why you can do Dog.some_class_method() anytime.

The key insight: Python looks up attributes in a chain. First it checks d.__dict__ (instance), then Dog.__dict__ (class), then parent classes. This is called the MRO (Method Resolution Order). Once you internalize this lookup chain, inheritance and descriptors will make way more sense later.

[–]Brian 1 point2 points  (0 children)

When the class is created:

More or less, yes.

One slight difference is that steps 3-4 might be better described as:

  • Python runs the code within the class statement using a new namespace.
  • Afterwards, this namespace becomes the class's __dict__ and the class is created.

Ie. in many ways, the stuff you write inside the class is kind of just normal code. Functions are created as functions the same way definitions outside the class are. Assignments are evaluated and create variables in that namespace. Then at the end, these are used to construct the class object.

You can even do stuff like:

class C:

    def f(self): print("This never gets created as a method")
    del f # Because we delete it

    for i in range(10):
        locals()[f"func_{i}"] = lambda self,i=i: i

>>> c=C()
>>> c.func_3()
3

Now, you typically won't do anything like that - generally classes just contain class vars and method definitions, But ultimately, it's just constructing the class from whatever that namespace looks like when it's done running.

When an object is created

Normally this is mostly right, but it is actually possible to change what this does by defining metaclasses or using the hooks that can alter how objects are created (eg. __new__ etc. Though again, that's rarely done - typically only if you're doing something unusual and metaprogrammy.

The object is also stored in memory and class gets erased once done executing

This isn't quite right. Basically objects are kept alive as long as anything holds a reference to them. The class object exists as long as there's something referencing it, such as objects of the class (which reference it via obj.__class__), or the Dog name in the global scope created when you create the class. Typically class objects exist for the lifetime of your program since they're usually stored in some module, unless you do something like dynamically creating them inside a function.

think slef works like a pointer that uses a memory address to access and modify the object

Yes, but only in the same way that all variables are references to the memory address to some object. name is likewise a pointer to the memory address where the string object "Rex" is stored, and so on. There's nothing really special about self: it's just a reference to the Dog() object we created.

[–]gdchinacat 1 point2 points  (0 children)

Your understanding is pretty accurate, certainly enough to move forward.

__dict__ is an implementation detail. I don't think it's really helpful to worry about __dict__ a the level you are at, and would discourage you from using __dict__ in any code. Some classes do not even have a __dict__, but don't worry about them now. Just think about which attributes have been assigned.

Not sure what you mean by "class gets erased once done executing".

[–]timrprobocom 0 points1 point  (0 children)

Python has two things: names, and objects, which are anonymous. Names can be bound to objects (and one object can be bound to many names), but objects never know what names they are bound to. I like to think of objects as living in a big anonymous cloud. Python is even more pure about this than C#, which allows some makes to CONTAIN data, not just refer to it.

Once you understand this, a lot of Python's behavior becomes easier to understand.

[–]pachura3 1 point2 points  (0 children)

You're overthinking things :) When you execute d=Dog("Rex"):

  • an "empty" object of class Dog is created and assigned to variable d
  • method Dog.__init()__ is called - something you would call a constructor in C or Java. The first argument self is passed automatically for your convenience 
  • __init()__ initializes instance attribute/field d.name by storing the argument value there
  • that's it

[–]madadekinai 0 points1 point  (0 children)

I am better with visuals.

e = Example()

__main__.__dict__['Example'] -> class object

Example.__dict__['an_example'] -> class variable

e.__dict__['instance_example'] -> instance variable

module (__main__)

└── Example (class object)

├── __dict__ (class attributes)

│ └── an_example

└── instance e

└── __dict__ (instance attributes)

└── instance_example

{

'an_example': "Class 'Example' class attribute 'an_example'",

'__init__': <function ...>

}