#You might start off with
name = "hero"
health = 100
level = 10
#or if you were clever and wanted to make a single object you might just go with a dictionary
hero = {name: "hero", health: 100, level: 1}
But now let's say we want to make a monster for the hero to fight.
monster_1 = {name: "Gargantuan", health: 1000, level: 100}
#Now I have the monster, but I need a bunch of them to fight, but we can at least always give them the same stats
#I'll let you all make two more. Also put in a category that declares what the monster wants to fight
#make your monsters here. Actually, make 10
As you can see, this can get pretty old, make difficult to read code, and just suck in general. But we can make this much easier! And do some powerful things along the way. First, let's make a class.
A blueprint for an object that contains:
- Properties
- Functions related to that object type
class Creature():
#the equals I am putting here represent what the default will be if nothing is passed
def __init__(self, name, health=10, level=1):
self.name = name
self.health = health
self.level = level
def receive_damage(self,damage): #we'll talk about why I am writing this this way in a second.
self.health = self.health - damage
The "__init__" part is sometimes called a constructer. It determines what happens when an instance of that class is created or instantiated
#Now let's create some
hero = Creature("hero1", 100, 10)
monster1 = Creature("Gargantuan", 1000, 100)
print("monster name: " + str(monster1.name) )
print("hero level: " + str(hero.level) )
#Can we make a few more even more easily? Yes! With default values!
Yay! Now we have some characters! But what if we want to create a bunch? Let's make a list!
list_of_creatures=[Creature("monster" + str(i)) for i in range(10)]
print("Monster number: " + list_of_creatures[0].name)
#make it automatically make reciprocal enemies by self.enemy = new_enemy and new_enemy.enemy = self
#at least mention static and class methods
Question: what is the difference between a function and method?
One of the fundamental concepts of OOP is inheritence. Basically it means you can create a subclass of a class and import everything from the parent class automatically. For example: a monster is a creature. But we want to add other properties to the monster.
class Monster(Creature):
#these are class properties. These are shared by the entire class, and are not particular to a single instance.
monster_count = 0
def __init__(self, name, health=10, level=1):
#anything coded here with "self" is an instance property
Creature.__init__(self, name, health, level) #here we must include a call to the parent class's init function
#if we want to keeep it because the child class init overrides it
self.enemy = [] #this will be a list of all enemies
Monster.monster_count = Monster.monster_count + 1
def add_enemy(self, enemy):
self.enemy.append(enemy)
enemy.enemy = self #This is very interesting, we can program a reciprocal change of state!
#You'll also notice we never declared an enemy property in Creature, but
#we're doing it anyway. Python is very flexible, but you probably
#shouldn't do this!
def attack(self, enemy):
enemy.health = enemy.health - 1 #Why is this method a terrible idea?
#what should I make instead?
def attack_creature(self, enemy): #fill this one in in a better way
enemy.receive_damage(1)
my_monster = Monster("monster15")
my_monster.add_enemy(hero)
print("Enemy of monster: " + my_monster.enemy[0].name)
print("Enemy of hero: " + hero.enemy.name)
print("Monster count: " + str(Monster.monster_count))
#Now what if we make a new monster?
second_monster = Monster("Monster16")
print("Monster count: " + str(Monster.monster_count))
#attack
my_monster.attack_creature(hero)
my_monster.__dict__
These are actually two different concepts but we'll combine them because they do overlap. Basically, they mean we create objects with the idea of focusing on what they do rather than how they work. Everyone knows what a car does and the methods for getting it from point A to point B, at least in principle. But far fewer people know all the details of how everything works under the hood.
Where have we seen this so far?
1. __init__:
Dunder is a way for Python to make sure a method or porperty is not easily called outside the class.
2. The attack method
This basically means that objects can be created that do the same thing with different data types. Python has so much polymorphism flowing through it it really is hardly worth it to point it out. The biggest reason to notice is to point out that it is acceptable to override a method of the same name from a higher class. That can be a big time saver or make things work that didn't before. But it can also lead to errors later when people mess with your code and remove yur override and leave the code depending on it behind for example. Mostly that's important to know for some high-level debugging. But in data science you should never really have to know that.
#look at everything inside just one object
class myClass():
pass
thing = myClass()
dir(thing)
class fMRI_Data_Volume():
def __init__(self, volume, scan_type):
self.volume = volume
self.scan_type = scan_type
def double_size(self):
self.volume.extend(self.volume) #adds a copy of the list to the list
return self
def halve_size(self):
self.volume = self.volume[:round((len(self.volume) / 2))] #selects the first half of my list
return self
def add_a(self,item):
self.volume.append(item)
return self
def add_items(self,items):
self.volume.extend(items)
return self
my_volume = fMRI_Data_Volume([5,256,3], "dti")
print(my_volume.volume)
my_volume.double_size()
print(my_volume.volume)
my_volume.double_size().halve_size()
print(my_volume.volume)
my_volume.double_size().add_a(6).halve_size().add_a(6).add_items([2,8,7,6,5,4,3,2,1]).double_size()
print(my_volume.volume)
def func():
print("blah")
func()
#functions can have attributes
#func.other_name = "hello"
#func.other_name
#we can even reassign, just like any other object
bloop = func
bloop()
#What are all the parts of bloop? What does it inherit?
dir(bloop)
#what about just inside of the object?
bloop.__dict__
#There was nothing inside. Let's add an attribute
#Then we can see what belongs to just this object
bloop.my_attr = "1st attribute"
#now
bloop.__dict__
#What about the string "hello"
dir("hello")
#What else can we see inside?
dir(bloop.__code__)
anything under if __name__ == '__main__': runs when running the class as a script. If you ever import it into something else, it won't
class Creature():
#the equals I am putting here represent what the default will be if nothing is passed
def __init__(self, name, health=10, level=1):
self.name = name
self.health = health
self.level = level
def receive_damage(self,damage): #we'll talk about why I am writing this this way in a second.
self.health = self.health - damage
#let's test: if you import it later, it won't run
if __name__ == '__main__':
a_hero = Creature("Jordyn")
print(a_hero.name)
from Creature import Creature new_c = Creature("hello")
__getattr__ intercepts access to nonexistent attributes. Can be used to route arbitrary access to a wrapped object. retains the interface of the wrapped object and may add additional operations of its own. Basically, you might need them when using code that changes interfaces, like from an IP port to Bluetooth, or from one programming language to another.