Showing posts with label Python Classes. Show all posts
Showing posts with label Python Classes. Show all posts

Thursday, February 29, 2024

PYTHON CLASSES

Introduction

  • THE_INIT_() METHOD
  • CLASSES
  • ACCESSING ATTRIBUTES
  • CLASSES AND INSTANCES
  • INHERITANCE
  • OVERRIDING METHODS FROM THE PARENT CLASS
  • INSTANCES AS ATTRIBUTES
  • IMPORTING CLASSES
Best software development strategies include object-oriented programming. Object-oriented programming involves building classes that reflect real-world objects and then generating objects from them. Write a class to indicate the generic traits all objects in that category can have.

Python classes are created with the class statement. Classes, which process and use pre-built object types, merely apply and expand Python principles. Classes build and manage new objects and enable inheritance, which allows for more code customization and reuse than we've seen before. Classes in Python make codes much easier to understand and organize.

We may provide each class object with any specific traits once it has a general behavior. We'll be amazed at how well object-oriented programming simulates reality.

OOP in Python is optional and does not need classes. Simple top-level script code or functions may do a lot of work. Strategic users, who create products over time, are more interested in classes than tactical users, who have less time. Successfully utilizing classes requires planning ahead.

Instantiation involves constructing an object from a class instance. Classes and instances will be written and built in this chapter. We'll define instance data and actions. We will also construct classes that enhance existing classes, making code interchange easier. Classes from other programmers will be imported into our software files and saved in modules.

Learning OOP lets us see the world like a programmer. This lets us comprehend our code holistically, not simply line by line, by exploring its ideas and notions. Understanding the foundations fosters rational thinking and helps us develop programs that can handle many difficulties.

Real-World Example of Python Classes Usage

To understand the Python classes much better way let’s take an example that we are building a software application that manages a library’s inventory and leading system. To make our code streamline and organize our data effectively, we decided to use Python classes.

We start by defining a ‘Book’ class to represent each book in the library. This class could have methods for adding new books to the inventory, removing books that are no longer available, and searching for books by title, author, or genre.

To keep track of library members and their borrowing history, we create a ‘Member’ class. This class could store information like the member’s name, contact information, and a list of books they currently have checked out. Methods like ‘check_out_book()’ and ‘return_book()’ would handle borrowing and returning books for each member.

Finally, we implement a ‘LendingSystem’ class to tie everything together. This class could manage the interactions between library patrons, books, and the library itself. It may incorporate procedures for creating library accounts, handling book returns and checkouts, and producing library usage reports.

To make our code more readable, maintainable, and extensible, we may use Python classes to group together relevant data and functions. We can create a powerful and adaptable library management system that satisfies the demands of library employees and users by using this modular approach.

It would be impractical to construct this system without classes since doing so would need a lot of extra room, time, and effort, not to mention the added complexity that makes the code difficult to comprehend and fix in the event of a mistake.

Creating and Using a Class

We can model nearly anything using classes. Let's construct a basic Python class called cat to learn Python classes. What about pet cats should we know before coding? Most cats leap and roll, and they have names and ages. Our cat class will include these two cats' names, ages, and actions (jump and roll) because they're similar to all cats. Python builds cat instances using our class as a template. After defining a class, we generate objects that represent each cat.

Creating the Cat Class

Every instance of the Cat class that is created will have the capacity to sit() and roll_over() in addition to storing a name and an age:


The first line of this code sample declares the Cat class. Classes are named in uppercase in Python. Parentheses are empty in this class declaration since it is new. The second line contains a document string that describes this class's purpose and capabilities.

The __init__() Method

A class function is called a method and follows all function rules. How we employ these approaches differs. The third line of the code defines __init__(). This function is unusual because Python performs it when a new Cat class instance is generated. This method is denoted by two underscores at the beginning and end of the init method to avoid conflicts with Python's default method names.

This code defines the __init__() function with self, name, and age arguments. Add self to the method definition first. A self-argument is automatically sent to the __init__() function when Python calls it to create a Cat instance. Self is automatically given with class method calls as a reference to oneself instance. This approach lets an instance access class attributes and methods. Python executes the Cat class's __init__() method when we create a Cat instance. 

Age and name are the only traits we value. The cat() function takes name and age. It is automatically delivered and does not need to be provided individually. Thus, just name and age are valued when instantiating the Cat class. 

The __init__() function variable is self. Every class method may access self-prefixed variables, as can every class instance. self.name = name assigns the argument name to the variable name, which links to the new instance. The same process occurs when self.age = age. Examples like this provide attributes and variables.

There are two more sit() and roll() methods in the Cat class. These methods just need one parameter, self, because they don't need a name or age. Our following instances will have these methods. Rolling and sitting are possible. roll() and sit() are idle. The warning just states that the cat is sitting or rolling. This concept may be extended to many realistic situations: A computer game using this class would include methods to animate, sit, and turn a pleased cat. If that class controlled a feline robot, those methods would tell it to sit and roll over.

Why Use Classes?

Python classes organize data and methods in a reusable manner. They enable object creation with linked functions and characteristics. This helps organize code logically, reuse code, and simplify complex system administration and maintenance. Enclosing data and behavior in classes improves modularity and abstraction. These are software engineering fundamentals. Inheritance lets you create new classes from existing ones. This promotes hierarchical code and simplifies reuse. Overall, classes are crucial to Python object-oriented programming and enable scalable, manageable, and successful codebases. Classes let us mimic more real-world structures and connections. Here, two OOP concepts help:

Inheritance: Python's strong inheritance system lets classes inherit attributes and functions from parent or base classes. Code reuse and hierarchical organization are promoted by this notion. Let's look at a basic Python inheritance example to demonstrate this:

In the previous example, we create a base class named Animal with an abstract make_sound function that is not implemented and a name attribute initialization method. Next, we define the Animal subclasses Dog and Cat. Every subclass has its own make_sound method. 

When created, Dog and Cat instances inherit the Animal class name attribute. Python examines the subclass for make_sound before invoking it on these instances. Dog and Cat override make_sound, thus when the subclass method is invoked, we get different noises for each animal.

Under the composition design principle, Python classes are built by assembling existing classes or objects rather than inheriting their functionality. This encourages integrating smaller things to build complex ones, increasing flexibility and adaptability. Let's examine a simple Python composition example:

This example has Engine and Car classes. The engine is a property of the Car, however, it is not inherited from the Engine. Composition is used.

We immediately construct an Engine instance when we build a Car. Start and stop methods in the Car class transfer their functionality to Engine class methods, allowing the Car object to compose Engine behavior.

Composition is sometimes better than inheritance because it offers greater object creation freedom, increases code reuse without tight connections between classes, and simplifies code maintenance and testing.

Every application that can be broken down into objects can benefit from object-oriented programming basics like inheritance and composition. For instance, graphical user interface (GUI) systems employ widgets like buttons and labels to enhance their appearance and usefulness. With this composite, bespoke widgets may inherit features and behavior from common UI components. Custom buttons with customized fonts or labels with individual color schemes are inherited from common UI components.

Python classes are like functions and modules but have a real purpose. Data and logic are stored in them, organizing code. Like modules, classes create namespaces. Classes vary from other software units in three areas that boost object creation efficiency:

Several instances: Python calls the ability to produce numerous instances of a class many instances. Each instance may execute methods and have its own features since they are independent. This lets you construct distinct objects with different states but comparable behavior. Let's view the Python code to clarify:

This example defines the rectangle's width, height, and area(). Three Rectangle classes with distinct width and height values are instantiated: rectangle1, rectangle2, and rectangle 3.

When we use area() on an instance, Python calculates its area depending on its width and height. We can operate with several rectangles since each instance runs separately.

Customization by inheritance: In object-oriented programming, inheritance helps us extend or change pre-existing classes to fit unique needs. It inherits methods and attributes. This strategy promotes modularity, code reuse, and software architectural flexibility. Let's view the Python code to clarify:

This Animal-based class provides a generic talk() method. Next, we define the Animal-derived Dog, Cat, and Bird subclasses. This subclass overrides talk() and replaces animal noises with their own.

Using inheritance to customize subclass behavior, we may create Animal classes for different animal species. This strategy keeps a clear class hierarchy and simplifies functionality extensions.

Operator overload: Python's operator overloading enables us to change operators' behavior on custom class objects. To describe the behavior of operators like ‘+’, ‘-‘, ‘*’, ‘/’, ‘==’, ‘!=’, ‘<’, ‘>’, etc., we may write specific methods within a class. We can act on objects in a way that makes sense for that class. Let's view the Python code to clarify:

In this example, we define a two-dimensional Point class. Define custom methods like __add__, __sub__, __mul__, and __eq__ to overload addition (+), subtraction (-), multiplication (*), and equality (==).

Implementing these methods defines Point class instances' behavior when certain operators are used. This lets us use well-known operators to act on custom objects in a class-appropriate and intelligible manner.

Making an Instance from a Class

A class provides object-building instructions. The class Cat instructs Python to create instances of various cats. Each instance follows Cat class rules.

Now let’s create an instance that represents a specific cat:

We'll utilize the Cat class from the previous example. We tell Python to construct "Willie" a 6-year-old cat in the last line. This line triggers Python to invoke the Cat class's __init__() method with "Willie" and 6. The __init__() function creates a cat instance with these arguments and sets its name and age. Python automatically returns an instance of that cat from the my_cat variable. This naming convention clarifies: Lowercase names like my_cat relate to instances produced from the class, but uppercase names like Cat refer to the class.

Accessing Attributes

Dots access instance attributes. My_cat.name in the second last line retrieves the my_cat attribute name.

Python regularly employs dot notation. The Python syntax for retrieving attribute values. Python validates my_cat before naming the property. The Cat class calls self.name. The final code utilizes the age property approach. Our print statement begins with a capital letter for "willie," his name attribute, from My_cat.name.title(). The second print command converts my dog's age property value, 6, to a string using str(my_cat.age).

Calling Methods

Any method defined in the class Cat can be called using dot notation once an instance has been created. Allow our dog to sit and roll.

The dots access instance attributes. Returns my_cat's attribute name on the second final line.

Python uses dot notation often. Python attribute retrieval syntax. Python verifies my_cat before naming. Class Cat calls self.name. The final code uses the age property. The print statement starts with a capital letter for "willie," his name attribute from My_cat.name.title(). The second print command uses str(my_cat.age) to stringify my dog's age, 6.

Creating Multiple Instances

We can make as many instances from the class as we want. Let’s create another cat called your_cat”:


This circumstance creates Willie and Lucky cats. Every cat has distinct traits and can execute the same activities.

Python would create a new Cat instance even if we gave it the same name and age. We can construct as many instances of a class as needed if each instance has a unique variable name list or dictionary location.

Classes and Instances

Classes and instances in both trees may appear identical, but they are separate Python objects. Types have distinct namespaces, instance variables, and attributes. Modules are similar because they are organized. Classes are declarations, not files, and class trees' objects are automatically linked to other namespace components via lookup techniques.

Classes are blueprints for instances and differ most from instances. For instance, the Employee class defines employee traits and behavior, whereas instances represent individual workers. Classes may generate many instances without reloading modules for changed code, unlike modules.

Typically, instances include data utilized by class methods like hours worked and classes have related methods like computeSalary. An OOP approach parallels the classical data processing paradigm of records and programs. OOP classes treat "programs" like "data" records. However, inheritance in OOP enhances program customization over earlier approaches.

Working with Classes and Instances

Python classes structure code and represent real-world concepts well. When we create a class, we create a blueprint for building instances. Instantiating and interacting with class instances takes up most of our time following class definitions.

Dealing with class instances involves modifying their properties. Like variables, attributes are instance-specific and include information. We can edit these characteristics by directly accessing them or writing class methods to update them.

By creating a class, we determine the behavior and structure of objects that will be based on it. Class instances resemble blueprint-based objects. Changing instance attributes changes each object's state. We can accomplish this manually or by providing class methods to govern attribute modifications.

The Car Class

Let's establish a vehicle class. Our class will contain car type information and provide a method that summaries it:

This Python code defines a simple car class. This class's main methods are __init__ and get_descriptive_name.

Python classes employ a unique function called ‘__init__’ to initialize new objects. This Car class's "make," "model," and "year" attributes represent the car's make, model, and year. The ‘__init__’ function allocates these arguments to object attributes using the ‘self’ keyword. These characteristics must be set when creating a new ‘Car’ object since they will be preserved with it.

The ‘get_descriptive_name’ method generates a nicely structured vehicle descriptive name. It concatenates ‘year’, ‘make’, and ‘model’ into a string variable called ‘long_name’. The method returns this string. Remember that the ‘long_name’ variable is defined via string concatenation and returned, eliminating the requirement to invoke the function.

This sample shows Python object-oriented programming basics such as class definitions, object creation, and method calls. Encapsulating automotive attributes and actions in a single class promotes code structure and reusability.

Setting a Default Value for an Attribute

Usually, every class attribute should have an initial value, even 0, or an empty string. This starting value helps the __init__() procedure create default settings. It's not necessary to assign a property parameter while doing this.

Add an odometer_reading property with a constant value of 0. We'll also add read_odometer() to let us read each car's odometer:

Most class attributes should start with 0 or an empty string. This initial value aids __init__() in defining defaults. This doesn't require a property parameter.

Add an odometer_reading property with a constant 0. We'll also implement read_odometer() to read each car's odometer.

Modifying Attribute Values

We can adjust an attribute's value by incrementing, setting, or instance-changing it. Let's analyze each strategy.

Modifying an Attribute’s Value Directly

Class instances can directly modify attribute values. This involves directly modifying the attribute's value without middlemen. Here, we immediately set the odometer to 23:

In the code above, dot notation is used to access and set the car's odometer_reading attribute. Python is instructed to take the instance my_new_car, find its odometer_reading attribute, and set its value to 23.

Modifying an Attribute’s Value Through a Method

Of course! Methods that update class instances internally might be handy instead of manually accessing and altering their properties. This strategy enhances and simplifies classes by encouraging encapsulation and abstraction.

It works like this:

Attribute Update Method: We use class methods to update features like odometer_reading instead of accessing them directly. These methods usually employ fresh values as arguments to update attributes internally.

New Values Pass: To change an attribute, we call the relevant method and pass the new value. This covers attribute updating in the method and shields the caller from implementation details.

Internal Handling: The method validates and processes the attribute before changing it. This ensures that attribute updates follow method logic and are regulated.

Overall, methods to update attributes simplify class state management. By hiding attribute manipulation implementation, encourages encapsulation and simplifies class management.

Example: The preceding code has a function named update_odometer():

The only modification to Car is adding update_odometer() to the last function. This method stores a mileage value internally.Odometer reading. The second-to-last line calls update_odometer() with 23 as the mileage argument, per the method specification. Read_odometer() prints 23.

We can add work to update_odometer() whenever the odometer changes. Let's add logic to prevent odometer resets:

The update_odometer() method verifies the new odometer reading for logic before changing the property. The new mileage "mileage" is compared to the self.odometer_reading file's current mileage. The if statement changes the odometer if the new mileage is more or equal to the current mileage. As indicated in the second sentence, the odometer cannot be reset if the new distance is less than the existing mileage.

Incrementing an Attribute’s Value Through a Method

Instead of creating a new value, we may wish to increase an attribute's value by a certain amount. Imagine buying a secondhand automobile and driving 100 miles before registering it. 

Add that value to the odometer and pass this incremental amount using this method. We can now add a class method to boost the attribute's value by a certain amount. An argument indicating how much to add to the existing value usually affects the property.

Finally, increment_odometer() adds the specified miles to self.odometer_reading. The variable my_used_car creates a used car. If my_used_car has 23,500 miles on its odometer, update_odometer() is called. The last call to increment_odometer() on my_used_car adds 100 miles between purchase and registration.

Change this method to reject negative increments to prevent odometer rollbacks.

These methods provide users some flexibility over updating data like the odometer in our software. Note that app users can still modify odometer readings. Effective security needs attention to detail and fundamental measures as explained above.

Inheritance

Class development doesn't necessarily have to start from zero. We can use inheritance if the class we're developing is a modified version of one we've already created. The parent class's attributes and methods are automatically inherited by a class through inheritance. The superclass is the original class, while the subclass is the new one. A subclass can implement its properties and methods, but it inherits all of its superclasses'.

The __init__() Method for a Child Class

Before establishing a child class, Python checks all parent class properties for initialization. The child class's __init__() procedure utilizes the parent class's help. To initialize all parent class attributes before customizing the child class, the child class must explicitly invoke the parent class's __init__() function.

By creating a modified Car class called ElectricCar, we may represent an electric automobile. Electric cars have wheels, doors, and steering wheels, thus we may base our ElectricCar class on the vehicle class. This implies that electric car features like battery capacity, charging methods and motor performance will require more coding. We may utilize inheritance to exploit the Car class's features while customizing our ElectricCar class for electric autos.

Let’s make a simple version of the ElectricCar class, that does everything the Car class does:

It is crucial to declare the superclass in the same file as the subclass when establishing a Python subclass. Due to inheriting methods and attributes from the superclass, the subclass needs this organizational structure to define itself.

The ElectricCar class defines the Car-derived child class ElectricCar. The parent class name is in parenthesis after the child class name to indicate inheritance.

Initializing ElectricCar instances is done using the __init__() function. It includes any electric car-related statistics together with Car class data. This populates all Car class properties and any unique ElectricCar class characteristics when we create an ElectricCar object.

Super() is a unique Python feature that links child and parent classes. When used in a child class's __init__() method, super() calls the parent class's method. We can assure that a child class instance will inherit all parent class features and methods by completing this step.

When we use super(), we access and use the parent class's __init__() function. This method lets the child class use its parent class's capabilities and inherit its traits.

The term "super" originated from calling the parent class the superclass and the child class the subclass. The super() method shows the links between these classes, simplifying inheritance and code organization.

We aim to build an electric car using the same specifications as a normal car to test inheritance.

The variable my_new_tesla receives an ElectricCar instance. The ElectricCar class's __init__() method is invoked upon instantiation. The Car parent class's __init__() method is then called by Python.

We offered 'tesla','model S', and 2022, which are used to denote automobile year, make, and model. These parameters ensure the Car class is started appropriately, proving inheritance works. This method lets us inherit and apply the parent class's initialization logic to construct an electric car.

So far, we've proven that the electric vehicle instance functions like a car instance since it inherits all the vehicle class's attributes and methods, including the __init__() method's startup logic.

But we haven't added electric vehicle-specific features or methods. So far, we've focused on having the electric car act like a car.

Electric vehicle characteristics and operations may now be defined. With these improvements, we can appropriately display electric car attributes.

Defining Attributes and Methods for the Child Class

We can add features-specific methods and properties to a child class. A child class inherits from a parent class. We may discuss electric vehicle attributes like batteries and how to provide information about them.

For instance, we could store electric car battery capacity. We'll also use a technique that generates a thorough battery description to get size statistics. This method distinguishes electric automobiles from regular cars by introducing distinctive characteristics.

The code creates a battery_size attribute in the ElectricCar class. Initialized at 70. ElectricCar instances have this trait; Car instances do not.

The ElectricCar class has describe_battery(). This method prints electric car battery information. When we call an electric car's battery an instance of the ElectricCar class, this method provides a detailed explanation. This function retrieves and displays battery information to distinguish electric cars from standard cars.


To accurately represent electric vehicles, the ElectricCar class may be substantially modified. The ElectricCar class can have as many properties and methods as needed to correctly depict an electric car.

It's important to distinguish between electric vehicle characteristics and practices and those that apply to all cars. The automobile class should include any attribute or method that applies to any automobile, independent of the power source. This ensures that Car class users may utilize this capability, but the ElectricCar class only includes code specific to electric car traits and characteristics. By organizing the code and ensuring that each class is focused on its own objectives, this modular approach improves code clarity and maintainability.

Overriding Methods from the Parent Class

By overriding a parent class method with the same name in a subclass, we may modify its behavior. This instructs Python to utilize only the child class method and ignore the parent class method implementation.

Say the automobile class contains a fill_gas_tank() function that an all-electric automobile doesn't need. In certain cases, overriding this function in the ElectricCar class provides a better electric car-specific solution. When instances of the ElectricCar class call fill_gas_tank(), the child class's custom implementation is run instead of the parent class's method. This lets us tailor the technique to electric cars.

When inheritance is used to call fill_gas_tank() on an electric automobile instance, Python will favor the ElectricCar class implementation over the car class one. The child class can use the functionality inherited from the parent class by overriding or ignoring any methods that are superfluous or incompatible with its needs.

Inheritance allows child classes to ignore or override methods that do not match the parent class's behavior and selectively maintain and enhance inherited functionality. This function ensures that any class may use its parent class's reusable elements to recreate its intended behavior and features.

Instances as Attributes

When representing real-world stuff in code, our classes often get increasingly intricate with additional characteristics and functions. Our code files may become lengthy and tough to manage. In these cases, class components may be separated into distinct functional units.

Splitting our huge class into smaller, more specific classes can help us solve this. Our code may be broken into smaller classes, each with a subset of methods and attributes describing a certain aspect of the model. This modular approach enhances code reusability, maintainability, clarity, and structure. It simplifies complicated system administration and comprehension and improves scalability.

As we enhance the ElectricCar class, battery-related characteristics and operations may accumulate. Our code must be better structured and modularized when this happens.

To fix this, we may move battery methods and attributes from ElectricCar to Battery. Add a Battery object to the ElectricCar class's attributes.

This strategy creates a more ordered and modular architecture where each class handles a particular function. The division of duties improves code readability, maintainability, and scalability, simplifying system administration and allowing for future upgrades.


 

The given code creates a non-inherited Battery class. Self and battery_size are sent to __init__(). The battery_size argument is optional. Defaults to 70 without value. We also relocated describe_battery() to this class.

The ElectricCar class adds self.battery. Python imports the Battery class, sets its default size to 70, and creates an instance. The self.battery property holds this instance. This instantiation occurs whenever an ElectricCar instance's __init__() method is called, ensuring that every ElectricCar object has a Battery instance.

The battery property of an electric automobile allows us to access the describe_battery() function. In the code, we call “my_new_tesla.battery.describe_battery()” to explain the electric car instance's battery. Our technique lets us use the Battery class's battery functionality through the ElectricCar class's battery feature.

Python executes "my_new_tesla.battery.describe_battery()" to discover the battery property of "my_new_tesla" and use the describe_battery() function.

The above code produces similar or identical output.

By adding a function that calculates and reports the car's range based on battery size, we can simplify the Battery class and maintain a clear separation of duties. By adding this function to the Battery class, we ensure that the ElectricCar class exclusively covers electric car features. This prevents code clutter and promotes order.


The preceding code uses the Battery class's get_range() function to analyze battery capacity. The above code sets the electric car's range to 240 miles using a 70 kWh battery. Similarly, 80 kWh capacity yields 270 miles. The method prints the calculated range.

The electric car's battery property must be used to invoke this function. This approach shows that battery size determines automobile range. This method improves code organization and clarity by enclosing battery operations in the Battery class and focusing the ElectricCar class on electric car features.

Modeling Real-World Objects

When modeling complex products like electric automobiles, we encounter fascinating difficulties concerning assigning characteristics and strategies. We might inquire if an electric car's range is due to the car or the battery. Associating the Battery class with get_range() may be enough for one automobile. If we're working with a manufacturer's whole vehicle selection, moving get_range() to the ElectricCar class may be better. The model-specific range would be advertised, but battery size must be measured. Another option is to add a car_model argument while maintaining get_range() linked to the battery. The program would determine the range based on the car model and battery size.

Importing Classes

As we add functionality to our classes, our code files may grow even with careful inheritance. Python lets us store classes in modules to arrange our code. This solution follows Python's modular, clutter-free code file philosophy. Grouping classes into modules improves code efficiency and reusability. We can then import the exact classes we need into our main application, making the codebase cleaner and easier to maintain.

Importing a Single Class

We will specifically build car.py to contain the Car class. This decision conflicts with this chapter's car.py file. Replacement of the old car.py file with a module containing the Car class will cure this problem. Thus, programs that utilize this module must have an exact filename like my_car.py to avoid misunderstanding. For order and clarity, our codebase will only contain the Car class's code in car.py.

In my_car.py, we will import the Car class from the car module and create an instance. This division lets us separate code into distinct chunks for code reuse. The Car class's functionality may be used in my_car.py without importing it. Modular code is more scalable, legible, and maintainable, making complex systems easier to build. For documentation and reference, we will provide a module-level docstring in car.py.


Python imports the Car class from the car module using the code's import line. This technique lets us utilize the Car class in the current file as if it were declared directly. Importing the Car class provides us access to its properties and methods for instantiating and acting on objects. Thus, the results will match those from defining the Car class directly in the file. This technique encapsulates relevant functionality in external modules, improving code modularity and reusability.

By relocating the class to a module and importing it, we keep its functionality while keeping the main application file simple. This approach separates the class implementation into numerous modules, keeping the main application file tidy. We can speed up development by placing most of the logic in separate files. After our classes work, we may disregard such files and focus on our core program's logic. Distributing responsibilities enhances code modularity, maintainability, and structure, making modifications and debugging easier.

Storing Multiple Classes in a Module

As long as each class is related, a module can hold as many classes as needed. Battery and ElectricCar will be in car.py since they symbolize cars. This keeps our codebase organized and clear by maintaining the module's emphasis on automobiles. We maintain consistency and cohesion by merging important classes into a single module, making automobile representation functionality easier to manage and understand. This modular architecture supports code reuse and scalability, so we can add and replace automobile features.



After creating modifying or adding the new classes to the car.py class we make a new file called my_electric_car.py, it imports the ElectricCar class from the car.py class and makes an electric car:

This code has the same output as we saw earlier, even though most of the logic is hidden away in a module:

Importing Multiple Classes from a Module

Many classes can be imported into a software file. We must import the vehicle and ElectricCar classes to construct a standard and electric automobile in the same file. Since both classes offer functionality, we may construct objects of each kind and use their specific attributes and methods. Importing the necessary classes into our program file lets us effortlessly integrate and use their functionalities to achieve our goals.


The given code imports several module classes separated by commas. After importing the classes, we may create unlimited instances of each.

The aforementioned code calls an Audi S4 and an electric Tesla roadster, as seen below.

Importing an Entire Module

Another option is to import the complete module and use dot notation to access the classes. This method simplifies and increases code readability. Dot notation lets us immediately access module classes by importing the entire file, which reveals the source of each class. Each instance creation call includes the module name, preventing naming conflicts in the current file. This prevents local file names from conflicting, making code cleaner and more structured. Overall, importing modules in this manner speeds up development and improves code maintainability and clarity.

The code snippet above explains how to import the vehicle module and use it to add a standard and electric automobile:


This code imports the full automobile module in the first line. After that, we use module_name.class_name to access classes. We made another Audi s4 and my_new_tesla at my_audi. We built a Tesla Roadster.

Importing All Classes from a Module

If we need then we can import every class from a module we just need to use the following syntax:

from module_name import *

This procedure is inadvisable for various reasons. Clear and straightforward import declarations at the beginning of the file let us rapidly identify program classes. It is unclear which module classes are utilized when importing a full module and accessing dot-marked classes. In bigger codebases, ambiguity can cause misunderstanding. Additionally, this method might generate file name problems. Accidentally importing a class with the same name as another program file element might produce hard-to-fix issues. It's best to import the complete module and use module_name.class_name to import several classes. Despite not listing all classes at the front of the file, this method shows where the module is utilized throughout the application. It also reduces name conflicts when importing module classes.

Importing a Module into a Module

To keep files small and organize unrelated classes, we may distribute our classes over numerous modules. In such cases, a class in one module may depend on another class in another. We may import the needed class into the first module to address this dependence. We make the dependent class available and usable in the first module by doing so. We can better arrange our code and handle class dependencies across modules with this modular approach.

To simplify, put the Car class in one module and the Electric Car and Battery classes in another. Copy only the battery and electric car classes into a new module called "electric_car.py" to replace our previous file. This split improves code organization and class dependencies:


Class ElectricCar needs access to its base class Car. Thus, the Car class is imported into the module at the start of the code. If this import statement is missing, Python will fail to create an ElectricCar instance. Change the Car module to just contain the Car class to organize and clarify the codebase. Each module will contain one class. This approach simplifies code maintenance and reduces errors and misunderstandings when using several classes in many modules.


We can then import from each module individually so that we can build what we want. Let's see it in the demo code:


The code imports the Car and ElectricCar classes from their modules in the first and second lines. After that, we instantiate standard and electric vehicle objects. Execution demonstrates both code sets function properly.

The Python Standard Library

Each Python installation includes modules from the standard library. After learning about classes, we may use these modules from other programmers. A simple import line at the start of our Python scripts lets us access any standard library function or class. Take the collection module's OrderedDict class. Unlike the conventional dict class, this Python dictionary data format respects element order. Using common library classes like OrderedDict, we can improve Python programs' functionality and performance without writing code.

While Python dictionaries are efficient at storing pairs of data, they do not retain item order. For input order tracking, the OrderedDict collection module class is useful. OrderedDict instances function like conventional dictionaries but retain key-value pair order when inserted. This method is useful when we need to use dictionary elements in order or preserve a specified order. With OrderedDict, we provide consistent and predictable behavior where element order is crucial.

Let’s look at a Python code example:

Import the collection module's OrderedDict class. Next, we assign an OrderedDict object to favorite_languages. OrderedDict() builds an empty ordered dictionary in our favorite_languages, unlike square bracket dictionaries. We then add each name and language from rows three to six to the list of favored languages. When we repeat the preferred languages in the for loop, this ordered dictionary returns the responses in order of entry. This maintains key-value pair order for consistent and predictable data transport.

Here is the output of the code:

The OrderedDict class from the collections module is a useful Python utility. It successfully combines the main benefits of lists, such as keeping element order, with dictionaries, which enable us to correlate information. We may find occasions where an ordered dictionary satisfies our needs when we mimic crucial real-world occurrences. OrderedDict lets us keep items in order while yet having dictionary-like flexibility. As we explore the Python standard library, we will find modules like collections that solve basic programming problems, helping us build more efficient and resilient programs.

Summary

We covered Python class creation basics in this chapter. We learned how to design methods to provide class behaviors and utilize attributes to encapsulate data. The __init__() function helped us establish class instances with preset properties. We also learned how to, directly and indirectly, update attributes to interact with instances dynamically. We found that inheriting attributes and methods from parent classes speed up related class generation. We also examined composition, which promotes modularity and simplicity by employing class instances as attributes.

We also discovered that keeping classes in modules and importing them into relevant files improves project maintainability and structure. We investigated in the Python standard library and discovered an example in the collection module's OrderedDict class. This course showed how to leverage pre-existing modules to address basic programming issues using dictionaries and lists.

Overall, this chapter offered us the knowledge and skills to efficiently design and implement Python classes, fostering modularity, organization, and code reuse in our projects. Understanding classes and modules simplifies building stable and scalable software.


PYTHON FUNCTIONS

Python Functions

Introduction

  • Types of Python Functions
  • Advantages of Python Functions 
  • Disadvantages of Python Functions
  • Best Practices for Python Functions

Python has several built-in methods like print(), input(), and len(). Python lets us write custom functions to solve specific tasks, encapsulating instructions in one object. Invoking functions to do their jobs requires knowing their purpose before implementing them.

Programming relies on functions to reuse and simplify code. Encapsulating a job in a function avoids repeating it in a program. Our code is simpler and easier to manage when we call the function to complete the task.

We'll learn how to construct functions to show information or messages and how to process input and produce results in this chapter. We will also learn modular programming by storing functions in modules. This organizational method keeps codebases clean and simple, making complicated programs easier to understand. In this chapter, we also look at various examples of Python functions in code. There is a Python main function that is also available in Python at the point where the function starts its execution.  

Defining a Functions

Let’s start with a simple Python function example that only prints greetings to understand the basics of the function, how it works, and how it looks (that is its syntax). 


The code's first line defines a function with a 'def' declaration. This function is called "greetings". After the 'def' declaration, the function's body contains its tasks and instructions. Calling the function, as in the last line of code, executes it. Don't mistake defining a function for calling it. Type a function name in parentheses to execute it. We use parenthesis for function parameters. We merely call the function by name in the example because no arguments are needed.

Note: it is common to say that a function “takes” an argument and “returns” a result. The result is also called the return value. There is an inbuilt or Python define function available in Python that makes code compact but it is difficult to understand the user called it is a lambda function in Python. Lambda function Python is used when we want to decrease the length of the code mainly. 

Real-World Example of Python Function Usage

To understand it better let’s take a real-world example, in which Python functions are used to automate and streamline data analysis tasks in a marketing department.

Let’s suppose we are working in a company that sells consumer products online. Our marketing team wants to analyze customer purchase data to identify trends and optimize marketing strategies. We decided to use Python functions to perform various data analysis tasks efficiently.

We first create a function called ‘load_data()’ to read customer purchase data from a database or CSV file. This function takes parameters such as file path and data source type and returns a pandas DataFrame containing the purchase records.

Then, we set up a method called "calculate_metrics()" to figure out important metrics like total revenue, average order value, and conversion rate. This function takes the purchase data DataFrame as input and returns a dictionary with the calculated metrics.

We also create a function called “visualize_data()” to generate visualizations like histograms scatter plots, and time series plots to visualize trends and patterns in the data. This function accepts parameters like the DataFrame and visualization type and displays the plots using libraries like Matplotlib or Seaborn.

In addition, we created a method named "segment_customers()" to divide customers into different groups according to their demographics or buying habits. Utilizing machine learning methods such as K-means clustering or hierarchical clustering, this function use the DataFrame as an input and outputs clusters or segments.

Lastly, we construct a report-generating method called "generate_report()" to arrange the findings of the study in a formal report. This feature compiles data, visualizations, and consumer segments into an all-inclusive report for usage in decision-making or sharing with stakeholders.

The above example teaches us that code that doesn't make use of functions is not only difficult to comprehend and maintain, but also grows more complicated as we attempt to fix the issues. The use of functions allows us to circumvent these issues by separating the code into smaller units that may be accessed as needed.

Passing Information to a Function 

We may utilize the "greetings()" method to greet users by name by adding a username parameter to the parenthesis. By adding the username parameter, the function may take any username. Thus, we must pass a username when using the method. We may call the "greetings()" method with a name like "Bheem" in the parenthesis. This change lets the function dynamically greet people by name based on input: 

Entering greetings(‘Bheem’) calls greetings() and gives the function the print statement information. The function above shows greetings for the name we pass in. Ignoring the name in parentheses and passing an argument in parentheses in our function definition causes a TypeError. Look at this in Python. 

When we define the function, we pass an argument, but when we call it, we don't, so the function throws an error with a message saying that the function "greetings()" needs an argument in place of "username."

Arguments and parameters

The greetings() function in the preceding software needs a username. After we call the method and pass the username, it prints the right greetings.

The preceding code defines an argument, or username, that the function needs to fulfill its work. Greetings include "Bheem" as an argument. Function calls pass arguments to functions. Calling a function with a parameter requires passing a value in parentheses. The function greetings() received the argument ‘Bheem’, which was saved in the parameter username.

Note: Arguments and parameters are sometimes used interchangeably. We shouldn't be startled when function definition variables are called arguments or function call variables are called parameters.

Positional Arguments

Python matches every function call argument with a function definition parameter. The order of arguments is the easiest and most popular method. These value matches are positional parameters.

Take an example function that displays pet information to understand it. The feature displays the pet's kind and name. Look at its Python code.

The method takes two parameters: “animal_type” and “pet_name”. The function “pets” requires two arguments: “animal_type” and “pet_name” in sequence. In the following software, calling the function stores “Dog” in the “animal_type” parameter and “Bruno” in the “pet_name” parameter. These two parameters display pet information in the function body.

Multiple Function Calls

We can call a function as many times as needed and give a different argument. Repeat the preceding example but send different parameters to the code function call. 

In the given code, pets() receives a different parameter. Before executing the function body, Python matches “Cat” with “animal_type” and “Tom” with “pet_name” from the given inputs.

We can use as many positional arguments as needed in functions. Python matches each function argument with its defining parameter.

Use the same amount of arguments and parameters to run the function; otherwise, Python will throw a TypeError.

Order Matters in Positional Arguments

Mixing the order of positional parameters in a function call might provide strange consequences. See the above code with the wrong positional argument to understand.

In the preceding code example, we first call the pet's name and then its type, resulting in an odd output where the pet type receives the pet's name and the pet name gets the animal kind. We send the name as a first argument to the aforesaid function, and the pet name as a second argument saves the animal_type.

Keyword Arguments
A keyword argument is a name-value pair sent to a function. This approach immediately associates the name and value within the parameter, so passing it in the function is clear. It defines the purpose of each value in the function call and allows us to ignore the order of the parameters. Here's the same Python code, but we call the method with a keyword parameter.
The function definition is unchanged, but the call is modified. In the code sample, we identify which parameter each argument belongs to when calling the function. Python assigns "Cat" to the "animal_type" argument and "Tom" to the "pet_name" field. Thus, the report properly indicates we have a cat named Tom. This method offers exact parameter assignment, enabling correct function execution.
The sequence of arguments doesn't matter because Python places each value dependent on its parameters. Specifying arguments in the function call lets Python appropriately assign values to their places. Python binds values to arguments, thus the following two function calls work similarly. Python functions are more versatile and usable due to parameter order flexibility: 
 
Note: Keyword arguments require that the function definition's parameter names match the function calls. This guarantees Python appropriately links each argument with its parameter, preventing function execution issues.

Default Values
Python routines may need default parameter values. Python uses an argument's value in a function call. Python uses the parameter's default value if no argument is given. By setting default parameter values, we simplify function calls and clarify function usage.
It's beneficial when we call a function repeatedly and the parameters are usually the same. If we have to use the previous example again and the “animal_type” parameter is usually “Dog” then we can set “animal_type = Dog” default so we just need to give the pet's name parameter when inserting a dog's information. Non-default parameters come before default parameters, hence pre-defined parameters appear after undefined functions. Use Python to examine this: 
 
If we want to describe other than a dog, we could use a function call like this:

pets(pet_name="harry", animal_type='Cat')

Note: Default values must be placed after non-default options. This ordering helps Python parse positional arguments without ambiguity. 

Equivalent Function Calls
Python functions can use positional, keyword, and default parameters. The function can be called in several ways due to its flexibility. Consider pets(), which has one default value:

def pets(pet_name, animal_type="Dog"):

According to the definition, we must always pass a value to pets(), which might be positional or keyword. If we need to describe the animal kind (not Dog), we must provide the “animal_type” parameter to the call, which can be positional or keyword.
Below is the code with different function calls that would work for the above function:

Note: Personal preference determines calling style. We care most about function calls producing the intended result. Thus, we should choose the most simple and understandable calling style.

Avoiding Argument Errors
A typical problem when working with functions is mismatched parameters. Unmatched argument errors arise when we provide a function fewer or more parameters than it needs. Let's say we call a function without an argument yet define it with two arguments.  

The error notice suggests checking our function call for errors on line 4. The last line of error tells us the error type is TypeError and describes the problem with our software. The notification says pets() needs pet_name and animal_type.

Return Values and Return Statements
Len() calculates and returns the parameter length as an integer. The len() method returns 5 since "Hello" comprises five characters. Function returns are known as return values.
We may provide a function's return value using a return statement. Specific restrictions govern the return statement: -
  • The return keyword
  • The value or expression that the function should return
When combined with an expression, a return statement outputs the expression. This suggests that the function return value is the expression output. To demonstrate, let's create a function that creates a unique string from numerical input. 
Python imports random first. The method `getAnswer()` is then defined. Since its code block is declared, not called, it's not run immediately. In the third-to-last line, the `random.randint()` function is called with inputs 1 and 9, and its result is placed in `r`. This variable contains a random integer from 1 to 9.
The software gives "r" as input to the `getAnswer()` method. The function stores "r" in "answerNumber". The function then returns one of many string values dependent on "answerNumber". Later in the program, the `getAnswer()` method is called again, storing the string in the "fortune" variable. Finally, the "fortune" variable is sent to the `print()` method, showing the outcome using the specified code.
As seen below, we may reduce the last three lines of the code into one line by giving return values as parameters to other function calls.

print(getAnswer(random.randint(1,9)))

Making an Argument Optional
Sometimes an argument must be optional to provide users the option to enter more information in the function. When filling out an online form, we may choose our first, middle, and last names. Such middle names may be optional, so users might choose to use them. We can do this by making the parameter optional with default values.  
The musician_name() method takes first, last, and middle names. Starting with a necessary middle name, the function is defined as follows:

This function accepts three strings as arguments and combines them to produce the output, but only if all three parameters are provided. 
Without a middle name parameter, the above code will fail as the middle name is not always required. The function can work without a middle name by setting an empty default value. We allow users to omit the middle_name option unless they give a value by changing its default value to an empty string and placing it at the end of the list. Here's the Python implementation:

This function accepts three strings as arguments and combines them to produce the output, but only if all three parameters are provided. 
Without a middle name parameter, the above code will fail as the middle name is not always required. The function can work without a middle name by setting an empty default value. We allow users to omit the middle_name option unless they give a value by changing its default value to an empty string and placing it at the end of the list. Here's the Python implementation.

Python Classmethod() Function
Python classmethod() is related to the class not to the object. they can be called both class and object. We can call this method via a class or an object.

Recursion in Python

The meaning of recursion is that defining something in terms of itself. In other words, we can say that recursion in Python means that a function calls itself indirectly or directly. Below is the very basic code on recursion.

In the above code ‘factorial’ function is used to calculate the factorial of a non-negative value ‘n’. it uses recursion by calling itself with a smaller argument (‘n-1’) unit the base case (n==0’) is not met. When it meets the condition n=0 then it returns ‘1’. Then, it unwinds the recursive calls, multiplying each result by ‘n’ until it reaches the original call.

Returning a Dictionary
A function can return different values to meet program demands. Includes simple data types and complex data structures like lists and dictionaries. Python methods that accept name components and create a dictionary describing an individual is an example:

The "build_dict" function stores first and last names in a dictionary allocated to "person". First and last names are recorded in this dictionary under the "First" and "Last" keys. Use "return" to return the person's details dictionary to end the function. Printing the returned dictionary shows the original information.
Expanding the "build_dict" function to include age is easy. To accommodate this additional data, we may add an optional age parameter to the function specification. Age values are added to the dictionary under the relevant key. This change lets the function keep the person's age and first and last names. The function smoothly accommodates no age value, ensuring data structure flexibility.

The function specification includes an optional argument "age" with an empty default value. The person dictionary stores the "age" argument value when the function is called. The function always stores a person's name and allows for extra information, allowing for the preservation of different personal facts.

Using a Function with a while Loop
Python components we've covered can merge functions. For example, we may create a while loop method to greet users. We may first welcome people by first and last names:
The example shows a simpler `build_dict()` code without middle names. In the code, the while loop requests the user's first and last names consecutively. 
No quit condition is given in our while loop, which is flawed. When asking multiple inputs, where should a quit condition go? Every prompt should provide a way to stop so users may do so easily. Break statements make the loop exiting at any prompt easy. Look at this code: 

The code includes a message to help people leave the loop. If the user enters 'q' at either prompt, the loop breaks out, allowing the application to run until the user quits by entering 'q' at any time during name entry. 

Passing a List  
Programmers often pass lists to functions, whether they include names, numbers, or dictionaries. A function has direct access to list items when supplied a list. Create a list-efficient function to demonstrate this. We obtain a list of users and print individualized greetings for everyone. Despite its simplicity, this function shows how lists may be modified within functions. Let's analyze Python code: 
The code creates a function called "greetings" that takes a list of names, "names". The method prints a personalized welcome for each user by looping over the list's names. To test the function, the "greetings()" method receives a list of usernames called "usernames" as input.

Modifying a List in a Function
We may change a list inside a function, knowing it will survive after execution. We may apply modifications directly to the list supplied as an input to the function, which helps us work effectively while managing large datasets.
Suppose we have to print a 3D model from the user's design. We keep a list of designs to print and move them to another list after finishing. Below are two codes without functions and one with functions. Using a function makes our code easier to understand and execute anytime we want.

The software starts with a collection of print designs. After printing designs, an empty list called completed_designs is formed. While loop iterates until unprinted_designs still have designs. In each iteration, a design is deleted from unprinted_designs, indicating printing. The current printed design is shown and added to the completed_designs list. After printing, the application displays a list of completed designs.
Refactoring the code into two task-specific functions improves efficiency. The first function prints designs, while the second summarizes them. This simplifies and improves code readability without changing functionality: 


The list of designs to print and the list of completed models are sent to "printing_models()". It iterates through the list, shifting designs from unprinted to completed models to simulate printing. The single-parameter "showing_completed_models()" method iterates through the list of finished models to output model names. This job division into two functions improves code modularity and clarity.
The result is unchanged, but the code is more organized. Splitting the code into two functions, each with unique duties clarifies the program's primary functionality. Modular code is easier to understand and maintain.
The software initializes a list of unprinted drawings and an empty list of produced models. With important tasks outsourced to distinct functions, the main code body calls them and passes parameters. The list of unprinted designs and completed models are passed to printing_models() initially. Each design's printing is simulated using this function. After that, the showing_completed_models() method displays the successfully printed models using the list of finished models. Descriptive function names improve code readability and comprehension without comments.
Programming with functions is far more extensible and maintainable. If we need to print more designs, we use "printing_models()" again. If any printing process adjustments are needed, we can modify them once in the function and have them instantly applied elsewhere. This is far more efficient than updating the code at various points in the program.
The code example above shows how each function is dedicated to a purpose. Here, the first function prints each design, and the second displays the finished models. This method is better than integrating both chores into one. If a function performs numerous jobs, it's best to split it into two or more functions that focus on different parts of the process. Following this method organizes and simplifies code. Additionally, invoking one function from another can help divide complicated operations into simple chunks.

Preventing a Function from Modifying a List

The original list may need to be protected from function changes in some cases. As an example, we may want to save the list of unprinted designs after printing. Transferring all design names to the `unprinted_designs` list within the software has left the original list empty, leaving just the updated version. We can provide the function a copy of the list to avoid data loss. This guarantees that function changes affect the duplicated list, not the original. We can replicate a list for a function with one line of code:

function_name(list_name[:]) 

The slice notation [:] lets us copy the list and give it to the function without changing it. In the example, we may execute printing_models() to prevent depleting the unprinted designs list:

printing_models(unprinted_design[:]. completed_models)

Since it accesses all unprinted design names, printing_models() works. Instead of the original unprinted_designs list, it uses a copy. Thus, the function does not change the unprinted designs list, but the completed_models list will still include the names of the printed models.
Except in certain cases, functions should get the original list. Although sending a duplicate can retain the list's contents, especially when adjustments are done within the function, it's more resource-efficient, especially with big lists, to let a function operate directly with the existing list. This method reduces the computational expense of duplicating the list, optimizing program speed.

Passing an Arbitrary Number of Arguments 

Sometimes, while programming functions, the number of parameters is unknown. Fortunately, Python allows functions to take any amount of parameters from the call. This feature lets us gracefully handle varied argument counts without parameter declarations.
This notion can be best understood with an example. Imagine a pizza-assembling function. A person may pick how many toppings to put on a pizza. *toppings is a function argument in the code below. This parameter is unique since it can receive any function call argument. See the Python code:

The asterisk in function parameter *toppings tells Python to construct an empty tuple called "toppings" and collect any values. According to the output, Python can handle function calls with one or more values using the print statement in the function body. Python treats both call types equally. Python packs a tuple even if the function receives one value.
The above software may be modified to loop over the toppings and describe the pizza requested:

Even though the function responds appropriately, whether it receives one value or three values as we can see in the above result.

Mixing Positional and Arbitrary Arguments

If a function may receive many parameters, the parameter that accepts an arbitrary number must be in the last definition. Python matches positional and keyword arguments first, then collects any leftover arguments in the last parameter.
For instance, update the above example. The pizza size parameter must come before the toppings parameter in this new example.

Python stores the first value in the argument “size” in the function definition above. All subsequent values are kept in tuple tops. The function calls start with a size parameter and then as many toppings as needed.
Each pizza has a size and many toppings, and the information is printed in the correct order, starting with the size and then the toppings.

Using Arbitrary Keyword Arguments

Sometimes we wish to take an arbitrary amount of arguments but don't know what the function will get up front. The calling statements let us build a function that accepts numerous key-value pairs. One example is generating user profiles, when we know we'll obtain user information but don't know what. For clarity, let's create a function called building_profile() that receives a first name, last name, and an arbitrary number of keyword parameters: 

Above, building_profile() demands a first and last name and then lets us or the user give in as many name-value combinations as we wish. The above method uses double asterisks before **user_info** in the last argument. Python creates an empty dictionary named user_info and packs name-value pairs into it. In the function, we may access user_info name-value pairs like any other dictionary.
In building_profile(), we create an empty dictionary named profile to store the user's profile. After constructing the dictionary, we included the user's first and last names (which are the following two lines after declaring the dictionary) because we always receive them. For each key-value pair in user_info, the for loop adds it to the profile dictionary. Lastly, we return the profile dictionary to the function call line.
We run building_profile() with the first name ‘albert’, last name ‘einstein’, and key-value pairs location=’princepton’ and field=’physics’. User_profile is saved and printed with the returned profile.
Location and study area are included in the dictionary along with the user's first and last names. No matter how many key-value combinations are given, the function will work.
We can mix positional, keyword, and arbitrary values in our functions. Knowing these argument types is useful since we'll see them often while reading other people's code. Practice is needed to master the many types and know when to employ them. Use the simplest, most effective technique for now. As we progress, we'll learn to always take the best practical action. 

Local and Global Scope

Calling a function assigns parameters and variables to that function exclusively, or in its local scope. The global scope includes variables assigned outside all declared functions. Variables defined inside the local scope are termed local variables, whereas those declared outside all functions are called global variables. Local and global variables cannot coexist.
Think about scopes as variable containers. All scope values are lost if the scope is deleted. Only one global scope was generated when our program began. Our application deleted its global scope and forgot all variables after execution. If they aren't removed, their values will be kept from the previous time we ran it, which might impact the program or produce an error.
Calling a function creates the local scope. Variables declared in this function are strictly local. After the function returns, the local scope is deleted and these variables are lost. If we call the function again, the local variables don't remember or don't know their prior value.
Scopes matter for some reasons:
  • Code in the global can access global variables.
  • However, a local scope can access global variables.
  • Code in a function’s local scope cannot use variables in any other local scope.
  • We can use the same name for different variables if they are in different scopes. That is, there can be a local variable named x and a global variable also named x.
Python has several scopes instead of global variables. The function can only interact with the rest of the program through its arguments and return values when variables are modified by the code in a specific call. It reduces the number of code lines that might fail. If our application exclusively uses global variables and we identify a fault that causes a variable to have an incorrect value, it would be difficult to trace where it was set. This variable value may be set anywhere in our program, which may include hundreds or thousands of lines. Local variables can create errors, but we can easily discover the code in that function that sets the variable incorrectly. 
Global variables are OK for tiny applications, but they're horrible for long ones.
 
Local Variables Cannot Be Used in the Global Scope
Let’s first look at a program then by explaining it we learn more about why we cannot use local variables in the global scope.

The preceding code fails because the eggs variable only exists in the local scope generated by spam(). The local scope is deleted when the program execution returns from spam, thus eggs no longer exist. When we execute print(eggs), Python gives us an error because eggs are not defined. When a program is executed with the global scope, no local scopes are generated or exist, hence there are no local variables. Thus, only global variables may be utilized globally.

Local Scopes Cannot Use Variables in Other Local Scopes 

A new local scope is formed when we call a function, including functions called from another function. Show and explain a code to better comprehend it:

When the program starts, the spam() function (the final line of out) is run, the local scope is formed, and eggs are set to 99. Then we call becon() to generate another local scope. Many local scopes can coexist. Unlike spam()'s local scope, this new local scope sets ham to 101 and contains new local variable eggs. This local variable egg is 0.
This call's local scope is removed when bacon() returns. The program proceeds in the spam() method, which prints the value of eggs. Because the local scope for spam() is still present, the eggs variable is set to 99. This is software output.
One function's local variables are fully independent from the other's.

Global Variable Can be Read from a Local Scope

Let's understand how a global variable can be read from the local scope with the help of Python code:
Since there is no argument called eggs or code that assigns a value to eggs, Python considers eggs as a reference to the global variable eggs in the spam() method. This is why the previous program prints 42.

Local and Global Variables with the Same Name

Avoid using local variables with the same name as global variables and other local variables to simplify programs. Python is technically fine for this. We'll see what happens using Python code:

In the above program there are three different variables, but to make us confused we named them all eggs. The variables are as follows:
  • A variable named eggs that exists in a local scope when spam() is called.
  • A variable named eggs that exists in a local scope when bacon() is called.
  • A variable named eggs that exists in the global scope.
As shown above, all three variables have the same names, making it difficult to determine which one is being utilized. This is the major reason to avoid reusing variable names across scopes.

The Global Statement

To edit a global variable within a local variable, use the global statement. If we write “global eggs” at the top of a function, Python knows to not create a local variable with the same name. Look at a code to understand:
When we run the program, the final print() call will output “spam”.
The eggs are declared global at the start of the spam () method, thus setting eggs to "spam" assigns it to the global scoped spam. Thus, no local spam variable exists.
There are four rules to tell whether a variable is in a local scope or global scope:
  1. A variable is always considered global if it is utilized in the global scope, which means it is used outside of all functions.
  2. A variable is considered global if a function has a global statement for it. 
  3. The variable is local if it is not utilized in an assignment statement within the function.
  4. However, a variable is global if it is not used in an assignment expression.
To understand these rules much better let’s look at a Python code:

In the spam() method, eggs are the global variable because there is a global statement at the beginning. The bacon() method has an assignment statement, therefore eggs are a local variable. Since there is no assignment statement or global statement for eggs in the third function, ham(), eggs are the global variable. Run the application and get “spam” back.
We know that function variables are either global or local. A function cannot utilize a local variable named eggs and then the global eggs variable.
To edit a global variable's value from a function, we must use a global statement.
Python will warn if we utilize a local variable inside a function without assigning a value. Let's check a program for errors. 


Python believes eggs are local because spam() has an assignment statement for eggs. But print(eggs) is run before eggs are assigned a value, therefore local value eggs don't exist. Python will not use global eggs.

Exception Handling
Getting an error or exception in Python crashes the entire application. We don't want this in real-world programs. Our application should identify issues, fix them, and keep running.
For example, let’s create a program that divides a number by zero:
The spam function above took an argument and returned 42 split by it. We print the function's value with several parameters to see its output. The first two values are appropriately divided, but 42 by 0 yields a ZeroDivisionError. Whenever we divide by zero, this happens. According to the error message's line number, spam()'s return statement causes the fault.
The above program has an error, but attempting and except statements can fix it. This puts error-prone code in a try clause. Errors cause the program to start the following unless clause.
The prior divide-by-zero code can be in a try clause, and an except clause can address this issue.

The preceding code instantly executes the except clause if the try clause fails. The output shows that after running that code, the remaining code executes normally.
We must also know that try block function call errors are caught. The same Python code as previously, but with try and except clauses in function calls.

The reason print(spam(1)) is never executed is that after the execution jumps to the code in the except clause, it never comes back to the try clause. Instead, it continues moving forward/down as normal.

Storing Your Functions in Modules
Features of functions include separating code from the main application. Using descriptive function names will make our main program simpler to read. We may also save our functions in a module and import it into our main application. An import statement in Python makes code available in the program file's executing module.
Putting our functions in separate files lets us hide the code and focus on the program's logic. This lets us utilize the same function in several apps. If we put our functions in separate files, we may share only the files related to or needed by other programmers without sharing our full program. If we know how to import functions, we can use other programmers' function libraries.

Importing an Entire Module

We must construct a module before importing functions. A module is a.py (or.ipynb) file that contains the code we wish to import into our application. Let's examine a module with our function. To construct this module, we must delete everything from that file except the function.
This code creates a file named making_pizza.py. It usually has to be in the same directory as pizza.py. The second file imports the newly generated module and executes make_pizza() twice. 
Python examines the line import pizza and opens pizza.py to transfer all its routines into the current program or program it is called in. Python copies code silently as the application runs. We just need to know that making_pizza.py does not contain pizza.py functions.
If we need to call a function from an imported module or file, we enter the module name, pizza, followed by the function name, make_pizza(), separated by a dot. This result matches the original output without importing a module.
In the first method, we type import followed by the module name to import all its functions into our application. This import line imports module_name.py and makes all its functions available:
module_name.function_name()

Importing Specific Functions

If we want to only import a specific function from a module, then we can do this also. Below is the general syntax for this approach:
from module_name import function_name
By the above syntax, we can import as many functions as we want from a module, the functions are separated by commas. Let’s look at a common syntax example for this:
from module_name import function_0, function_1, function_2
If we take the above example for importing only the function from the pizza.py module then the code will be written as:
The aforementioned solution eliminates the necessity for dot notation when calling functions. Since we specifically imported make_pizza() in the import section, we can call it by name when we use it.

Using as to Give a Function an Alias

If the function we're importing clashes with an existing name in our application or is lengthy, we may use a short, unique alias—like a function's nickname. We must import the function with this specific moniker. 
Import make_pizza as mp and alias it to mp(). Functions are renamed using the specified alias when using the as keyword:

The import statement renames make_pizza() to mp() in the above code. Any time we need to use make_pizza(), we only need to type mp() in the program instead of the complete name, and Python will run the code. This avoids confusion with other make_pizza() functions in the program file.
from module_name import function_name as fn

Using to Give a Module an Alias

We may also alias module names if needed. If we give a module a short alias, say p for pizza in the following code, we may call its method faster. Calling p.make_pizza() is shorter and easier than a pizza.make_pizza():

In the preceding code, pizza is given the alias p in the import statement, while the other module's functions keep their names. Calling the functions with p.make_pizza() is shorter than pizza.make_pizza() and focuses attention away from the module name to the descriptive function names. These function names make it clear what each function does, improving code readability more than the module name.
The common or general syntax for this approach is:
import module_name as mn

Importing All Functions in a Module

If we need to import all the functions from a module then we need to use an asterisk (*) operator. Let’s look at the code and explain it for better understanding.
The asterisk in the import statement tells Python to transfer all pizza module functions into the current program file. This approach lets us call each function by name without using the dot notation because every function is imported. We shouldn't utilize this strategy when working with huge modules we haven't constructed or written since if the module's function name matches ours, we may obtain odd outcomes. Python may replace functions or variables with the same name instead of importing them.
The best option is to import the appropriate function or functions or the complete module and use dot notation. This produces clean, easy-to-read code. This section helps you spot import statements like this in other people's programs:
from module_name import *

Styling Functions
Function names should be meaningful, using lowercase letters and underscores. Using descriptive labels helps you and others understand your code. Module names should follow these guidelines.
Using program comments, we must clearly explain each function. The function comment must follow the function declaration and use docstring format. A well-documented function can help other programmers use it appropriately by reading the docstring. If students know the function name, its inputs, and the value it returns, they should be able to utilize the code in their applications. The remark describes what the code will perform.
Function parameters with default values should not include spaces on either side of the equal sign. An example syntax follows:

def function_name(parameter_0, patameter_1=’default value’)
The same convention should be used for keyword arguments in function calls:
function_name(value_0, parameter_1=’value’)

PEP8 recommends limiting code lines to 79 characters to fit in a reasonable editing window. If a function's definition exceeds 79 characters due to parameters, press Enter after the opening parenthesis. Press the tab twice to separate the function body, which will be indented one level, from the parameters on the next line.
Almost every editor automatically aligns subsequent argument lines with the indentation on the first line of function, loop, program, etc.
Divide numerous functions in your program or module by two blank lines to make it easier to identify where one ends and the next begins.
File opening statements should include import statements. The only exception is if you describe the program in the file's beginning remarks.

Summary
In this chapter, we learned about Python functions, which are blocks of code that execute certain tasks. By breaking down repetitious activities into functions, they improve code reuse, readability, and modularity.
We learned how to construct functions using the def statement, call them by name, and give parameters to dynamically change their behavior. Python matches arguments with parameters by order, allowing function calls and declarations to flow smoothly.
Function arguments, positional and keyword arguments, and parameter interactions must be understood. We saw how Python required matching parameter counts for function execution and handled multiple positional arguments smoothly.
We explored how return values improve program functionality and modularity by providing function results. We also examined real examples of return values generating dynamic outputs from input parameters.
We also used default parameter values to identify optional inputs, improving function flexibility and usability. We stressed parameter order and how to handle optional parameters in functions.
Additionally, we studied how functions may efficiently handle and interact with lists by receiving lists as parameters or altering them in the function body. Function specialization and modularization improve code clarity and maintainability.
Finally, we learned how to handle arbitrary parameters, letting functions handle variable input data. Variable scopes were also stressed to define local and global variables within functions.
Python developers may use functions to solve varied programming problems by grasping these ideas and writing fast, adaptable, and maintainable code.


Featured Post

ASSOCIATION RULE IN MACHINE LEARNING/PYTHON/ARTIFICIAL INTELLIGENCE

Association rule   Rule Evaluation Metrics Applications of Association Rule Learning Advantages of Association Rule Mining Disadvantages of ...

Popular