import datetime
print(datetime.datetime.now().isoformat())
2022-07-08T13:41:54.956566
The idea of "types" is less a matter of computer science or mathematics, and more a matter of ordinary language, wherein we learn to group things by their type.
The screwdriver is a type of tool. A car is a type of motorvehicle. A motorvehicle is a type of vehicle.
If I say "Sheila is a type of animal" then the game might be to guess which one.
Is Sheila a dog, cat, octopus or parrot?
Depending on which "type" (or "species") of animal Sheila is, we might expect certain capabilities, behaviors, attributes. If Sheila is a giraffe, we would expect she has a long neck.
Humans have presumably learned to typify objects ever since they started thinking, whenever that was (a matter of ongoing research). Identifying the type of something, say a plant, is a core survival skill.
Which plants are safe to eat? We know by recognizing their type.
"Oriented" means "pointing in that direction" or "especially fit for that use". Python is both type oriented and object oriented. We think in terms of objects, each of a specific type or types (an object may be of more than one type in Python).
In Python and other similar computer languages (such as Java), we're free to set up a kind of "type scheme" like an "ecosystem" of several kinds of object. Perhaps a "jungle" would be a good word, in terms of variety and propensity to grow.
This jungle is designed to get work done in some way. One object gets data from a gigantic database. Another object wraps that data in pretty-to-look-at styling (HTML + CSS). These objects pass objects between them.
The surrounding Notebook is perhaps our guide book, explaining what's here and what it does.
Let's code up a Parrot type:
class Parrot:
"""
A type -- same idea as class (the class of all Parrots)
"""
def __init__(self, nm):
# triggered by calling Parrot
self.name = nm
def __call__(self, say_it):
# triggered by calling a Parrot self
return f"{self.name} says '{say_it}'"
def __repr__(self):
# represents a Parrot self, called "the repper"
return f"Parrot named {self.name}"
You may remember from meetup one, our introduction to "special names" (also known as "magic methods"). They're the ones with the double-underline on both sides. Python uses the underline character extensively. That's one of its hallmark traits.
__repr__
, for example, the last method under Parrot (it didn't have to be last -- method order is arbitrary meaning it doesn't matter), is a special name, and what we call "the repper".
When an object needs to represent itself as a string, a set of characters, this method will be used. Unless, that is, there's also an __str__
in which case it gets priority. We'll try that later.
pet = Parrot("Sheila") # create an instance of the Parrot type, triggers __init__
pet # __repr__ fires (executes)
Parrot named Sheila
type(pet) # what type of object am I? repper of the class itself
__main__.Parrot
isinstance(pet, Parrot) # am I an instance of a Parrot?
True
pet("Hello! Hello!") # triggers __call__
"Sheila says 'Hello! Hello!'"
class Dog:
"""
A type
"""
def __init__(self, nm):
self.name = nm
self.stomach = [ ]
def eat(self, food):
self(food) # triggers __call__
def __call__(self, food):
self.stomach.append(food)
def __repr__(self):
return f"Dog named {self.name}"
dog_1 = Dog("Rover")
dog_1
Dog named Rover
dog_1.eat('🍕') # could be the word 'pizza' also
dog_1.stomach
['🍕']
dog_1('🍩') # same as .eat, but this time a donut
dog_1.stomach
['🍕', '🍩']
from random import choice
# warning: this is not healthy food for a real dog. For learning purposes only.
foods = set(['🍩', '🍌', '🍪', '🍕']).union(['🍩','🍨', '🍰','🍪'])
foods = list(foods) # choice uses left-to-right indexing so convert to a list
kennel = [Dog("Rover"), Dog("Sheila"), Dog("Fido"), Dog("Wolfie")]
for dog in kennel:
for _ in range(3):
dog.eat(choice(foods))
kennel
[Dog named Rover, Dog named Sheila, Dog named Fido, Dog named Wolfie]
for dog in kennel:
print(f"{dog.name:<10}: {dog.stomach}")
Rover : ['🍌', '🍨', '🍕'] Sheila : ['🍩', '🍰', '🍌'] Fido : ['🍌', '🍪', '🍰'] Wolfie : ['🍌', '🍕', '🍨']
class Bird:
"""
Adapted from Parrot, adding __str__
"""
def __init__(self, nm):
self.name = nm
def __call__(self, say_it):
return f"{self.name} says '{say_it}'"
def __repr__(self):
return f"{self.__class__.__name__} named {self.name}"
def __str__(self):
# testing what this does
return f"I am {self.name}"
crow = Bird("123")
print(crow) # triggers __str__
I am 123
crow # triggers __repr__
Bird named 123
Lets remember how Parrot works. We still have pet
from earlier, an instance of the Parrot type.
pet
Parrot named Sheila
The rule is, print(obj) which fires str(obj), will look for a __str__
method first, allowing it to be something different from __repr__
. Oft times when designing types, a programmer will appreciate having this optional distinction, between __str__
and __repr__
.
print(pet) # to __str__ so fall back to __repr__
Parrot named Sheila
str(pet)
'Parrot named Sheila'
repr(pet)
'Parrot named Sheila'
repr(crow) # fire the repper
'Bird named 123'
And now for something completely different
Ornery Type at Replit.it
If the link is operational, it will take you to a codepen at Replit where a class named Ornery is defined.
"Ornery" means "in a bad mood" which is the theme for this type's personality.
The special names are more of the usual ones that Python offers, not names we make up.
How does an object work with brackets? That's for __setitem__
and __getitem__
to determine.
How should an object behave when we access it with a dot (the "accessor"?). That's a job for __getattr__
(pronounced "get atter"). We want an object to check for the attribute and only run __getattr__
as a last resort, if the attribute is not located.
class Anything:
def __getattr__(self, attr):
print(f"Do something with {attr}")
any_object = Anything()
# give it some attributes
any_object.color = "brown"
any_object.edible = True
any_object.name = "cookie"
any_object.__dict__ # here's where internal attributes get saved
{'color': 'brown', 'edible': True, 'name': 'cookie'}
any_object.color
'brown'
any_object.age # this doesn't exist, so run __getattr__
Do something with age
Remember that we do not invent our own special names unless we are extending the Python language itself. That's not what we usually want to do. The special names we're given are well thought out, and Python knows exactly what to do with each one of them.
The code is actually here in this repo as well, so why not just load it?
# %load ornery.py
"""
This type of object gets along with nobody!
自 = self in Chinese, disregard errors
"""
class Ornery:
def __init__(自, name="Fred"):
自.name = name
print("A sourpuss is born!")
def __getitem__(自, key):
return "How dare you touch me with those brackets!"
def __call__(自, *args, **kwargs):
return "Don't call me at home!"
def __getattr__(自, attr):
return "I'm insulted you'd suppose I'd have {}".format(attr)
def __repr__(自):
return "Don't bother me! Go away."
def __invert__(自):
return "I can't invert, are you kidding?"
How does any of this stuff relate to mathematics?
We have seen the beginnings of data structures and algorithms (methods), working together in the form of types, with objects of those types.
In Python, some of the special names govern the behavior of mathematical operators, such as the add, subtract, multiply and divide symbols: +, -, *, /. As a programmer, you are allowed to specify what these will do when used with your own types.
class Concatter:
def __init__(self, chrs):
self.value = chrs
def __add__(self, other):
return Concatter(self.value + other.value)
def __str__(self):
return self.value
def __repr__(self):
return f"Concatter('{self.value}')"
c1 = Concatter("The cat")
c2 = Concatter(" chased")
c3 = Concatter(" the mouse!")
c1
Concatter('The cat')
c1 + c2
Concatter('The cat chased')
c1 + c2 + c3
Concatter('The cat chased the mouse!')
print(c1 + c2 + c3)
The cat chased the mouse!
The string type all by itself is already perfectly able to concatenate with other strings, by means of the plus sign operator.
The Concatter type is therefore not really needed for anything, except to show how __add__
works.
The __add__
method is fired whenever a Concatter object is followed with a plus symbol (+).
c1.__add__(c3)
Concatter('The cat the mouse!')
c1 + c3
Concatter('The cat the mouse!')
'abc'.__add__('def') # Python is using these same methods in its own types
'abcdef'
What math objects might we usefully define as a type of their own?
Polyhedrons are a good place to start, in part because they're objects in the conventional sense. They have edges, faces, corners. They have volume and surface area.
The power rule for any shape is: if you scale all the edges by S, then surface area will resize by a factor of S times S (S to the 2nd power), and volume by a factor of S times S times S (S to the 3rd power).
Let's code that:
class Polyhedron:
def __init__(self, name, cl, s, v):
self.name = name
self.control_length = cl
self.surface = s
self.volume = v
def __mul__(self, factor):
"""returns a new Polyhedron of changed dimensions, same shape"""
return Polyhedron(self.name,
self.control_length * factor,
self.surface * factor ** 2,
self.volume * factor ** 3)
def __repr__(self):
return f"Polyhedron(name='{self.name}', cl={self.control_length}, s={self.surface}, v={self.volume})"
tetrahedron = Polyhedron("Tetrahedron", 1, 4, 1)
tetrahedron * 2 # triggers __mul__
Polyhedron(name='Tetrahedron', cl=2, s=16, v=8)
tetrahedron * 3
Polyhedron(name='Tetrahedron', cl=3, s=36, v=27)