First, some background:
What Computers Do
Generally, computers take data as input, follow algorithms, and produce data as output.
Data
Generally, data must be held for a while during execution of the algorithm. Python uses variables to store data.
>>> 123
>>> foo = 7
Data Types
Data comes in a number of types:
Atomic (can’t be split into smaller parts)
>>> foo = 7 # integer
>>> goo = 3.14 # float (floating point integer)
>>> hoo = "z" # string (Python has no separate type for 'character')
Structured
>>> koo = "My hovercraft is full of eels." # string of characters
>>> moo = [2, 13, 474] # list (of numbers)
>>> noo = [5, "bob", 6.02, "doggy"] # list with more than one type of element
>>> qoo = (1,2,45) # tuple (as in 'double', 'quadruple', '7-tuple')
>>> roo = ("frog", 14) # tuple with elements of different types
>>> soo = {8, 2048} # set
>>> too = {"dynomite", 7.62} # set with elements of different types
>>> voo = {
Deeper structure
>>> woo = [5.56, (2, 4, "Burrito"), ["a", 1723], "klingon"] # list with structures as elements
Putting data into complex structures, especially if they (as often) are uniform in shape, can get boring, should be automated where possible, and computers can be used for this.
Objects and Object-Oriented Programming (OOP)
Predefined structures for data can be defined and used. Most computer languages refer to
- these standardized structures as objects
- the structure definitions (not the concrete objects, more like the blueprints) as classes
- the process of creating an object of a class as instantiation
- one specific object as an instance of a class
- pieces of data held in Objects as attributes
- ways of doing things to the attributes held in objects, or ways of doing things using attributes as inputs, as methods
- Programming making use of this way of organizing and using data is Object-Oriented Programming.
Object-Oriented Programming in Python
In Python, to define a class: (this one is as simple as possible, and is useless other than counting, since it does not have any attributes or methods)
Define a class
We can define a class very simply.
class ScaryAnimal:
pass
class is a keyword. Capitalization is exact. Followed by a semicolon.
ScaryAnimal is a name, and, by convention, is CamelCase (LikeTheseWordsHere)
pass is Python placeholder code. We really should add methods and attributes.
Make an object of that class
We can make (‘instantiate’) one (at the REPL, or in stored code)
my_first_monster = ScaryAnimal() # make me a scary animal, and store that in the variable 'my_first_monster'
my_first_monster # let's see what Python thinks my_first_monster is
__main__.
means that ScaryAnimal is a ‘top-level’ class
object
means my_first_monster is a Python object
The hex number like (but this can vary every time we run code) 0x7fef14a46410
is the address, in memory, where my_first_monster starts.
Make the object so that it can store some useful information
__init__
This is a method, which is a function specialized on a class.
__init__
is a special class, and that is what the underscores are there to indicate.
When Python instantiates a class (makes an object of that class), Python looks for a method named __init__
, and, if Python finds it, executes the code therein.
__init__
is also called the constructor method, but I think this is a little off.
We can create objects without __init__
being defined (so without being found by Python).
If __init__
is found (and we should create one, at least for top-level classes) it
- creates attributes (I think of them as named slots), and
- gives these attributes initial values.
To be clear, we did, in fact, create my_first_monster without an __init__
method, so it is possible. Just not particularly useful, except if we just want to count monsters.
Attributes
Attributes are variables specialized on each object. If each monster has an attribute like number_of_eyes, we can have different monsters with different numbers of eyes.
Attributes (named slots that belong to a specific object) are created, and given an initial value by, __init__
.
If we want to specify, at the time we create a new object, the value of some attribute of the new object (e.g., eye_count = 8, or has_fangs = True) we can identify the parameters we must pass when instantiating this class.
A Second Class
class ScarierAnimal:
def __init__(self, number_of_eyes, has_fangs):
self.number_of_eyes = number_of_eyes
self.has_fangs = has_fangs
self is used to refer to the new object we are building, so we can use Python code to manipulate that new object.
Even though we have defined an __init__
for ScarierAnimal, We can still instantiate an object of type ScarierAnimal without having it assigned any attributes.
(I do not think this is good.)
>>> my_second_monster = ScarierAnimal
>>> my_second_monster.number_of_eyes
ERROR
But, now that we have defined __init__
, taking two parameters, we can instantiate a ScarierAnimal by passing values, like so:
>>> my_third_monster = ScarierAnimal(16, False)
>>> my_third_monster.number_of_eyes
16
Note that, in our definition of __init__
, there are three parameters. The first one, self, however, is special. It is automatically assigned the value of the object itself, so we do not (and better not try to) pass a value via self.
Note the names of the parameters on the right side of assignments like
self.eye_count = eye_count
do not need to match the names of the attributes on the left side.
ALSO NOTE that they conventionally do.
A Silly Class
# This would work, but would be silly:
class ScaryAnimalOdd:
def __init__(self, darth, leia):
self.eye_count = darth
self.has_fangs = leia
>>> critter_2 = ScaryAnimalOdd(3, False)
>>> critter_2.eye_count
3
More about Methods
Methods are functions for changing or using values held in attributes. They are specified as part of the class definition, and so apply to every instance of that class.
Perhaps we know that monsters sometimes lose eyes, and sometimes must be defanged.
We can define a couple of methods as part of a class.
def defang(self):
self.has_fangs = False
def poke_eye_out(self):
self.eye_count = self.eye_count - 1
Like in a class definition
class EvenMoreScaryAnimal:
def __init__(self, number_of_eyes, has_fangs):
self.number_of_eyes = number_of_eyes
self.has_fangs = has_fangs
def defang(self):
self.has_fangs = False
def poke_eye_out(self):
>>> my_fourth_monster = EvenMoreScaryAnimal(6, True)
>>> my_fourth_monster.number_of_eyes # no parens, so we want to learn about the **attribute** 'eye_count'
6
>>>my_fourth_monster.has_fangs
True
>>> my_fourth_monster.poke_eye_out()
>>> my_fourth_monster.number_of_eyes
5
Notes:
- the parentheses indicate that we are invoking a function/method.
- even though self is a parameter in the definition of methods, no value is ever expected to be passed via self, and one (the special one of the object being called, like a leash) is what self evaluates to within the method definition.
Sub-Classes
Often making sub-classifications is useful, because, although objects in a sub-class (like SportsCar) might share attributes (engine_model) and methods with (add_miles()) with a parent class (like MotorVehicle), they might also have different attributes or methods (racing_league).
We can efficiently capture this by creating subclasses.