A Crash Course in Python: Part 3#

Lambda Functions#

Syntax

lambda arguments : expression

  • An anonymous function means that a function is without a name

  • The lambda keyword is used to create anonymous functions

Python lambda properties:#

  • can have any number of arguments but only one expression, which is evaluated and returned.

  • used when objects are required

  • syntactically restricted to a single expression

  • have various uses in particular fields of programming, besides other types of expressions in functions.

Example: Lambda Function#

calc = lambda num: "Even number" if num % 2 == 0 else "Odd number"

print(calc(20))
Even number

Example 2: Lambda Function#

string = "Drexel Dragons"

# lambda returns a function object
print(lambda string: string)
<function <lambda> at 0x0000020320AA00D0>

Explanation: The lambda is not being called by the print function, but returning the function object and the memory location. So, to make the print to print the string first, we need to call the lambda so that the string will get pass the print.

Example 2: Invoking lambda return value to perform various operations#

# Returns all of the non-digit parts of a string
filter_nums = lambda s: "".join([ch for ch in s if not ch.isdigit()])
print("filter_nums():", filter_nums("Drexel101"))
filter_nums(): Drexel
# Adds an exclamation point at the end of a string
do_exclaim = lambda s: s + "!"
print("do_exclaim():", do_exclaim("I am tired"))
do_exclaim(): I am tired!

In-Class Exercise:#

Use the Lambda function to find the sum of all integers in a string

  • Input an integer n

  • Use a for loop to extract the individual integers

  • Print the sum

# Type your solution here
# # finds the sum of all integers in a string
find_sum = lambda n: sum([int(x) for x in str(n)])
print("find_sum():", find_sum(104))
find_sum(): 5

Example: Use of lambda function inside a function#

l = ["1", "2", "9", "0", "-1", "-2"]
# sort list[str] numerically using sorted() a build in function
# and custom sorting key using lambda
print("Sorted numerically:", sorted(l, key=lambda x: int(x)))
Sorted numerically: ['-2', '-1', '0', '1', '2', '9']
# filter positive even numbers
# using filter() and lambda function
print(
    "Filtered positive even numbers:",
    list(filter(lambda x: not (int(x) % 2 == 0 and int(x) > 0), l)),
)
Filtered positive even numbers: ['1', '9', '0', '-1', '-2']
# added 10 to each item after type and
# casting to int, then convert items to string again
print(
    "Operation on each item using lambda and map()",
    list(map(lambda x: str(int(x) + 10), l)),
)
Operation on each item using lambda and map() ['11', '12', '19', '10', '9', '8']

Object Oriented Concepts#

Classes#

  • Is a logical group that contains attributes and methods

  • This allows programs to be created with simple objects that are easy to read and modify

  • It says that all instances of a class have the same attributes and methods

Example use case:#

Suppose you have a bunch of rubber ducks in your store and you want to keep an inventory where each duck has some attributes (e.g., color, size, name). This is a perfect use case for a class. You can define a class that is rubber ducks, which has a set of definable attributes.

  • Classes are created by the keyword class

  • Attributes are variables that belong to a class

  • Attributes can be accessed via the (.) operator (e.g., class.attribute)

Start with defining the class#

class Rubber_Duck:  # Defines the class
    pass  # passes when called

We created a class that does nothing :)

Objects#

  • An object is a variable that has a state and behavior associated with it.

  • An object can be as simple as a integer

  • An object can be as complex as nested functions

Components of Objects#

  • State: It is represented by the attributes of an object. It also reflects the properties of an object.

  • Behavior Methods of the object, and how the object interacts with other objects

  • Identity The Unique name that is used to identify the object

Let’s build and object from the class#

obj = Rubber_Duck()
  • This created an object of the type Rubber_Duck

The self class#

  • Class methods have an initial parameter which refers to itself

  • If we call myobj.method(arg1, arg2) we are actually calling myobj.method(myobj, arg1, arg2)

  • To simplify this in classes, the self refers to the object

The __init__ method#

def __init__(self, arg):
    self.arg = arg
  • This method is called when the object is instantiated

In-Class Exercise: Creating a class and objects with attributes and instance attributes#

Task:

  • Create a Class Rubber_Duck

  • Add an attribute obj_type and set it equal to “toy”

  • Use the __init__ function to set the name

  • Instantiate two objects luke_skywalker with a name Luke Skywalker and leia_Organa with a name Leia Organa

# Type your solution here
class Rubber_Duck:  # Defines the class

    # Class attribute
    obj_type = "toy"

    # Instance attribute
    def __init__(self, name):
        self.name = name


# Object instantiation
luke_skywalker = Rubber_Duck("Luke Skywalker")
leia_organa = Rubber_Duck("Leia Organa")
Test#
# Accessing class attributes
print(f"Luke Skywalker is a {luke_skywalker.__class__.obj_type}")
print(f"Leia Organa is a {leia_organa.__class__.obj_type}")

# Accessing instance attributes
print(f"My name is {luke_skywalker.name}")
print(f"My name is {leia_organa.name}")
Luke Skywalker is a toy
Leia Organa is a toy
My name is Luke Skywalker
My name is Leia Organa
Adding a Method#
  • You can add methods within the class

Task:

  • Add a method to the class Rubber_Duck that completes the line f"My name is {leia_organa.name}" when class.speak is called

# Your solution goes here
class Rubber_Duck:  # Defines the class

    # Class attribute
    obj_type = "toy"

    # Instance attribute
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"My name is {self.name}")


# Object instantiation
luke_skywalker = Rubber_Duck("Luke Skywalker")
leia_organa = Rubber_Duck("Leia Organa")
Test#
luke_skywalker.speak()
leia_organa.speak()
My name is Luke Skywalker
My name is Leia Organa

Inheritance#

  • Inheritance allows one class to inherit properties from another class

  • Usually referred to as child and parent classes

  • It allows you to better represent real-world relationships

  • Code can become much more reusable

  • It is transitive meaning that if Class B inherits from Class A than subclasses of Class B would also inherit from Class A

Inheritance Example#

Syntax

Class BaseClass: {Body} Class DerivedClass(BaseClass): {Body}

Step 1: Create a parent class with a method#
  • Create a parent class named Person that defines a name and age

  • Add a Class method that prints the name and title

# Your solution goes here
class Person:

    # Constructor
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # To check if this person is an employee
    def Display(self):
        print(self.name, self.age)
Testing#
jedi = Person("Darth Vader", 56)
jedi.Display()
Darth Vader 56

Step 2: Creating a Child Class;#

  • Create a child class of Person, Jedi that has a method Print that prints Jedi's use the force

# Your solution goes here
class Jedi(Person):
    def Print(self):
        print("Jedi's use the force")
Testing#
# instantiate the parent class
jedi_info = Jedi("Darth Vader", 56)

# calls the parent class
jedi_info.Display()

# calls the child class
jedi_info.Print()
Darth Vader 56
Jedi's use the force
Step 3: Adding Inheritance#
  • Starting with the base Class Person add a method to getName that returns the name

  • Add a method isAlliance that establishes if the person is part of the Rebel Alliance, the default should be False

  • Add a inherited child class Alliance that changes isAlliance to True

# Your solution goes here
class Person:

    # Constructor
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # Function that gets the name
    def getName(self):
        return self.name

    # Function that returns if the person is part of the alliance
    def isAlliance(self):
        return False


# Inherited child class
class Alliance(Person):

    # This will change the isAlliance class to True
    def isAlliance(self):
        return True
Test#
darth_vader = Person("Darth Vader", 56)
print(darth_vader.getName(), darth_vader.isAlliance())

luke_skywalker = Alliance("Luke Skywalker", 21)
print(luke_skywalker.getName(), luke_skywalker.isAlliance())
Darth Vader False
Luke Skywalker True

Exercise: Classes with Methods for Strings#

When you build a class there are a bunch of built-in magic methods methods. These can be used to do simple operations

In this exercise we are going to use the methods __str__ and __repr__

__str__ - To get called by built-int str() method to return a string representation of a type.

__repr__ - To get called by built-int repr() method to return a machine readable representation of a type.

Tasks#

  1. Build a class called Person that records the first name first_name, last name last_name, and age age

  2. Add built in methods that returns a __str__ as an f-string. It should read “{First Name} {Last Name} is {age}”

  3. Add built in methods that returns a __repr__ as an f-string. It should read “{First Name} {Last Name} is very old, they are {age}”

  4. Try this with a person named “Luke” “Skywalker” age “80”

  5. Since you added the __str__ and __repr__ functions the object can act as a string. Try this by printing the object using an f-string.

  6. You can print the machine readable version using the following syntax `f”{object!r}

# Type your code here
class Person:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self.age = age

    def __str__(self):
        return f"{self.first_name} {self.last_name} is {self.age}."

    def __repr__(self):
        return f"{self.first_name} {self.last_name} is very old, they are {self.age}"

new_person = Person("Luke", "Skywalker", "80")
f"{new_person}"
'Luke Skywalker is 80.'
f"{new_person!r}"
'Luke Skywalker is very old, they are 80'

*args and **kwargs#

What is Python *args ?#

The special syntax *args in function definitions in python is used to pass a variable number of arguments to a function. It is used to pass a non-key worded, variable-length argument list.

  • The syntax is to use the symbol * to take in a variable number of arguments; by convention, it is often used with the word args.

  • What *args allows you to do is take in more arguments than the number of formal arguments that you previously defined. With *args, any number of extra arguments can be tacked on to your current formal parameters (including zero extra arguments)

  • For example, we want to make a multiply function that takes any number of arguments and is able to multiply them all together. It can be done using *args.

  • Using the _, the variable that we associate with the _ becomes an iterable meaning you can do things like iterate over it, run some higher-order functions such as map and filter, etc.

Example: Simple *args Example#

def myFun(*args):
    for arg in args:
        print(arg)


myFun("Hello", "Welcome", "to", "Drexel")
Hello
Welcome
to
Drexel

Example of *args with First Extra Argument#

def myFun(arg1, *args):
    print("First argument :", arg1)
    for arg in args:
        print("Next argument through *args :", arg)


myFun("Hello", "Welcome", "to", "Drexel")
First argument :
 Hello
Next argument through *args : Welcome
Next argument through *args : to
Next argument through *args : Drexel

What is Python **kwargs?#

The special syntax **kwargs in function definitions in python is used to pass a keyworded, variable-length argument list. We use the name kwargs with the double star. The reason is that the double star allows us to pass through keyword arguments (and any number of them).

  • A keyword argument is where you provide a name to the variable as you pass it into the function.

  • One can think of the kwargs as being a dictionary that maps each keyword to the value that we pass alongside it. That is why when we iterate over the kwargs there doesn’t seem to be any order in which they were printed out.

Example using **kwargs#

def myFun(**kwargs):
    for key, value in kwargs.items():
        print(f"{key} {value}")


# Driver code
myFun(first="Welcome", mid="to", last="Drexel")
first Welcome
mid to
last Drexel

Example using *args and **kwargs#

def myFun(arg1, arg2, arg3):
    print("arg1:", arg1)
    print("arg2:", arg2)
    print("arg3:", arg3)


# Now we can use *args or **kwargs to
# pass arguments to this function :
print("Version with *args")
args = ("Welcome", "to", "Drexel")
myFun(*args)
Version with *args
arg1: Welcome
arg2: to
arg3: Drexel
print("Version with **kwargs")
kwargs = {"arg1": "Welcome", "arg2": "to", "arg3": "Drexel"}
myFun(**kwargs)
Version with **kwargs
arg1: Welcome
arg2: to
arg3: Drexel
def myFun(*args, **kwargs):
    print("args: ", args)
    print("kwargs: ", kwargs)


# Now we can use both *args ,**kwargs
# to pass arguments to this function :
myFun("Welcome", "to", "Drexel", first="Come", mid="to", last="Drexel")
args:  ('Welcome', 'to', 'Drexel')
kwargs:  {'first': 'Come', 'mid': 'to', 'last': 'Drexel'}

Example: Using *args and **kwargs to set values of object#

class car:  # defining car class
    def __init__(self, *args):  # args receives unlimited no. of arguments as an array
        self.speed = args[0]  # access args index like array does
        self.color = args[1]


# creating objects of car class

audi = car(200, "red")
bmw = car(250, "black")
mb = car(190, "white")

print(audi.color)
print(bmw.speed)
red
250
class car:  # defining car class
    def __init__(
        self, **kwargs
    ):  # args receives unlimited no. of arguments as an array
        self.speed = kwargs["s"]  # access args index like array does
        self.color = kwargs["c"]


# creating objects of car class

audi = car(s=200, c="red")
bmw = car(s=250, c="black")
mb = car(s=190, c="white")

print(audi.color)
print(bmw.speed)
red
250

Decorators#

Decorators a very powerful and useful tool in Python since it allows programmers to modify the behavior of a function or class. Decorators allow us to wrap another function in order to extend the behavior of the wrapped function, without permanently modifying it.

Example: Recall, Using Functions as Objects#

# Python program to illustrate functions
# can be treated as objects
def shout(text):
    return text.upper()
 
print(shout('Hello'))
HELLO
yell = shout
 
print(yell('Hello'))
HELLO

Example: Passing a Function as an Argument#

# Python program to illustrate functions
# can be passed as arguments to other functions
def shout(text):
	return text.upper()

def whisper(text):
	return text.lower()

def greet(func):
	# storing the function in a variable
	greeting = func("""Hi, I am created by a function passed as an argument.""")
	print (greeting)

greet(shout)
greet(whisper)
HI, I AM CREATED BY A FUNCTION PASSED AS AN ARGUMENT.
hi, i am created by a function passed as an argument.

Example: Returning a Function from Another Function#

# Python program to illustrate functions
# Functions can return another function
 
def create_adder(x):
    def adder(y):
        return x+y
 
    return adder
 
add_15 = create_adder(15)
 
print(add_15(10))
25

In the above example, we have created a function inside of another function and then have returned the function created inside

The above three examples depict the important concepts that are needed to understand decorators. After going through them let us now dive deep into decorators

Syntax for Decorators#

@gfg_decorator
def hello_decorator():
    print("Gfg")

'''Above code is equivalent to -

def hello_decorator():
    print("Gfg")

hello_decorator = gfg_decorator(hello_decorator)'''

In the above code, gfg_decorator is a callable function, that will add some code on the top of some another callable function, hello_decorator function and return the wrapper function

Example of How Decorators Modify Behavior#

# defining a decorator
def hello_decorator(func):

	# inner1 is a Wrapper function in
	# which the argument is called
	
	# inner function can access the outer local
	# functions like in this case "func"
	def inner1():
		print("Hello, this is before function execution")

		# calling the actual function now
		# inside the wrapper function.
		func()

		print("This is after function execution")
		
	return inner1
# defining a function, to be called inside wrapper
def function_to_be_used():
	print("This is inside the function !!")
# passing 'function_to_be_used' inside the
# decorator to control its behavior
function_to_be_used = hello_decorator(function_to_be_used)
# calling the function
function_to_be_used()
Hello, this is before function execution
This is inside the function !!
This is after function execution

Let’s see the behavior of the above code and how it runs step by step when the “function_to_be_used” is called.

Decorator Example with Returns#

def hello_decorator(func):
    def inner1(*args, **kwargs):

        print("before Execution")

        # getting the returned value
        returned_value = func(*args, **kwargs)
        print("after Execution")

        # returning the value to the original frame
        return returned_value

    return inner1


# adding decorator to the function
@hello_decorator
def sum_two_numbers(a, b):
    print("Inside the function")
    return a + b


a, b = 1, 2

# getting the value through return of the function
print("Sum =", sum_two_numbers(a, b))
before Execution
Inside the function
after Execution
Sum = 3