And we are done with objects!
This course finally gave me what I was looking for all these months.
The ability to think of and reason about Python, so that I can then think of and reason about, how to build my own programs.

Notes

  • Abstract base classes
    • Sounds all high and mighty, but does something pretty logical and simple
    • Helps me test if my object is of a generic (of sorts) type. I don’t want to test if 5 is an integer or a float or any of the other various county types in Python. I just want to see if it’s a number, of some sort. The abstract base class numbers helps me do that.
    • There are such classes for strings and files, I hear.
    • Will go into learning more about them in detail later, as I actually write more Python. But at least I know what they are now.

** Part 3, Context Managers**

Again, Context Managers, sound like something really complex, but they are just a matter of writing and doing things in a certain manner.

  • It’s something like a Tiny Habit stack loop, I learnt in the Atomic Habits book, as I was building up new habits.

    • When I do this,
    • then I will do this immediately after.
    • and when I am done, I will do that.
  • Context Managers have a similar paradigm.

    • With this object (as this variable, optionally)
      • I will run the __enter__ method
      • then after, I will do stuff
      • and when I am done, I will run the __exit__ method
    • I can add the context manager protocol to my objects too, by impletementing the __enter__ and __exit__ methods in my classes
    • They are used in a particular context, where you want to turn something on and then turn something off, before and after that context, thus Context Manager.
      • I can set something up in my __enter__ method and then tear it down in my __exit__ method.
      • This could be, for example, a network connection, a file, a redefinition of some sort, etc.
      • The __enter__ method must return self if all has to work well.
      • The __exit__ block, takes four arguments, one for self, one for an error class, one for the object, and one for the address in memory, the traceback. I can use them to catch errors in the main body of my with block, and then decide what the appropriate way to deal with them is.
      • A good example is using redirecting the print function’s output to a file, using a context manager. (The print function can write to a file, when I specify it as an output device like so print ("Hello there", file=f)
      • While I might be able to catch errors in the body of the context manager, I cannot if something happens during __enter__. just something to be aware of.
  • Contextlib is a library of context managers, Python already provides us.

    • import contextlib
    • contextlib.redirect_stdout/err will redirect stuff from the default output (the screen) to whereever else you want it to.

** Part 4, Properties and Descriptors **

  • A property is an attribute, that acts like getters/setters in other languages. It uses methods to set other attributes in the class (within the bounds we set). For e.g. a .temp property that will let you set temperatures, but only in the range you specify. not below 18C and not above 25C.
  • set by putting @property atop a getter function (let’s say temp) and then defining the setter properties in an identically named temp function again but decorated with @function_name.setter; in our case @temp.setter
  • the whole actual way to do it is like a contortionist twisting into a pretzel shape. Getting fluent with this will take time and practice.
  • it basically (to my mind) is like me giving someone something, and that someone goes and magician like, does something marvellous with that something, instead of just storing it in their pocket. it looks like assigning data to an attribute, but behind the scenes there are methods, just waiting to weave spells on said data.
  • When I take a property out of a class and abstact it, so that it can be used by many classes, it’s called a descriptor. (kinda like abstracting functions out of program files and into modules)
  • To kinda formally state, a descriptor is a class whose instances are supposed to be class attributes in other classes.
    • To fit the descriptor protocol, a descriptor class needs to implement…
      • a __get__(self, host_instance, host_class) method with a corresponding __set__(self, host_instance, new_value_to_set) method
    • These are class specific. As in, the descriptor is shared amongst all the instances it is used in.
    • To get instance specific descriptors, you set the value that you are changing to be assigned as a dictionary using the host instance as the key and the value, as the value. (More contorting!) and then return the value of the [host_instance] value.
    • Definitely tons of practice needed with this.
    • Not used much, in day to day work, but used heavily behind the scenes by Python itself for its object system. The methods, Python classes have, are plain old functions, with descriptors (in conjunction with weak referenced elements) applied to them.
    • The more I dive into all of this, the more Python feels less magical and more like the work of thousands of hard working atomic elements. And I think, I like this better. That everything complex, is actually lots of simple pieces, working together in concert.

** Part 5, Advanced Topics **

  • The only thing I can write here, is that Reuven goes deep into hijacking how Python creates classes and then using them for our own customised ends. How to create my own custom __new__ method, my own custom __init__ method, my own custom type of object, create my own class with a metaclass…
  • Never going to use this, unless I am writing the next Django or something, and even then I doubt, these would be needed 😂
  • But this was so much fun to learn!

Read all about my Python Objects journey here