Function Scope Review

Remember that Python functions create a new scope, meaning the function has its own namespace to find variable names when they are mentioned within the function. We can check for local variables and global variables with the locals() and globals() functions.

	
	s = 'Global Variable'

	def check_for_locals():
		a=5
		b="Ram"
		print("globals() :\n{}\n".format(globals()))
		print("globals()['s'] :\n{}\n".format(globals()['s']))
		print("locals() :\n{}\n".format(locals()))
		print("globals().keys() :\n{}".format(globals().keys()))
		print("locals().keys() :\n{}".format(locals().keys()))
		
	check_for_locals()

	
	
	#  Output : 
	globals() :
	{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, 
	'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 's': 'Global Variable', 
	'check_for_locals': <function check_for_locals at 0x7f10c19d2d30>}
	
	globals()['s'] :
	Global Variable

	locals() :
	{'a': 5, 'b': 'Ram'}

	globals().keys() :
	dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 's', 'check_for_locals'])
	
	locals().keys() :
	dict_keys(['a', 'b'])
	

Passing Function into other functions.

In Python everything is an object. That means functions are objects which can be assigned labels and passed into other functions.

	
	def sayHi(name="Sam"):
		return "Hello "+name

	print(sayHi())		# 	Hello Sam
	
	
	hey = sayHi
	print(hey())		# 	Hello Sam
	

Even though we deleted the name sayHi, the name greet still points to our original function object. It is important to know that functions are objects that can be passed to other objects!

	
	del sayHi	
	print(hey())		# 	Hello Sam	
	print(sayHi())		#	Traceback (most recent call last): File "<string>", line 7, in <module> NameError: name 'sayHi' is not defined
	

Nested Functions : Functions within functions

	
	def sayHi(name='Sam'):
		print('Inside sayHi() function')
		
		def greet():
			return '\tInside greet() function'
		
		def welcome():
			return "\tInside welcome() function"
		
		print(greet())
		print(welcome())
		print("Now we back inside the hello() function")
		
	
	
	sayHi()
	
	#  Output : 
		Inside sayHi() function
			Inside greet() function
			Inside welcome() function
		Now we back inside the hello() function
	

Due to scope, the greet() and welcome() function is not defined outside of the sayHi() function.

	
	print(greet()) 
	
	#  Output : 
		Traceback (most recent call last):
			File "", line 15, in 
		NameError: name 'greet' is not defined
	

Returning Functions :

	
	def sayHi(name='Sam'):
		print('Inside sayHi() function')
		
		def greet():
			return '\tInside greet() function'
		
		def welcome():
			return "\tInside welcome() function"
		
		if name=='Sam':
			return greet		# 	Without ()
		else:
			return welcome
	

In the if/else clause we are returning greet and welcome, not greet() and welcome().

This is because when we put a pair of parentheses after the function, the function gets executed; whereas if we don’t put parentheses after it, then it can be passed around and can be assigned to other variables without executing it.

	
	x = sayHi()
	print(x)
	print(x())
		
	#  Output : 
		Inside sayHi() function
		< function sayHi..greet at 0x7f3238682dc0 >	
			Inside greet() function
	

Functions as Arguments

	
	def sayHi():
		return 'Hi There!!'

	def sayHello(func):
		print('Other code would go here')
		print(func())
	
	
	sayHello(sayHi)
	
	#  Output : 
		Other code would go here
		Hi There!!
	

Decorator in Python :

Decorators can be thought of as functions which modify the functionality of another function.

Decorators are like gift wrappers.If we want to extend the behavior of a function but don’t want to modify it permanently, we can wrap a decorator on it.

	
	def new_decorator(func):

		def wrap_func():
			print("Some lines of code ...before executing the function...")

			func()

			print("Some lines of code ...after executing the function...")

		return wrap_func

	def func_needs_decorator():
		print("This function is in need of a Decorator")
	
	
	func_needs_decorator()
	
	#  Output : 
		This function is in need of a Decorator
	
	
	# Reassign func_needs_decorator
	func_needs_decorator = new_decorator(func_needs_decorator)
	
	func_needs_decorator()
	
	#  Output : 
		Some lines of code ...before executing the function...
		This function is in need of a Decorator
		Some lines of code ...after executing the function...
	

A decorator simply wrapped the function and modified its behavior. Now let's understand how we can rewrite this code using the @ symbol, which is what Python uses for Decorators:

	
	@new_decorator
	def func_needs_decorator():
		print("This function is in need of a Decorator")

	
	
	func_needs_decorator()
	
	#  Output : 
		Some lines of code ...before executing the function...
		This function is in need of a Decorator
		Some lines of code ...after executing the function...