From GeeksforGeeks

Context Managers

##################
# File example
class FileManager():
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None
          
    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file
      
    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.file.close()
  
# loading a file 
with FileManager('test.txt', 'w') as f:
    f.write('Test')
  
print(f.closed)

##################
# Database example
from pymongo import MongoClient
  
class MongoDBConnectionManager():
    def __init__(self, hostname, port):
        self.hostname = hostname
        self.port = port
        self.connection = None
  
    def __enter__(self):
        self.connection = MongoClient(self.hostname, self.port)
        return self
  
    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.connection.close()
  
# connecting with a localhost
with MongoDBConnectionManager('localhost', '27017') as mongo:
    collection = mongo.connection.SampleDb.test
    data = collection.find({'_id': 1})
    print(data.get('name'))

Python instance variables are stored as dict.

class C:
    def __init__(self):
        self.a = 1
        self.b = 2
        self.c = 2

a_object = C()
instance_variables = vars(a_object)

print(instance_variables)

# Prints {'a': 1, 'b': 2, 'c': 2}

# Using Slots
class ArticleWithSlots:
    __slots__ = ["date", "writer"]

    def __init__(self, date, writer):
        self.date = date
        self.writer = writer

However, with large amounts of instance variables, dict access and set becomes O(n). One solution is to use slot.

Reduces RAM, faster attribute access,

Tradeoffs

Once you create slot, you can't change it, it is fixed.

# Normal
x = 0
def outer():
    x = 1
    def inner():
        x = 2
        print("inner:", x)

    inner()
    print("outer:", x)

outer()
print("global:", x)
# inner: 2
# outer: 1
# global: 0

# Nonlocal
x = 0
def outer():
    x = 1
    def inner():
        nonlocal x
        x = 2
        print("inner:", x)

    inner()
    print("outer:", x)

outer()
print("global:", x)
# inner: 2
# outer: 2
# global: 0

# Global
x = 0
def outer():
    x = 1
    def inner():
        global x
        x = 2
        print("inner:", x)

    inner()
    print("outer:", x)

outer()
print("global:", x)
# inner: 2
# outer: 1
# global: 2

Global Interpreter Lock