a = 1
print(type(a))

print(type(int))

"""
Explanation: Python Classes Strike Again
The Python implementation of classes has some peculiarities. The bare truth is that in Python the class of an object is an object itself. You can check this by issuing type() on the class
End of explanation
"""


class Door:
    def __init__(self, number, status):
        self.number = number
        self.status = status
        
    def open(self):
        self.status = 'open'
        
    def close(self):
        self.status = 'closed'

"""
Explanation: This shows that the int class is an object, an instance of the type class.
This concept is not so difficult to grasp as it can seem at first sight: in the real world we deal with concepts using them like things: for example we can talk about the concept of "door", telling people how a door looks like and how it works. In this case the concept of door is the topic of our discussion, so in our everyday experience the type of an object is an object itself. In Python this can be expressed by saying that everything is an object.
If the class of an object is itself an instance it is a concrete object and is stored somewhere in memory. Let us leverage the inspection capabilities of Python and its id() function to check the status of our objects. The id() built-in function returns the memory position of an object.
In the first post we defined this class
End of explanation
"""


door1 = Door(1, 'closed')
door2 = Door(1, 'closed')

hex(id(door1))

hex(id(door2))

"""
Explanation: First of all, let's create two instances of the Door class and check that the two objects are stored at different addresses
End of explanation
"""


hex(id(door1.__class__))

hex(id(door2.__class__))

"""
Explanation: This confirms that the two instances are separate and unrelated.
The second instance was given the same attributes of the first instance to show that the two are different objects regardless of the value of the attributes.
However if we use id() on the class of the two instances we discover that the class is exactly the same
End of explanation
"""


class Door:
    colour = 'brown'

    def __init__(self, number, status):
        self.number = number
        self.status = status
        
    def open(self):
        self.status = 'open'
        
    def close(self):
        self.status = 'closed'

"""
Explanation: Well this is very important. In Python, a class is not just the schema used to build an object. Rather, the class is a shared living object, which code is accessed at run time.
As we already tested, however, attributes are not stored in the class but in every instance, due to the fact that __init__() works on self when creating them. Classes, however, can be given attributes like any other object; with a terrific effort of imagination, let's call them class attributes.
As you can expect, class attributes are shared among the class instances just like their container
End of explanation
"""


door1 = Door(1, 'closed')
door2 = Door(2, 'closed')

Door.colour

door1.colour

door2.colour

"""
Explanation: Pay attention: the colour attribute here is not created using self, so it is contained in the class and shared among instances
End of explanation
"""


Door.colour = 'white'
Door.colour

door1.colour

door2.colour

hex(id(Door.colour))

hex(id(door1.colour))

hex(id(door2.colour))

"""
Explanation: Until here things are not different from the previous case. Let's see if changes of the shared value reflect on all instances
End of explanation
"""


door1 = Door(1, 'closed')
door2 = Door(2, 'closed')
print(type(Door.__dict__))
Door.__dict__

print(type(door1.__dict__))
door1.__dict__

"""
Explanation: Raiders of the Lost Attribute
Any Python object is automatically given a __dict__ attribute, which contains its list of attributes. Let's investigate what this dictionary contains for our example objects:
End of explanation
"""


try:
    door1.__dict__['colour']
except KeyError as e:
    print("Cannot find key {}".format(e))

door1.__class__.__dict__['colour']

"""
Explanation: Leaving aside the difference between a dictionary and a mappingproxy object, you can see that the colour attribute is listed among the Door class attributes, while status and number are listed for the instance.
How comes that we can call door1.colour, if that attribute is not listed for that instance? This is a job performed by the magic __getattribute__() method; in Python the dotted syntax automatically invokes this method so when we write door1.colour, Python executes door1.__getattribute__('colour'). That method performs the  attribute  lookup action, i.e. finds the value of the attribute by looking in different places.
The standard implementation of __getattribute__() searches first the internal dictionary (__dict__) of an object, then the type of the object itself; in this case door1.__getattribute__('colour') executes first door1.__dict__['colour'] and then, since the latter raises a KeyError exception, door1.__class__.__dict__['colour']
End of explanation
"""


door1.colour is Door.colour

"""
Explanation: Indeed, if we compare the objects' equality through the is operator we can confirm that both door1.colour and Door.colour are exactly the same object
End of explanation
"""


door1.colour = 'white'
door1.__dict__['colour']

door1.__class__.__dict__['colour']

Door.colour = 'red'
door1.__dict__['colour']

door1.__class__.__dict__['colour']

"""
Explanation: When we try to assign a value to a class attribute directly on an instance, we just put in the __dict__ of the instance a value with that name, and this value masks the class attribute since it is found first by __getattribute__(). As you can see from the examples of the previous section, this is different from changing the value of the attribute on the class itself.
End of explanation
"""


door1.open is Door.open

"""
Explanation: Revenge of the Methods
Let's play the same game with methods. First of all you can see that, just like class attributes, methods are listed only in the class __dict__. Chances are that they behave the same as attributes when we get them
End of explanation
"""


Door.__dict__['open']

Door.open

door1.open

"""
Explanation: Whoops. Let us further investigate the matter
End of explanation
"""


try:
    Door.open()
except TypeError as e:
    print(e)

"""
Explanation: So, the class method is listed in the members dictionary as function. So far, so good. The same happens when taking it directly from the class; here Python 2 needed to introduce unbound methods, which are not present in Python 3. Taking it from the instance returns a bound method.
Well, a function is a procedure you named and defined with the def statement. When you refer to a function as part of a class in Python 3 you get a plain function, without any difference from a function defined outside a class.
When you get the function from an instance, however, it becomes a bound method. The name method simply means "a function inside an object", according to the usual OOP definitions, while bound signals that the method is linked to that instance. Why does Python bother with methods being bound or not? And how does Python transform a function into a bound method?
First of all, if you try to call a class function you get an error
End of explanation
"""


Door.open(door1)
door1.status

"""
Explanation: Yes. Indeed the function was defined to require an argument called 'self', and calling it without an argument raises an exception. This perhaps means that we can give it one instance of the class and make it work
End of explanation
"""


door1.__class__.__dict__['open']

"""
Explanation: Python does not complain here, and the method works as expected. So Door.open(door1) is the same as door1.open(), and this is the difference between a plain function coming from a class an a bound method: the bound method automatically passes the instance as an argument to the function.
Again, under the hood, __getattribute__() is working to make everything work and when we call door1.open(), Python actually calls door1.__class__.open(door1). However, door1.__class__.open is a plain function, so there is something more that converts it into a bound method that Python can safely call.
When you access a member of an object, Python calls __getattribute__() to satisfy the request. This magic method, however, conforms to a procedure known as descriptor protocol. For the read access __getattribute__() checks if the object has a __get__() method and calls this latter. So the converstion of a function into a bound method happens through such a mechanism. Let us review it by means of an example.
End of explanation
"""


dir(door1.__class__.__dict__['open'])

door1.__class__.__dict__['open'].__get__

"""
Explanation: This syntax retrieves the function defined in the class; the function knows nothing about objects, but it is an object (remember "everything is an object"). So we can look inside it with the dir() built-in function
End of explanation
"""


door1.__class__.__dict__['open'].__get__(door1)

"""
Explanation: As you can see, a __get__ method is listed among the members of the function, and Python recognizes it as a method-wrapper. This method shall connect the open function to the door1 instance, so we can call it passing the instance alone
End of explanation
"""


Door.open

door1.open

type(Door.open)

type(door1.open)

"""
Explanation: and we get exactly what we were looking for. This complex syntax is what happens behind the scenes when we call a method of an instance.
When Methods met Classes
Using type() on functions defined inside classes reveals some other details on their internal representation
End of explanation
"""


class Door:
    colour = 'brown'

    def __init__(self, number, status):
        self.number = number
        self.status = status

    @classmethod
    def knock(cls):
        print("Knock!")

    def open(self):
        self.status = 'open'
        
    def close(self):
        self.status = 'closed'

"""
Explanation: As you can see, Python tells the two apart recognizing the first as a function and the second as a method, where the second is a function bound to an instance.
What if we want to define a function that operates on the class instead of operating on the instance? As we may define class attributes, we may also define class methods in Python, through the classmethod decorator. Class methods are functions that are bound to the class and not to an instance.
End of explanation
"""


door1 = Door(1, 'closed')
door1.knock()

Door.knock()

"""
Explanation: Such a definition makes the method callable on both the instance and the class
End of explanation
"""


door1.__class__.__dict__['knock']

door1.knock

Door.knock

print(type(Door.knock))

print(type(door1.knock))

"""
Explanation: and Python identifies both as (bound) methods
End of explanation
"""


class Door:
    colour = 'brown'

    def __init__(self, number, status):
        self.number = number
        self.status = status

    @classmethod
    def knock(cls):
        print("Knock!")

    @classmethod
    def paint(cls, colour):
        cls.colour = colour

    def open(self):
        self.status = 'open'
        
    def close(self):
        self.status = 'closed'

"""
Explanation: As you can see the knock() function accepts one argument, which is called cls just to remember that it is not an instance but the class itself. This means that inside the function we can operate on the class, and the class is shared among instances.
End of explanation
"""


door1 = Door(1, 'closed')
door2 = Door(2, 'closed')
Door.colour

door1.colour

door2.colour

Door.paint('white')
Door.colour

door1.colour

door2.colour

"""
Explanation: The paint() classmethod now changes the class attribute colour which is shared among instances. Let's check how it works
End of explanation
"""


door1.paint('yellow')
Door.colour

door1.colour

door2.colour

"""
Explanation: The class method can be called on the class, but this affects both the class and the instances, since the colour attribute of instances is taken at runtime from the shared class.
End of explanation
"""


