Functions

Let's declare a function


In [ ]:
def spam():       # Functions are declared with the 'def' keyword, its name, parrentheses and a colon
    print "spam"  # Remeber to use indentation!

spam()  # Functions are executed with its name followed by parentheses

Let's declare a function with arguments


In [ ]:
def eggs(arg1):         # Functions arguments are declared inside the parentheses
    print "eggs", arg1

eggs("eggssss")  # Function calls specify arguments inside parentheses

In [ ]:
def func(arg1, arg2, arg3):         # There is no limit of arguments
    print "func", arg1, arg2, arg3

func("spam", "eggs", "fooo")

In [ ]:
print func("spam", "eggs", "fooo")  # By default functions return None

In [ ]:
def my_sum(arg1, arg2):
    return arg1 + arg2    # Use the return keyword to output any result

print my_sum(3, 5)

In [ ]:
print my_sum(3.333, 5)
print my_sum("spam", "eggs")  # Given that Python is a dynamic language we can reuse the same method

Let's declare a function with arguments and default values


In [ ]:
def my_pow(arg1, arg2=2):  # It is possible to define deault values for the arguments, always after arguments without default values
    return arg1 ** arg2

print my_pow(3)

In [ ]:
def my_func(arg1, arg2=2, arg3=3, arg4=4):
    return arg1 ** arg2 + arg3 ** arg4

print my_func(3, arg3=2)  # Use keyword arguments to call skip some of the arguments with default value

Let's use an arbitrary arguments list


In [ ]:
def my_func(arg1=1, arg2=2, *args):  # This arbitrary list is a (kind-off) tuple of positional arguments
    print args
    return arg1 + arg2

print my_func(2, 3)

In [ ]:
print my_func(2, 3, 5, 7)

In [ ]:
spam = (5, 7)
print my_func(2, 3, *spam)  # It is possible to unpack a tuple or list as an arbitrary list of arguments

The same applies for arbitrary keyword arguments


In [ ]:
def my_func(arg1=1, arg2=2, **kwargs):  # This arbitrary 'args' list is a (kind-off) tuple of positional arguments
    print kwargs
    return arg1 + arg2

print my_func(2, 3)

In [ ]:
print my_func(2, 3, param3=5, param4=7)

spam = {"param3": 5, "param4": 7}
print my_func(2, 3, **spam)  # It is possible to unpack a tuple or list as an arbitrary list of arguments

Functions are first classed objects


In [ ]:
def function_caller(f):
    f()

In [ ]:
def func_as_arg():
    print 'There should be one-- and preferably only one --obvious way to do it.'

In [ ]:
function_caller(func_as_arg)  # Functions can be passed as arguments

REMEMBER:

  • Functions are declared with the 'def' keyword, its name, parrentheses and a colon
    • Specify arguments inside the parentheses
    • Define arguments' default values with an equal, after arguments without def val
    • Specify arbitrary arguments or keyword arguments with *args or **kwargs
      • Actually only the asterisks matter, the name is up to you
  • Use indentation for the body of the function, typically 4 spaces per level
  • Functions are executed with its name followed by parentheses
    • Provide input arguments inside the parentheses
    • Provide keywords arguments specifying their name
  • Functions can be declared and called outside classes
  • Functions are first classed objects
    • You can pass them as arguments

Classes

Let's see how to declare custom classes


In [ ]:
class Spam:       # 'class' keyword, camel case class name and colon :
    pass

spammer = Spam()  # Class instantiation: spammer becomes an instance of Spam

print spammer

In [ ]:
class Eggs(Spam):                       # Ancestor superclasses inside parentheses for inheritance
    a_class_attr = "class_val"          # Class attributes inside the body, outside class methods. Must have value

    def __init__(self, attr_val):       # __init__ is called in the instances initialization (not constructor)
        self.attr = attr_val

    def method(self, arg1, arg2=None):  # Method declaration. Indented and receiving self (the instance)
        print "'method' of", self
        print self.attr, arg1, arg2     # Access instance attributes using self with a dot .

    def second_method(self):
        self.attr = 99.99
        self.method("FROM 2nd")         # Methos may call other methods using self with a dot .

Still easy?


In [ ]:
egger = Eggs(12.345)                    # Provide __init__ arguments in the instantiation
print egger

In [ ]:
print egger.attr                        # Retrieve instance attributes with a dot

print egger.a_class_attr                # Retrieve class attributes with a dot

In [ ]:
print Eggs.a_class_attr

egger.a_class_attr = "new value"

In [ ]:
print egger.a_class_attr
print Eggs.a_class_attr
  • Class attributes can be retrieved directly from the class
  • Instances only modify class attributes value locally

In [ ]:
print Eggs

Classes are objects too:

  • Python evaluates its declaration and instantiates a special object
  • This object is called each time a new class instance is created

In [ ]:
egger.method("value1", "value2")

egger.second_method()

print egger.method

print Eggs.method

In [ ]:
inst_method = egger.method
inst_method("valueA", "valueB")

Methods are also attributes (bounded) of classes and instances

Time to talk about new-style classes


In [ ]:
class Spam:
    def spam_method(self):
        print self.__class__  # __class__ is a special attribute containing the class of any object
        print type(self)

spammer = Spam()

In [ ]:
spammer.spam_method()

print spammer
print type(spammer)

In [ ]:
# Why type says it is an 'instance' and not a 'Spam'?
class Spam(object):           # Inherit from 'object'
    def spam_method(self):
        print self.__class__
        print type(self)

spammer = Spam()

print spammer
print type(spammer)           # This is a new-style class
  • New-style classes were introduced in Python 2.2 to unify classes and types
  • Provide unified object model with a full meta-model (more in the Advanced block)
  • Other benefits: subclass most built-in types, descriptors (slots, properties, static and class methods)...
  • By default all classes are old-style until Python 3

    • In Python 2 you have to inherit from 'object' to use new-style
    • You must avoid old-style
    • So you must inherit ALWAYS from 'object'
  • Other changes introduced Python 2.2: new, new dir() behavior, metaclasses, new MRO (also in 2.3)

  • More info: http://www.python.org/doc/newstyle/


In [ ]:
class OldStyleClass():
    pass

old_inst = OldStyleClass()
print type(old_inst)

In [ ]:
# Let's inherit from an old-style class
class NewStyleSubClass(OldStyleClass, object):  # Multiple inheritance
    pass

new_inst = NewStyleSubClass()
print type(new_inst)

Inherit from both old-style classes and 'object' to obtain new-style classes

Let's play a bit with inheritance


In [ ]:
class Spam(object):
    spam_class_attr = "spam"                             # Class attributes must have value always (you may use None...)

    def spam_method(self):
        print "spam_method", self, self.spam_class_attr
        print self.__class__


class Eggs(object):
    eggs_class_attr = "eggs"

    def eggs_method(self):
        print "eggs_method", self, self.eggs_class_attr
        print self.__class__

In [ ]:
class Fooo(Spam, Eggs):                                  # Specify a list of ancestor superclasses
    fooo_class_attr = "fooo"

    def fooo_method(self):
        self.spam_method()
        self.eggs_method()                               # Retrieve superclasses attributes as if they were yours
        print "fooo_method", self, self.fooo_class_attr
        print self.__class__

In [ ]:
foooer = Fooo()

foooer.fooo_method()

In [ ]:
foooer.spam_method()

foooer.eggs_method()  # self is ALWAYS an instance of the subclass

In [ ]:
print foooer.spam_class_attr
print foooer.eggs_class_attr
print foooer.fooo_class_attr  # We have access to all own and ancestors' attributes

In [ ]:
# Given that Python is a dynamic language...

class Spam(object):
    pass

spammer = Spam()
spammer.name = "John"
spammer.surname = "Doe"
spammer.age = 65
spammer.male = True      # ... this is legal

In [ ]:
print spammer.name
print spammer.surname
print spammer.age
print spammer.male

What about static or class methods?


In [ ]:
class Spam(object):
    def method(self, arg=None):
        print "Called 'method' with", self, arg

    @classmethod                                    # This is a decorator
    def cls_method(cls, arg=None):
        print "Called 'cls_method' with", cls, arg

    @staticmethod                                   # This is another decorator
    def st_method(arg=None):
        print "Called 'st_method' with", arg

spammer = Spam()

In [ ]:
spammer.method(10)

In [ ]:
Spam.method(spammer, 100)   # Although it works, this is not exacty the same

In [ ]:
print spammer.method
print Spam.method           # It is unbounded, not related with an instance

In [ ]:
spammer.cls_method(20)

Spam.cls_method(200)

In [ ]:
print spammer.cls_method
print Spam.cls_method     # Both are a bounded method... to the class

In [ ]:
spammer.st_method(30)

Spam.st_method(300)

In [ ]:
print spammer.st_method
print Spam.st_method     # Both are a plain standard functions

REMEMBER:

  • Classes are declared with the 'class' keyword, its name in camel case and a colon
    • Specify ancestors superclasses list between parrentheses after the class name
    • So you must inherit ALWAYS from 'object' to have new-style classes
  • Use indentation for class body declarations (attributes and methods)
  • Specify class attributes (with value) inside the class, outside any method
  • Specify methods inside the body, with indentation (method body has 2x indentation)
    • Method's first parameter is always self, the instance whose method is being called
    • Use self to access attributes and other methods of the instance
  • When inheriting, ancestors attributes and methods can be accessed transparently
  • There are no private attributes in Python
    • There is a convention to use underscore _ prefix
  • Classes definition is not closed. At any time you can add (or delete) an attribute
  • classmethod to specify class methods; bounded to the class, not its instances
    • Used to implement alternative constructors (e.g. dict.copy)
  • staticmethod to specify static methods; standard functions declared inside the class
    • Only for organisation, it is equivalent to declare the function in the class module

Modules

What is it a python module?

  • A module is a file containing Python definitions and statements.
  • Python interpreter reads the file and evaluates its definitions and statements.
  • Python does not accept dashes - in modules names

In [ ]:
print "'__name__' value:", __name__
  • The file name is the module name with the suffix .py appended.
  • Global variable 'name' contains the name of current module.
  • Functions and classes also have a variable containing their module name

In [ ]:
def func():
    print "Called func in", __name__

In [ ]:
print "'func.__module__' value:", func.__module__

In [ ]:
!cat my_modules.py

In [ ]:
!python my_modules.py

In [ ]:
import my_modules

The module name depends on how the module is being evaluated (imported or executed)

Use if name == "main": to detect when a module (script) is imported or executed


In [ ]:
# What will it happen if we import the module again?
import my_modules

In [ ]:
### All code is evaluated (executed) only once the first time it is imported

In [ ]:
func()
my_modules.func()

In [ ]:
from my_modules import func
func()

In [ ]:
func()

In [ ]:
!rm -rf basic_tmp
!mkdir basic_tmp
!echo 'print "This is the __init__.py", __name__\n' > basic_tmp/__init__.py
!cp my_modules.py basic_tmp

In [ ]:
!python -c "import basic_tmp.my_modules"

Packages are folders with a init.py file

  • This init.py is also evaluated, so it may contain code
  • The init.py is actually the package (check its module name)
  • The module name depends on the packages path

In [ ]:
!python -c "from basic_tmp.my_modules import func;func();print my_modules"

In [ ]:
!python -c "from basic_tmp.my_modules import func as the_module;the_module();print the_module.__name__"

LESSONS LEARNT:

  • Modules are objects too, and their variables, functions and classes are their attributes
  • Modules can be imported in different ways:
    • import packages.path.to.module.module_name
    • from packages.path.to.module import module_name_1, module_name_2
    • from packages.path.to.module import (module_name_1, module_name_2,
                                       module_name_3, module_name_4)
    • from packages.path.to.module import module_name as new_module_name
      • You are binding the module to another name, like you do with lists or strings
  • Modules is indepent on how you call (bind) them when importing

In [ ]:
!rm -rf basic_tmp
!mkdir basic_tmp
!echo 'print "This is the __init__.py", __name__\n' > basic_tmp/__init__.py
!cp my_modules.py basic_tmp
!echo 'from my_modules import func\n' > basic_tmp/__init__.py

In [ ]:
!python -c "from basic_tmp import func;func()"

LESSONS LEARNT:

  • Importing a package does not import its whole content
  • init.py is used for package initialization and for exposing desired content
  • Avoids that 3rd parties using your code have to know its internal structure

It is possible to import all contents of a package or module

  • from packages.path.to.module import *
  • It is inefficient if you don't need everything
  • all is used to define what will be imported with the asterisk *