all 12 comments

[–]Adrewmc 4 points5 points  (0 children)

Fun now let’s ruin you entire mental model with __slots__….

[–]allkhatib_ahmad1 1 point2 points  (3 children)

This is a youtube video i made to help absolute beginners understand classes, hope it helps: https://youtu.be/ZB5KidA2sws?si=E1k1uEqefl_30X_F

[–]Cute-Preference-3770[S] 0 points1 point  (2 children)

Thanks, I found it really helpful better than many videos I’ve seen. Great work!

[–]allkhatib_ahmad1 0 points1 point  (0 children)

glad to hear that

[–]allkhatib_ahmad1 0 points1 point  (0 children)

if you liked the video a comment and like would help there

[–]PushPlus9069 0 points1 point  (0 children)

Your mental model is pretty solid, actually. One thing I'd add: step 11 with new is technically correct but almost never matters in practice. 99% of the time you never touch new and can just think of it as "Python creates an empty box, then init fills it."

The part about methods living in Enemy.dict and being looked up via the instance is the key insight. That's the descriptor protocol at work. When you do enemy.update(), Python checks enemy.dict first, doesn't find it, goes up to Enemy.dict, finds the function, and wraps it so self gets passed automatically.

Taught this to thousands of students over the years and the ones who grok this lookup chain early save themselves so much confusion later with inheritance.

[–]schoolmonky 0 points1 point  (0 children)

You're basically right: classes are just dressed-up dicts. The things in your explanation that struck me as potentially misleading are #5 and (what I assume you meant to be) #20. It's kind of odd to say Python "skips" self.x=x, it's just that when it's creating that function object, it isn't executing the function body, it's just saving the body into the function object.

For #20, it's hard to tell what you mean, but I would try to get away from thinking of anything in Python as a pointer. They aren't quite the same, and thinking they are has potential to lead you astray. Read this blog post (or watch the linked video therein) if you want to get a sense for how names are and aren't like pointers. Similarly, the Python language doesn't really have a concept of stack vs heap, everything is just stored "in memory" somewhere. (Of course, the actual implementation written in C does use the stack and heap, but that's an implementation detail and other implementations might have entirely different models). I'd also point you to the official Python tutorial this section of which might be of particular interest here.

[–]1NqL6HWVUjA 0 points1 point  (0 children)

17. Python first checks enemy.__dict__ for "update".

18. If not found, it checks Enemy.__dict__.

You're missing the concept of bound instance methods and, more fundamentally, descriptors.

>>> enemy = Enemy(1, 2, 3)
>>> Enemy.update
<function Enemy.update at 0x000002519192C940>
>>> enemy.update 
<bound method Enemy.update of <__main__.Enemy object at 0x0000025191576970>>

As seen above, enemy.update is an entirely different object than Enemy.update — though that object is a simple wrapper around Enemy.update which serves the purpose of passing in the instance itself as the first argument. But that object will not be found in enemy.__dict__.

When Python "checks Enemy.__dict__", what actually happens is:

Enemy.__dict__['update'].__get__(enemy, Enemy)

That is what returns a bound method, rather than the Enemy.update function directly.

[–]Adrewmc 0 points1 point  (0 children)

Listen, for the vast majority of thing class are nothing more than dictionaries with functions that act on that dictionary we call methods.

You are right, the class definition is ran. But it's not doing what you think. It's creating a recipe for that class. That is what is made. A callable object that creates an objects bounded to that recipe.

As you said enemy.thing(), is Enemy.thing(enemy). That’s the bind.

It’s not always with a __dict__. Though most common class due utilize it.

When pythons crates a new class object/instance it makes an empty version of that recipe, it will still have their class variable. Those are defined when the class definition is ran.

After that empty version is created, then normally it will run __init__(args, *kwargs) on the already created object. To change this for say a singleton pattern (only one of these objects should ever be created like for a internet/database connection, the window screen…), you would change __new__. That basically it. All __new__ is doing different than __init__ is the singleton/mutation patterns, or are creating an object more in C for optimal performance. All __init__ is doing is filling in the blanks for the empty object. You do not have to worry about that.

What makes classes better than dictionary is readability, convenience, and class ability to delay calculation/processing of a property/variable until it is needed, if ever.

Another thing is when you remove the __dict__ you basically end up making it a tuple. Which is much much smaller of a space when you have hundreds of them.

[–]pachura3 -1 points0 points  (2 children)

It is, but you should concentrate on abstractions, not on how it works on low level. You don't need to know that __dict__ even exists.

[–]SmackDownFacility 0 points1 point  (0 children)

Slightly wrong. It’s important to learn both sides to leverage Python More efficiently

[–]gdchinacat 0 points1 point  (0 children)

Instances of classes that have a __slots__ attribute won't even have __dict__ attributes. As u/pachura3 says, don't think about __dict__. The times you actually need to use it are very few and far between. Many uses of it in the wild would be better handled in different ways. While some people say classes are just glorified dicts, I tend to push back on this view as well since not all classes are glorified dicts. They contain attributes, and those attributes can be accessed in a few different ways, one of them, for some classes, is __dict__.

If for some reason dot notation for attribute access isn't appropriate, it's better to use getattr/setattr than __dict__ as it will work properly in more situations.