If you want to try the Twitter example, here is how to do the authentication.
pip install requests_oauthlib
>>> import requests
>>> from requests_oauthlib import OAuth1
>>> url = 'https://api.twitter.com/1.1/account/verify_credentials.json'
#replace placeholders with your keys from registering with Twitter
>>> auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET',
... 'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET')
>>> requests.get(url, auth=auth)
<Response [200]>
Let us turn to the Bible.
"And God made the beast of the earth after his kind, and cattle after their kind, and every thing that creepeth upon the earth after his kind: and God saw that it was good." -- Genesis 1:25 (King James Bible)
"God made all sorts of wild animals, livestock, and small animals, each able to produce offspring of the same kind. And God saw that it was good." -- Genesis 1:25 (New Living Translation)
OOP is a programming paradigm that lets us organize/create data according its kind.
Lets us think about programming in a very natural and intuitive way.
For example, all lists can have objects appended to them.
mylist = [1]
mylist.append(2)
print(mylist)
[1, 2]
But we can't append to an object of type integer.
That would be behaviour that is not according to its kind. God says that is not good.
x = 2
x.append(3)
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-33-7a1f00d403a2> in <module>() 1 x = 2 ----> 2 x.append(3) AttributeError: 'int' object has no attribute 'append'
Different object types have different attributes, according to their kind.
We've gone pretty far in the course using types that Python has made for us.
But the point of today is to learn how we can define our own classes.
In Python, type $\iff$ class.
The statement class
is used to create a class.
Like a function definition, anything tabbed in belongs to the class definition.
Here is the most basic class definition possible:
class Foo:
pass
We defined a class, or a type, that we called Foo
and pretty much left the definition empty otherwise.
Now we can create objects of type Foo
.
Once you defined a class, you can create an object of that type using the class name as a function.
Doing this creates an instance of the class Foo.
For example, each person is an instance of the imaginary class "Human".
#create an instance of the type Foo
myfoo = Foo()
#create another instance of type Foo
myotherfoo = Foo()
print(f"My object is of type: {type(myfoo)}")
print(f"The id of my object is: {id(myfoo)}")
My object is of type: <class '__main__.Foo'> The id of my object is: 4578050176
print(f"The other instance's id is: {id(myotherfoo)}")
print(f"myfoo and myotherfoo are the same type: {type(myfoo) == type(myotherfoo)}")
The other instance's id is: 4578050344 myfoo and myotherfoo are the same type: True
Like an object's id
, the type
of an object can never change
Classes are also objects but let's not go down that rabbit hole. If you're interested, look up "Metaprogramming".
type(Foo)
type
So we were able to create an object but we didn't actually code anything...
Clearly there is some magic going on here.
We can often understand the world as a hierarchy of types.
More specifically, derived classes inherit the properties of base classes.
For example: dog types have all the properties of the animal class which has all the properties of the 'living-thing' class, etc.
Types in Python work the same way.
In Python, the class hierarchy looks more like this:
Whenever we create a new class, it inherits the attributes of the class object (the most generic class).
This makes object
the base class of Foo
.
The object class contains some methods that let us create new objects.
The object
class has a method called __new__()
which creates new objects of a given type and returns it.
This is what is used to create new objects.
myfoo = object.__new__(Foo)
type(myfoo)
Is (partially) equivalent to ..
myfoo = Foo()
Once an object is created, it is like a blank slate.
In order for it to be useful we have to give it some attributes.
This is done by the __init__
method. Which is short for initialization.
__init__(self, *args, **kwargs)
takes as input an instance of the class, given the name self
here and returns nothing. It just sets attributes.
e.g. It lets us do something like this:
>>> x = 5
#numerator is an attribute of integer objects
>>> s.numerator
5
>>> s.denominator
0
So what's really happening when we do myfoo = Foo()
is the base class __new__
method is called to create a new object.
Then the __init__()
method is called to set its attributes.
myfoo = object.__new__(Foo)
object.__init__(myfoo)
#now the object is fully created
Since Foo
is actually using the __init__
method from the object
class we get a very boring looking object that is the most generic type of data we can create.
If we want to customize our object, we have to re-define or override the __init__
method inside our class definition.
Python will always use the definition of a method of the derived class before the base class if it is defined.
So let's redefine our class.
class Foo:
def __init__(self, n):
self.name = n
Now when I create an object, I can pass an argument and the __init__
method I defined will be used instead of the generic object
method.
We have overridden the base method definition for our own.
myfoo = Foo("Carlos")
myfoo.name
'Carlos'
What we've done is define something known as an instance attribute.
All instances of the class Foo
will have a name
attribute (or instance variable) but their values are independent of each other.
a = Foo("Plato")
b = Foo("Socrates")
print(a.name)
print(b.name)
Plato Socrates
a
and b
are both instances of the class Foo so they both have a name but their name
attributes are different.
We can also define data that is common to all instances of a class. These are known as class attributes.
Class attributes are defined outside any function definitions of a class.
class Family():
address = "1234 Elm St."
family_members = 0
def __init__(self, n):
self.name = n
Family.family_members += 1
alice = Family("Alice")
bob = Family("Bob")
print(alice.name)
print(bob.name)
print(Family.address)
print(Family.family_members)
Alice Bob 1234 Elm St. 4
You are not limited to having attributes defined in __init__(self)
.
At any point later in your code you can create and set new attributes.
The purpose of __init__(self)
is to ensure that all instances of a class have some common data.
alice.sex = "F"
print(alice.sex)
F
print(bob.sex)
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-143-c81fa51fa28a> in <module>() ----> 1 print(bob.sex) AttributeError: 'Family' object has no attribute 'sex'
As you know, object attributes can also be executable i.e. functions.
class DNA:
def __init__(self, seq):
self.sequence = seq
def compute_GC(self):
return len([n for n in self.sequence if n in {"G", "C"}]) / len(self.sequence)
The function dir(object)
tells us the accessible attributes of an object.
d = DNA("AACCGG")
dir(d)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'compute_GC', 'sequence']
d.compute_GC()
0.6666666666666666
b = DNA("AAAAAAAA")
b.compute_GC()
0.0
There are many other functions that are defined for us that we can override to do cool things.
class WeirdNumber:
def __init__(self, n):
self.num = n
def __add__(self, other):
return self.num / other.num
w = WeirdNumber(3)
x = WeirdNumber(2)
w + x
1.5
Now we know that the +
operator calls the __add__
instance method of the two objects being added.
Instance methods always take as input an instance of the class. Hence, the mandatory self
positional argument.
self
is implicitly passed through the .
operator.
class Foo:
def print_self(self):
print(self)
f = Foo()
f.print_self()
<__main__.Foo object at 0x110ddc898>
Classes define types of objects that share some common attributes.
A class can produce instance of itself using the ClassName(*args, **args)
syntax.
When a class instance is created, the __new__()
and __init__
methods are used to create the object, and set its attributes initially.
If the class definition overrides the base class's methods then the derived class definition will be used.
import datetime as dt
class Book:
def __init__(self, author, title, loaned):
self.author = author
self.title = title
self.loaned = loaned
def borrow(self):
if not self.loaned:
self.loaned = dt.date.today()
def book_return(self, borrow_days=10, late_fee=0.25):
date_loaned = self.loaned()
self.loaned = False
if self.loaned > dt.date.today() + 10:
return (dt.date.today() - self.loaned) * late_fee
else:
return None
class Library:
def __init__(self):
self.books = {}
def add_book(self, title, author):
b = Book(author, title, False)
if title not in self.books:
self.books[title] = b
mylib = Library()
mylib.add_book("How to Travel with a Salmon", "Umberto Eco")
print(mylib.books)
{'How to Travel with a Salmon': <__main__.Book object at 0x110dcaac8>}
salmon = mylib.books["How to Travel with a Salmon"]
salmon.borrow()
salmon.loaned
datetime.date(2017, 11, 1)