# 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

### 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")

``````

### 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)

``````

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)

``````

### 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

``````

### 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 *