## Data types are object, eg. integer is object of class 'int'
a = 30
print(type(a)) # <class 'int'>
# print some attributes/methods of class int
print(dir(a)[:3]) # ['__abs__', '__add__', '__and__']
## Data structures are object, eg list is object of class 'list'
a = [30, 10, 20]
print(type(a)) # <class 'list'>
# print some attributes/methods of class list
print(dir(a)[:3]) # ['__add__', '__class__', '__class_getitem__']
## Functions are objects of class 'function'
def my_fun(): pass
print(type(my_fun)) # <class 'function'>
## Classes are objects of a metaclass 'type'
class MyClass: pass
print(MyClass.__class__) # <class 'type'>
class DummyClass:
def some_method():
pass
my_instance = DummyClass()
# Check if a object has some attribute
print(hasattr(my_instance, "x")) # False
# Use 'getattr()' to get the value of a object's attribute,
# it's similar to accessing with '.' operator though one benefit is
# if the attribute is not found, the provided 'default_value' is returned
# Syntax: getattr(<object>, "<attribute_name>", <default_value>)
print(getattr(my_instance, "x", 42)) # 42
# Set value to a object's attribute, similar to assigning with '=' operator
setattr(my_instance, "my_new_var", "Hello")
print(my_instance.my_new_var) # Hello
# Delete a object's attribute, returns nothing and raises
# 'AttributeError' if not found
delattr(my_instance, "my_new_var")
print(my_instance.my_new_var) # AttributeError
delattr(my_instance, "my_new_var") # AttributeError