Last Time¶
- Dunder methods
- The
Python
Data Model
Today¶
- Class methods, static methods, instance methods
- Modules and packages
If we have time...
- "Privacy" in
Python
- More details on Polymorphism
Building out our class: instances and classmethods¶
At this point, you should feel comfortable with classes, special methods, and the python
data model.
We will take a short excursion to enhance our classes using classmethods
. We will also see staticmethods
and regular instance
methods.
A Favorite Example¶
class ComplexClass():
def __init__(self, real, imaginary):
self.real = real
self.imaginary = imaginary
@classmethod
def make_complex(cls, real, imaginary):
return cls(real, imaginary)
def __repr__(self):
class_name = type(self).__name__
return "%s(real=%r, imaginary=%r)" % (class_name, self.real, self.imaginary)
def __eq__(self, other):
return (self.real == other.real) and (self.imaginary == other.imaginary)
c1 = ComplexClass(1,2)
c1
make_complex
is a class method. See how its signature is different above. It is a factory to produce instances.
class ComplexClass():
def __init__(self, real, imaginary):
self.real = real
self.imaginary = imaginary
@classmethod
def make_complex(cls, real, imaginary):
return cls(real, imaginary)
def __repr__(self):
class_name = type(self).__name__
return "%s(real=%r, imaginary=%r)" % (class_name, self.real, self.imaginary)
def __eq__(self, other):
return (self.real == other.real) and (self.imaginary == other.imaginary)
c2 = ComplexClass.make_complex(1,2)
c2
c1 == c2
The take-away¶
- A
classmethod
has access to the actual class, but not the instance of the class. - Use it to provide alternative ways of constructing an object of your class.
- The original client only needed to create complex numbers from the real and imaginary parts.
- A new client needs to create complex numbers from the polar form (radius and angle).
- Provide a classmethod that allows users to construct complex number objects from the polar form!
import numpy as np
class ComplexClass():
def __init__(self, real, imaginary):
self.real = real
self.imaginary = imaginary
@classmethod
def from_polar(cls, r, theta):
real = r * np.cos(theta)
imaginary = r * np.sin(theta)
return cls(real, imaginary)
def __repr__(self):
class_name = type(self).__name__
return "%s(real=%r, imaginary=%r)" % (class_name, self.real, self.imaginary)
def __eq__(self, other):
return (self.real == other.real) and (self.imaginary == other.imaginary)
c1 = ComplexClass(1,2)
c2 = ComplexClass.from_polar(1.0, 0.75)
print(c1.real, c1.imaginary)
print(c2.real, c2.imaginary)
Breakout Room¶
- Recall your favorite number. The speaker for this room will be the person with the largest favorite number.
- Think about some situations where you might need to create a classmethod.
- Think of any class that you want to create.
- Think about some different scenarios that may require different constructors.
Static Methods, Class Methods, Instance Methods¶
What's really going on under the hood here?
# From fluent python
class Demo():
@classmethod
def class_method(*args): # Class methods do not have to return an instance of the class
return args
@staticmethod
def static_method(*args): # This is just a regular function
return args
def instance_method(*args): # This is a true blue instance method
return args
sm = Demo.static_method(1,2)
print(type(sm))
sm
# From fluent python
class Demo():
@classmethod
def class_method(*args): # Class methods do not have to return an instance of the class
return args
@staticmethod
def static_method(*args): # This is just a regular function
return args
def instance_method(*args): # This is a true blue instance method
return args
cm = Demo.class_method(1,2)
print(type(cm))
cm
ademo = Demo()
Demo.instance_method(ademo, 1,2)
ademo.instance_method(1,2)
cm = ademo.class_method(1,2)
sm = ademo.static_method(1,2)
im = ademo.instance_method(1,2)
print(type(cm), type(sm), type(im))
cm
sm
im
Class variables and instance variables¶
class Demo2():
classvar = 1
ademo2 = Demo2()
print(Demo2.classvar, ademo2.classvar)
ademo2.classvar = 2 # Different from the classvar above
print(Demo2.classvar, ademo2.classvar)
Class variables are shared between all instances of the class.
Practical Comments on class methods¶
- Act as a factory to produce objects that are configured the way you want.
- This can make life easier: Instead of defining a new object every time, just get a pre-defined one.
- See Factory Method Design Pattern
- Pre-define commonly used objects.
- These objects all still use the same constructor.
Practical Comments on static methods¶
- Python doesn't need to instantiate a bound method for each object.
- This saves on cost.
- Might improve code readability.
- You know right away that the method doesn't depend on the state.
class A():
def __init__(self, x):
self.x = x
def doit(self, y):
return self.x + y
dir
for classes contains the names of its attributes and recursively of the attributes of its bases.
dir(A)
vars
on an object gets the contents of a special attribute called __dict__
.
vars(A)
Let's make an instance of A
.
a = A(5)
dir
again:
dir(a)
vars
again:
vars(a)
There is some kind of a table implementation for Python objects (it's written in C
).
This implementation allows us to look for attributes and methods, and if not found look elsewhere.
The exact details are complex, using descriptors and other lookups, and we'll tackle them in more detail later (hopefully).
But currently it suffices for us to know that lookup first happens in the instance table, followed by the class table (methods) and if not there somewhere up in the inheritance hierarchy.
A.__class__, a.__class__
Creating Packages from Python
Code¶
Now that you know how to write classes, it's time to figure out how to combine them into a package.
A package is a collection of Python modules.
Let's start by reviewing how to import a Python module.
Module Recap¶
- Import a module with the
import
statementimport mymod
Here's how Python
searches for a module once it's imported:
- The interpreter searches for a built-in module with that name.
- If no built-in module exists with that name, then the interpreter searches for the name in the list of directories in the
sys.path
variable. - If the requested name can't be found, an
ImportError
exception is thrown.
The Many Ways to Import¶
Suppose your module contains some methods called myf1
, myf2
, and so on.
There are a variety of ways to import the module and its methods. Here are a few along with their uses:
import mymod as new_name # rename mymod
new_name.myf1() # access myf1() method in mymod via new_name
from mymod import myf1 # Just import myf1() from mymod
myf1() # Direct use
from mymod import myf1 as new_f # Import myf1 from mymod and rename
new_f() # Direct use
from mymod import * # Make all methods and objects in mymod directly accessible!
myf2() # (Except for objects with leading underscores)
Comments on Importing¶
- Generally a very bad idea to do
from mymod import *
. Can lead to name clashes! from mymod import myf1
is also dangerous if you're not careful.
- Recommendation: Just do
import mymod
orimport mymod as new_name
unless you have a very good reason for doing otherwise.
Where to put the import
statements in a module? A common convention is:
- After the module's documentation.
What order to import libraries?
- First import standard library modules.
- Then import third-party library modules.
- Then import your own modules.
Modules and Packages¶
- For larger projects, you will have multiple modules.
- A collection of multiple modules is called a package.
Why multiple modules?¶
Having multiple modules helps with code organization.
physics_code/
__init__.py
preprocessing/ # This is a (sub)package
__init__.py
parse_xml_inputs.py # This is a module
parse_txt_inputs.py # and this is a module
...
solvers/
__init__.py
time_integrators.py
discretization.py
linear_solvers.py
...
postprocessing/
__init__.py
write_hdf5.py
write_txt.py
...
stat_utils/
__init__.py
...
viz/
__init__.py
line_plots.py
...
tests/
...
What is __init__.py
?¶
- Used for package initialization-time actions.
- Generates a module namespace for a directory.
- In Python 3.3+, empty
__init__.py
is not required: Packages - Still use for package initialization
- In Python 3.3+, empty
- Implements the
from *
behavior.- This is done using
__all__
lists. - e.g. include the line
__all__ = ["mod1", "mod2", ..., "modN"]
- This is done using
More Practical Comments on __init__.py
¶
- Empty
__init__.py
files no longer necessary
- They help prevent directories with common names from hiding true modules
- The first time
Python
imports through a directory, it runs the code in__init__.py
.
Working With Packages¶
- Once you have your directory structure set up (with the
__init_.py
files), you are ready to use the package.
dir\
driver.py
package\
__init__.py
subdir1\
__init__.py
s1mod1.py
s1mod2.py
subdir2\
__init__.py
s2mod1.py
s2mod2.py
# driver.py: can make use of the package by simple imports.
import package.subdir1.s1mod1 as s1mod1
s1mod1.method()
...
Let's talk about __name__
¶
- You may have seen the code snippet:
if __name__ == "__main__": # Do some things
- The variable
__name__
is created whenever a.py
file is run and is set to the string"__main__"
. - However, when a module is imported,
__name__
is set to the module's name. - Hence, if the module is not being run as a
Python
script, theif
statement will not be executed.
Additional Information¶
As with most things Python
, you can simply consult the excellent documentation: Python
Modules.
- Absolute vs. Relative imports
- Compiled
Python
files
Illustrative Example¶
Consider the following directory structure:
dir1\
__init__.py
dir2/
__init__.py
mymod.py
Here is what is in each file:
# dir1/__init__.py
print("Initializing dir1/")
# dir2/__init__.py
print("Initializing dir2/")
# dir2/mymod.py
my_name = "David"
Outputs¶
- If I work from the command line in the container of
dir1
, I can see various things happen.
>>> import dir1.dir2.mymod
Initializing dir1/
Initializing dir2/
>>> dir1.dir2.mymod.my_name
'David'
>>> import dir1.dir2.mymod as mod
>>> mod.my_name
'David'
Some Practical Comments¶
- It's annoying to write all those paths manually.
- You can make the functions and classes available in
__init__.py
and then use the direct import statement
Consider the directory structure:
example.py
package\
__init__.py
mod.py
Now the import can be achieved with:
# __init__.py
from .mod import myclass
from .mod import myfunc
# mod.py
class myclass():
def __init__(self):
...
...
def myfunc():
...
# example.py
import package
C = package.myclass()
Breakout Room¶
- Think of a package that you may want to create.
- Think of the subpackages and modules that you will include. How will the package be organized?
- What would you like to include in
__init__.py
?
Creating and Distributing Packages¶
At this point, you know how to create packages in Python
and the basics of how things fit together.
Ultimately, you want to be able to distribute your package to other people.
There are a number of ways to do this...brace yourself.
- PyScaffold
- It sets up the entire infrastructure for you.
- That's great, but you might not understand all the details.
- Packaging and Distributing Projects
- Fantastic documentation covering the meaning of everything.
- How To Package Your Python Code
- Excellent tutorial on packaging.
- Tensor Basis Neural Network
- Easy-to-follow real world example.
- How to package a python application to make it pip-installable
- Bare-bones example.
- Submitting a Python package with GitHub and PyPI
- Slightly out-of-date, but still useful.
There Are So Many Options!¶
- As you can see, you have many options on how to set up and distribute your package.
- I will give you broad freedom in how you do this, but your project must be easily installable.
What does "easiliy installable" mean?¶
- Using
pip
is great! This would be the easiest for the user. - You are also welcome to host your project on
GitHub
and have the user manually install and test withsetup.py
. - Either way, your package should be installable and the user should be able to run the tests.
Privacy in Python¶
Python
does not have private names- It can "localize" some names in classes
- This localization is handled by "name mangling"
- Name mangling does not prevent access by code outside the class!
- Name mangling is intended to help avoid namespace collisions
Therefore, we say that Python
has the notion of pseudoprivate names.
Pseudoprivacy and Name Mangling¶
Names inside a class that begin with two underscores are expanded to include the name of the enclosing class.
For example, suppose you have a class called Universes
and a name in that class called __our_universe
.
Python
changes the name __our_universe
to _Universes__our_universe
.
Now if there is another class in the hierarchy containing an attribute name our_universe
then the two names will not clash.
If you know the name of the enclosing class, you can still access the "private" attributes.
class Universes():
__our_universe = "Big home"
class Galaxies(Universes):
__our_universe = "home"
our_galaxy = "Milky Way"
dir(Universes)
['_Universes__our_universe', '__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__']
vars(Universes)
mappingproxy({'__module__': '__main__', '_Universes__our_universe': 'Big home', '__dict__': <attribute '__dict__' of 'Universes' objects>, '__weakref__': <attribute '__weakref__' of 'Universes' objects>, '__doc__': None})
dir(Galaxies)
['_Galaxies__our_universe', '_Universes__our_universe', '__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__', 'our_galaxy']
vars(Galaxies)
mappingproxy({'__module__': '__main__', '_Galaxies__our_universe': 'home', 'our_galaxy': 'Milky Way', '__doc__': None})
U = Universes()
G = Galaxies()
U._Universes__our_universe
'Big home'
G._Galaxies__our_universe
'home'
By the way...¶
This simple example allows us to say something about the Python method resolution order.
We've been dancing around this idea: This is the order that Python looks for a method in the hierarchy.
Let's see what this looks like for our example:
Galaxies.mro()
[__main__.Galaxies, __main__.Universes, object]
Some details: Private Variables.
A note on single underscores:
a name prefixed with an underscore (e.g. _spam) should be treated as a non-public part of the API (whether it is a function, a method or a data member).