Python 3home |
Everything is an Object; Objects Assigned by Reference
object: a data value of a particular type variable: a name bound to an object
When we create a new object and assign it to a name, we call it a variable. This simply means that the object can now be referred to by that name.
a = 5 # bind an int object to name a
b = 'hello' # bind a str object to name b
Here are three classic examples demonstrating that objects are bound by reference and that assignment from one variable to another is simply binding a 2nd name to the same object (i.e. it simply points to the same object -- no copy is made).
Aliasing (not copying) one object to another name:
a = ['a', 'b', 'c'] # bind a list object to a (by reference)
b = a # 'alias' the object to b as well -- 2 references
b.append('d')
print(a) # ['a', 'b', 'c', 'd']
a was not manipulated directly, but it changed. This underscores that a and b are pointing to the same object.
Passing an object by reference to a function:
def modifylist(this_list): # The object bound to a
this_list.append('d') # Modify the object bound to a
a = ['a', 'b', 'c']
modifylist(a) # Pass object bound to a by reference
print(a) # ['a', 'b', 'c', 'd']
The same dynamics at work: a is pointing at the same list object as this_list, so a change through one name is a change to the one object -- and the other name pointing to the same object will see the same changed object.
Alias an object as a container item:
a = ['a', 'b', 'c'] # list bound to a
xx = [1, 2, a] # 3rd item is reference to list bound to a
xx[2].append('d') # appending to list object referred to in list item
print(a) # ['a', 'b', 'c', 'd']
The same dynamic applied to a container item: the only difference here is that a name and a container item are pointing to the same object.
Functions are variables (objects bound to names) like any other; thus they can be renamed.
def doubleit(val):
val2 = val * 2
return val2
print(doubleit) # <function doubleit at 0x105554d90>
newfunc = doubleit
xx = newfunc(5) # 10
The output <function doubleit at 0x105554d90> is Python's way of visualizing a function (i.e. showing its printed value). The hex code refers to the function object's location in memory (this can be used for debugging to identify the individual function).
Functions are "first-class" objects, and as such can be stored in containers, or passed to other functions.
Functions can be stored in containers the same way any other object (int, float, list, dict) can be:
def doubleit(val):
val2 = val * 2
return val2
def tripleit(val):
return val * 3
funclist = [ doubleit, tripleit ]
print(funclist[0](10)) # 20
print(funclist[1](10)) # 30
These functions allow us to pass a function as an argument to enable its behavior.
We can pass a function name (or a lambda, which is also a reference to a function) to any of these built-in functions. The function controls the built-in function's behavior.
One example is the function passed to sorted():
def by_last(name):
first, last = name.split()
return last
names = ['Joe Wilson', 'Zeb Applebee', 'Abe Zimmer']
snames = sorted(names, key=by_last) # ['Zeb Applebee, 'Joe Wilson', 'Abe Zimmer']
In this example, we are passing the function by_last to sorted(). sorted() calls the function once with each item in the list as argument, and sorts that item by the return value from the function.
In the case of map() (apply a function to each item and return the result) and filter() (apply a function to each item and include it in the returned list if the function returns True), the function is required:
def make_intval(val):
return int(val)
def over9(val):
if int(val) > 99:
return True
else:
return False
x = ['1', '100', '11', '10', '110']
# apply make_intval() to each item and sort by the return value
sx = sorted(x, key=make_intval) # ['1', '10', '11', '100', '110']
# apply make_intval() to each item and store the return value in the resulting list
mx = map(make_intval, x)
print(list(mx)) # [ 1, 100, 11, 10, 110 ]
# apply over9() to each item and if the return value is True, store in resulting list
fx = filter(over9, x)
print(list(fx)) # [ '100', '110' ]
A "higher order" function is one that can be passed to another function, or returned from another function.
The charge() function takes a function as an argument, and uses it to calculate its return value:
def charge(func, val):
newval = func(val) + val
return '${}'.format(newval)
def tax_ny(val):
val2 = val * 0.085
return val2
def tax_ca(val):
val2 = val * 0.065
return val2
nyval = charge(tax_ny, 10) # pass tax_ny to charge(): $10.85
caval = charge(tax_ca, 10) # pass tax_ca to charge(): $10.65
Any function that takes a function as an argument or returns one as a return value is called a "higher order" function.
A function can be kind of a "factory" of functions.
This function returns a function as return value, after "seeding" it with a value:
def makemul(mul):
def times(startval):
return mul * startval
return times
doubler = makemul(2)
tripler = makemul(3)
print(doubler(5)) # 10
print(tripler(5)) # 15
In the two examples above, the values 2 and 3 are made part of the returning function -- "seeded" as built-in values .
A decorator accepts a function as an argument and returns a replacement function as a return value.
Python decorators return a function to replace the function being decorated -- when the original function is called in the program, Python calls the replacement. A decorator can be added to any function through the use of the @ sign and the decorator name on the line above the function.
Here's a simple example of a function that returns another function (from RealPython blog):
def my_decorator(func):
def wrapper():
print("Something is happening before func() is called.")
func()
print("Something is happening after func() is called.")
return wrapper
def whoop():
print("Wheee!")
# now the same name points to a replacement function
whoop = my_decorator(whoop)
# calling the replacement function
whoop()
This is not a decorator yet, but it shows the concept and mechanics
If we wished to use this as a Python decorator, we can simply use the @ decorator notation:
def my_decorator(func):
def wrapper():
print("Something is happening before func() is called.")
func()
print("Something is happening after func() is called.")
return wrapper
@my_decorator
def whoop():
print('Wheee!')
whoop()
# Something is happening before...
# Whee!
# Something is happening after...
The benefit here is that, rather than requiring the user to explicitly pass a function to a processing function, we can simply decorate each function to be processed and it will behave as advertised.
To allow a decorated function to accept arguments, we must accept them and pass them to the decorated function.
Here's a decorator function that adds integer 1 to the return value of a decorated function:
def addone(oldfunc):
def newfunc(*args, **kwargs):
retvals = oldfunc(*args, **kwargs)
retvals = retvals + 1
return retvals
return newfunc
@addone
def sumtwo(val1, val2):
return val1 + val2
y = sumtwo(5, 10)
print(y) # 16
Now look closely at def newfunc(*args, **kwargs): *args in a function definition means that all positional arguments passed to it will be collected into a tuple called args. **kwargs in a function definition means that all keyword arguments passed to it will be collected into a dictionary called kwargs. (The * and ** are not part of the variable names; they are notations that allow the arguments to be passed into the tuple and dict.) Then on the next line, look at return oldfunc(*args **kwargs) + 1 *args in a function call means that the tuple args will be passed as positional arguments (i.e., the reverse of what happened above) **kwargs in a function call means that the dict kwargs will be passed as keyword arguments (i.e., the reverse of what happened above) This means that if we wanted to decorate a function that takes different arguments, *args and **kwargs would faithfully pass on those arguments as well.
Here's another example, adapted from the Jeff Knupp blog:
def currency(f): # decorator function
def wrapper(*args, **kwargs):
retvals = f(*args, **kwargs)
return '$' + str(retvals)
return wrapper
@currency
def price_with_tax(price, tax_rate_percentage):
"""Return the price with *tax_rate_percentage* applied.
*tax_rate_percentage* is the tax rate expressed as a float, like
"7.0" for a 7% tax rate."""
return price * (1 + (tax_rate_percentage * .01))
print(price_with_tax(50, .10)) # $50.05
In this example, *args and **kwargs represent "unlimited positional arguments" and "unlimited keyword arguments". This is done to allow the flexibility to decorate any function (as it will match any function argument signature).