############################################################
# querying object types
############################################################


# This week and next, we will examine more deeply the notion of an
# object's type. As we're familiar with by now, every object belongs to
# a certain type, and Python provides many built-in types, e.g., int,
# float, bool, str, list, dict, etc.

# To examine an object's type, we can pass it into the type() function.
# We've hinted at this before:
#   + In Lecture 9's pre-lecture code, we used type() to demonstrate
#     that file objects have a certain type.
#   + In Lecture 13, we used a DFS strategy to count the number of non-
#     list items within a structure of nested lists. We used type() to
#     distinguish whether each list element was still a list (and hence
#     needed to be handled recursively or not (and hence needed to be
#     tallied into our count).

# Thus, type() is a handy built-in function that allows us to query an
# object's type. The following code block prints out information
# indicating that the objects passed in are indeed an int, a float, and
# a list.

print()
print(type(123))
print(type(123.0))
print(type([123.0]))

# However, the printouts also suggest more is happening behind the
# scenes:
#    a) The fact that something is printed implies that there are actual
#       objects representing the **concepts** of the int, float, and
#       list types.
#    b) The printouts also suggest that these objects are represented by
#       something called a "class".

# The following code block prints out the exact same class objects.
# Thus, we can infer that the terms `int`, `float, and `list` in code
# are actually **variables pointing to those objects.**

print()
print(int)
print(float)
print(list)

# Now that we know this, you should be able to draw this in an
# environment diagram. First, draw the objects on the heap for 123,
# 123.0, and [123.0]. These objects should be labeled with type int,
# float, and list, respectively. But what do int, float, and list mean?
# Python effectively pre-populates these variables in the global frame,
# and they point to "class" objects on the heap, each of which give
# actual meaning to the **concept** of an int, float, or list.

# But wait, those "class" objects need to be labeled with their types as
# well. What could they possibly be?!

print()
print(type(int))
print(type(float))
print(type(list))

# Aha! Python tells us that objects representing types have type `type`.
# That sounds circuitous, but hopefully reasonable: We can label the
# type of those objects as `type`.

# And because we now have a new type label `type`, we can apply the same
# reasoning to infer that `type` must also be a variable that refers to
# a "class" object. And that object's type is also `type`.

print()
print(type)
print(type(type))

# Sound confusing? Try drawing the environment diagram as described, and
# we'll review it in class.

# The main takeaway is that Python uses objects to represent object
# types themselves, and this is **game-changing**: If Python represents
# types using its own variable/object mechanisms, then we should be able
# to define our own types as well and create objects of those types.

# Why would we want to do this? To be fair, we can accomplish a lot with
# Python's built-in types alone. Sometimes, though, it's convenient to
# think in terms of more application-specific concepts. Being able to
# define and use your own types is a powerful abstraction (and with
# great power comes great responsibility), and we will see several
# examples of this over the next few weeks.
