Transcripts
1. Introduction: Hello, guys. Welcome to the ultimate Python programming
course on skill share. In this comprehensive course, you'll dive deep into Python, mastering everything
from the fundamentals to advanced object
oriented programming. In this course, you not
only learn Python from the very basics to advanced
object oriented programming, but also discover
how to work with virtual environments and manage multiple versions of Python. You will learn how to
switch between versions and handle various
libraries efficiently. These are crucial
skills for working on real world projects
where the ability to work with different
Python versions and their respective libraries
is often required. I believe in delivering valuable knowledge without the fluff. Each lesson is concise and packed with
essential information, helping you learn effectively and with a focus on
hands on experience. In this project, we will build a fully functional QR code
generator application that can create QR codes for
your social media profiles. You will learn how to
use Python libraries, work with modules, and
implement key data structures. Allow me to show you why
diving into Python right now can be a game changer for your career and
personal development. According to recent
job market analysis, Python consistently ranks among the top programming
languages in demand. This translates to
numerous job opportunities and career stability
for Python developers. Knowledge of Python
can also open doors to specialized high paying roles
such as data scientists, machine learning engineers,
and AI specialists. Python is the best language
for the beginners. It syntax is clear
and straightforward, allowing you
programmers to pick it up quickly and focus on learning programming concepts rather than getting bogged down by
complex syntax rules. Investing your time in
learning Python today is an investment in your future.
Don't wait any longer. Enroll now and start your journey to becoming a
skilled Python programmer. Let's unlock the power
of coding together.
2. Python Installation: And first, let's check if Python is already
installed on your system. If you have Windows, you can do this by
pressing Wind plus R and typing CMD
and hitting Enter. In the command prompt window, type the following
command and press Enter. And here we are, we
can see our Python. On MacOS and Linux, we also open terminal Window and type the following command
and then press Enter. Don't pay attention all
these Python versions. We'll cover it later.
As we can see, I don't have the
command Python version, but I have Command
Python three version. The commands Python version
and Python three version are used to check the version of the Python interpreter
installed on your system. Python version, the command traditionally used to check
the version of Python two, while Python three version is specifically used to check the version of the Python
three interpreter. Typically, Python two was
used on older systems. If you prefer, you can adjust your system configuration to make Python point
to Python three. But I prefer continuing with the command Python three because
it's commonly practiced. If you don't have any Python
on your operating system, we will go to the python.org site and
follow the instructions. Here you choose your
operating system and choose version
of Python you need. I work on MacOS and Linux, but installing Python on Windows is not much
more difficult. You download the
version of Python, then open download it file, and follow the
installation prompts. Here we can download
Python for Linux. If we go to Python, Python packaging user guide, and here I choose tutorials and then choose
installing packages. We can see a lot of information how to check the version of Python and also how to install Python on different
operating systems. And here you can notice that we can install Python
in different ways. For example, here we can see
the commands we can use for installing Python on a
Bunto or if we use Macs, we can use Homebrew. Homebrew it's a package manager. You can read more on its site. With this package manager, we can install Python
using this command. But what the difference?
Well, shortly, the folder, Homebrew and official installer use the different folders
for installation. Also updating. Homebrew
easily update Python to the latest version with a single command
Brew upgrade Python. Homebrew also
manages dependencies and automatically
cleans up old versions. While official
installer to update, you need to manually
download the new version from the website and go through the installation
process again. The installation via
the official installer is more self contained, which may be useful
in certain scenarios, but can make managing
dependencies and additional tools
more cumbersome. And it often adds application icons in the
applications folder. If I want to see all
Python versions, I installed with the command
Brew install Python, I can use the Command
Brew List Python, and I immediately see all folders and all
Python versions I have. Also, I want to notice
that uni installation with Home Brew is
straightforward with a single command Brew
Uni Install Python, which completely removes the installed version
from your system. While official installer
uninstalling is more complex and require manual moving files
from various directories. As for me, Homebrew is more convenient for
developers and those who frequently work with Python and other tools via the terminal. Official installer might be
suitable for users who prefer minimal system interference and need to install Python
once for specific needs, and don't plan on frequently updating or managing
multiple versions. The same thing if we use
Linux, Ubuntu, whatever, we can use Sudo Ogat install Python or we can installing
Python from source. In the first case, this is the simplest method
to install Python, but you can only install
versions of Python that are available in your
distributions repositories. Installing from source, you can use any versions of Python, including the latest releases. Now that we've understood
how to install Python and the difference between the
installation methods, let's move on to
learning Python itself. I open terminal and type
the command Python. One of Python's unique features
is its interactive mode, which lets you execute code and immediately see the results. This is made possible by
the Python interpreter, a program that reads and
executes Python code. When you install Python
on your computer, it includes an interactive
interpreter known as apple. Redevelop pre ent loop. It allows you to enter code one line at a time and
see the result instantly. By using the interactive mode, you can quickly experiment with different code snippets and
see the result immediately. To exit the interactive mode, use the command exit. Well, we saw interactive
mode in the terminal, but we also have
several tools that helps us to write code
more efficiently. Let's take a look at them.
3. Setting Up: Installing Code Editors: Welcome back. In the
previous lesson, we've discovered interactive
mode when we can execute code and immediately see the results in the terminal. Today we will look at
the tools that make coding easier and
more efficient. Three of the most commonly
used code editors are Pycharm, Visual Studio code,
and Jupiter notebook. Which one you will
choose, it's up to you. The first one Pycharm. There are several options
community and professional. This idea is specifically
designed for Python, but you also can use
other languages. It's like a toolbox
for Python developers. You can download
Biharm Community. It's completely free,
and to be honest, it's enough for the first time. The installation process
doesn't take much time. We download the file and
follow the instructions. There are a lot of features
like debugging tools, project management,
code suggestions. Very useful. It's better for larger projects
where you need to keep everything
organized and efficient. Then we have Visual Studio code. It's a lightweight
code editor that supports many programming
languages, including Python. After downloading the file, open it by double click it. This will extract the Visual
Studio code application. Drag the Visual Studio
code application into your application folder, and here we are, this
installs VS code on your mac. Here you can create
file, open folder, or view recently opened files here we can
see several tabs. No folder opened. The tab appears when you haven't
opened a folder for workspace. It prompts you to open a folder to start
working on a project. Once you open a folder, the tab disappears and the files in the
folder are displayed. Open Editor tab shows a list of all files you currently
have opened in the editor. Here we can create new
file from the scratch. The outline tab provides a structured view
like functions, variables, classes in
the currently open file. It helps you quickly
navigate through your code by jumping to
different parts of the file. If you want, you also
can hide these tabs. It's perfect if you are
working on different types of project and want a flexible
and all in one editor. It's very easy to install
and very easy to use. Then we have Judi notebook. It's a web based tool. You can use it for data
science and research. It allows you to write and
run code in small chunks, and you can immediately
see the results. It looks like interactive mode, except this tool is
great for creating interactive documents
that combine code, text and visualization. Jupiter notebook, you can
install different ways. Installing Jupiter
using Anaconda and Conda or you have alternative. You can installing Jupiter with PIP package manager if you
choose to use Anaconda, so it installs Python
and Jupiter Notebook and other commonly used tag address for specific computing
and data science. So you have all in
one but to start, you can manage with
Jazz Jupiter notebook by installing it using PIP. You can use whatever you want. It's up to you. In this course, I will use Visual Studio code. Well, after all of
this preparation, let's start writing the code.
See you in the next lesson.
4. Managing Python Versions: Virtual Environments and Pyenv: Very often in reality, you will have to work with
multiple versions of Python. This is because each project has its own technology stack
and package versions. To avoid creating a mess on your work computer and dealing with conflicts between
different versions, it's ideal to use a
virtual environment. It's not urgently
needed right now, but I suggest you to
understand how it works. It will help you a lot.
You can skip this part. It will not affect your
learning of the Python basics. This will be more necessary when you start
working on a project. And now let's get started. Guys, if you want to manage multiple Python versions
on your machine, there is a tool PMF. It lets you easily switch
between multiple versions of Python and change the global Python
versions on your machine. Let's get started
from the Macos and then I will show you
how it works on Ubuntu. The first step you
need to take before installing anything
new, it's update. And just in case upgrade
to upgrade all packages. The first command updates local repository metadata,
the second command, Brew upgrade, upgrades all
the installed packages on your system to the
latest available versions. It's commonly used practice that users first run
Brew update to get latest metadata and then run Brew upgrade to update
their installed packages. This ensures that the system has the latest
software installed. We go to the Github and
follow the instructions. Then we use them Brew
install for installing PMF. Just copy this command
and execute it. Let's return to
the documentation and see what we need to next. Scrolling down, and here we
are I use Z SH or shell. It's a common line
shell that serves as an alternative to the more
commonly known Boss shell. So I copy all this code
and post it in SHC file. So we have installed PNF, and now I want to talk with you about Virtual environment. Virtual Environment
solves a real problem. You don't have to
worry about packages you install on the
main system location. PyNVirtoNv is a plugin for
the PNF tool that allows user to create and manage virtual environments
for Python projects. With this, you can isolate project dependencies
more efficiently. So again, follow
the instructions and install this plugin. After installation,
we copy this command and add this to the SHRC file. In this case, we do it manually. Open the HRC file. It's a hidden file and typically located in the
user's home directory. I use simple user friendly
text editor nano. You can use VIM. Here we can see three
rows of code that was executed when
PAN was installed. And pause this command here. I write my comment for the better understanding
in the future. Again, for nano text editor, I use the commands Control
O and control exit. It allows me write and
exit from the text editor. You can use your text editor
and execute your commands. Then restart shell
with this command. And we can use these tools. So let's check it. Here we
can see a small documentation with commands for
PNP and PM Vertls. The first command,
we check PN version. It displayed the currently
active Python version, along with information
on how it was set. For now, I don't have any. If I want to now list all
Python versions known to PM I use the Command
Pimp versions. And for now, I didn't install any Python version with PMP. If I want to see the list of Python version available
to be installed, I can use the Command
PM install dash list. So let's try to install
Python with PM. For this, we use the
command PM install, and then I specify the
version of Python. I will install
another version of Python to demonstrate how you can work in isolated
virtual environments with different Python versions. Now at the same time,
you will learn how to install and remove
Python using PNP. If I now check the versions, we will see several
Python versions. The Asterix indicates that right now I'm in a system
global environment, but I have two Python
versions that I can use for creating new virtual
environments for other projects. On this operating
system, I have globally, I mean, Python
version 3.10 eight. I said globally because
for every project, we can have its own
Python versions. And now with this command, I will create the first
virtual environment. For test project, I use the
command Py Virtual ENF. Then I choose the
version of Python, and then I can call my
virtual environment, whatever it's up to you. I will call it NF and
version of Python. And now with the command
PN virtual lens, I can see list all existing
virtual environments. To activate my newly created
virtual environment, I use the command PNF, activate, and then name my virtual
environment V 3.90. I immediately can
see that I'm in it. If I check the
Python version here, it's 3.90, unlike the global version
that we tested earlier. If you have several
virtual environments and want to switch between them, you can execute the
command PM, activate, and then name other
virtual environment, even if you write now in another active
virtual environment. Now if we install
something here, it remains isolated from
global environment. Any packages or
dependencies installed inside my virtual
environment will not affect the system wide
Python installation or other virtual environments
that we can create. So let's install something here. Let it be Jupiter. I go to the documentation
and follow the instructions. Jupiter it's a tool
for code execution. I choose Jupiter, for example, it can be any packages
or libraries you want. Now with the command PIP freeze, I can see all packages that was installed in
my virtual environment. PP is a package
manager for Python. Now let's imagine
that we don't need this virtual environment
anymore. How we can delete it. First, if it's active, we should deactivate it with
the command PM deactivate. Then we use the command Virtual delete and name our virtual environment
that we want to delete. So when I check Pi
on Virtual lens, we don't see our virtual
environment anymore. It was deleted along
with all packages and libraries that we installed
there very useful thing. But it doesn't mean that
Python version that we use in this virtual
environment was also deleted. If we check Python versions, we still can see several. I added before another one, just to show you how we can uninstall Python
versions with BMP, the command, uninstall and then version of
Python and Viola, we uninstall Python
version 3.9 0.8. With these tool, it's so simple manage different
Python versions. Now let's install it on a
Bunto. We do the same thing. Go to Github page and
follow the instruction. Here, I chose
automatic installer. And here I copy this
command for installation. Before installation, I use
the command pudo Ug update, this command, managing
system packages, then psudoUg upgrade. So we update and upgrade all installed packages
on our system. Now we can install PAMP Fast this command
that we copy previously. Then return to the front page, or documentation, and copy
these three lines of code. We will write it
in Bahar C file. I is also hidden file. It's very similar that we
did with MACOS previously. If you couldn't install
Tmp and you got an error, make sure that you installed
all dependencies for Python and have Gid on your PC. After all of this, restart
shell with the command, and we can use this tool. We use here the same command we used previously on Macros. Let's install Python 3.90. Here we can see our
installed Python. Now let's create
virtual environment based on this Python version. We activate it the same way
as previous with MacOS with the command and activate and then name a
virtual environment. Probably you won't
encounter this problem, but I have inconvenient
behavior on my system. Right now, I don't see that
I'm in virtual environment. If I check it, I can see
that we have created it. So I had to add these few rows of code in my BachRCFle
and everything works. On a Bundu, I also
use nanotext Editor. These commands allow me to write and execute
from the BachRCFle. Execute the command source BRC. It's for execution
the BRC script in the current shell session. And while we fixed it. Right now we are in our
virtual environment, and inside, we have the Python version that
was used for creation. So, guys, all commands
and all the next steps, the same as we did previously. I hope this knowledge will help you see you in
the next lesson.
5. Python Syntax and PEP Guidelines Explained: In the Python community, there is an established
approach to making changes. No significant changes are
made without the creation of a new document called the Python Enhancement
proposal PEP. PAP documents play a crucial role in the
Python community, serving different purposes
depending on the topic. Information, they provide
essential information for core Python developers and announce release
scheduled for Python. Standardization. They establish
coding style guidelines, offer documentation and
provide other recommendations. Design, they describe proposed new features or functionality. A list of all proposed pbs
is available in a dynamic, continuously updated
document called Pbzero. Pap eight, the most well known
PEP is the style guide for Python code outlining
best practices for writing readable and
consistent Python code. I also want to note
that Python three is not backward compatible
with Python two. Python two has efficiently been discontinued and is
no longer supported. The Python community has fully transitioned
to Python three, which is now the standard
for all new development. In this course, we will
be learning Python three. When learning Python, one of the first thing to understand is its syntax or the
set of rules that define how Python code is
written and interpreted. I can note that Python syntax is known for its simplicity
and readability. And let's start from
the first indentation to define blocks of code. Indentation and Python
is a key part of the syntax and serves to define the structure
and flow of the code. Like many other
programming languages that use braces to
denote blocks of code, Python relies on indentation to indicate which statements
are part of the same block. We should know that indentation use four spaces per
indentation level. Here we can see as
an example function, the lines message and return message are indented
under the deaf block, making them part of
the grid function. The print statement
is outside of the function and thus not
part of the grid function. Mixing tabs and spaces for indentation within the same file can lead to indentation error. So we should use just one thing. However, in editors
like VS code, there are mechanisms that may prevent this error from
appearing immediately. Vis code often auto corrects
the indentation as you type. It might replace tabs with
spaces based on your settings, so you don't notice the issue unless you mix them explicitly. But you should understand that Python itself will still raise an indentation error if mixing occurs in the same block
of code during execution. Let's go over a few
new concepts variable, operation, operator, and
expression and concatenation. In Python, a variable is
an object implemented as a named memory location that
can hold various values. A variable name starts with
one or more Latin letters, and it can include
digits and underscores. Also, variable names in
Python are key sensitive, for example, name, name, and name are three
distinct variables. Variables are assigned
using the equals operator. It binds a variable
name to an object. An operation is an action that needs to be
performed on variables. It can be, for example, addition, division,
subtraction, et cetera. An operator is a
symbol or object that performs an operation and has
familiar symbolic notation. You can see examples
on the screen. An operand is an object,
for example, a number, string, or variable on which the operator
performs an operation. And we have expression
is a combination of operations performed by
operators on operans. Here we see example of expression where addition
is the operation, plus in the operator and age
and two are the operans. Congatenation is
the operation of joining together characters
or sets of characters. It can be done with
plus operator. And also, it can be done with
multiplication operator. As I said before,
web eight provides guidelines on how to write Python code in a clean
and readable way. And here we have some rules. Maximum line length limit all lines to a maximum
of 79 characters. Blank lines and
Python are used to make your code more
readable and organized. They are not strictly required
by the Python interpreter, but are helpful for
people reading your code. It helps divide your code into logical parts like separating function definitions
or separating chunks of code that
do different things. So we have basic rules
according to pap eight. Two blank lines between top level code like functions
or class definitions, it makes it easier
to see where one function or class ends
and another begins. And one blank line
inside functions or methods to separate
logical sections of code. Commons allow you to explain
what your code does, making it easier for
others and yourself, of course, to understand the
logic and purpose behind it. I pattern commands are not
executed by the interpreter, so they don't affect the
performance of your code. They are for human readers. We have single line commands
and multiline commands. Single line commands start
with the hash symbol. They are used to
add brief nodes or explanation about a specific
line or block of code. Multiline commands can be
created using triple odes. It's for longer
explanation to provide documentation for modules,
classes, and functions. We can use either double
quotes or single quotes. Both type of triple quotes
serve the same purpose. The choice between
them often comes down to personal or project
specific style preference. Clear and consistent names, help others and yourself, understand the
purpose of variables, functions, whatever
may be classes. So we have name convention. Naming convention,
use snake case for variables and functions
and Camo Ks for classes. And now that we
are familiar with the fundamental concepts and basic syntax of the language, let's begin exploring
Python objects.
6. Python Numeric Data Types: int, float, and complex: In Python, everything
is an object. Let's consider Python
building data types. The first one numeric types. These are include integer that
represents whole numbers. Flowed that represents
decimal numbers, and complex represents
complex numbers with real and imaginary parts. Let's take a look at an
example to see how this works. In Python, integers
are whole numbers that can be either positive,
negative, or zero. They are one of
the most basic and commonly used data
types in Python. We can check data type
exactly with type function. Here we have int. In many programming languages, integers are limited to a certain range depending on the size of the
memory located. In Python, we have integers
that can grow beyond those limits and are only constrained by
the available memory, not by any fixed size. This means that
Python can handle arbitrarily large integers
without causing an overflow. We also can easy performing arithmetic operations
with integers. Here we can use any
arithmetic operator. The only limiting factor is your machines available
memory, not Python itself. As we can see, this flexibility
makes Python especially useful for mathematical
operations involving very large numbers. It's very useful in applications where
precision is critical. It's a common task
in programming when you need to work with user input data and if it
was represented as string, you can convert it into
integers with It function. Now we can see that
we have string. We use function type, and here we are, it's a string. But we need integer, so we use function Int. And if we now check the type, we can see that we have integer. Of course, we are not
always can do this. When converting a string, really string to a number, you will usually see
an error message. Let's continue with float. The flow data type
in Python represents real numbers with a fractional
part or decimal point. It's used to handle
decimal numbers or numbers that require more
precision than integers. Here we also have positive
or negative floats. We also can notice that float numbers can be expressed
using scientific notation, which is useful for representing very large or small numbers. We also can perform all
basic arithmetic operations, multiplication,
division, and so on. Floats are precise to about
15 to 16 decimal places. But beyond that, rounding
errors can occur due to how floating point numbers
are stored in memory. Floating point numbers are
represented in a way that can sometimes lead to small
precision errors. I show what I mean. This happens because
floating point numbers are stored in binary form, and not all decimal fractions can be precisely
represented in binary. As a result, we
can see like this. When we work with
financial labs, it can lead to serious errors, so how we can solve it. We have decimal module
and pattern for this. It helps you to work with decimal numbers with
arbitrary precision, avoiding these binary
rounding errors. So if I really need precision, I should use decimal module. You may ask me why I'm
using quotes here. Because when you create a
decimal object from a float, Bthon first converts the float into approximate
binary representation, which may introduce
inaccuracies and using a string ensures that
decimal interprets the number exactly as you wrote it without the
intermediate step. This prevents surrounding
errors or precision issue. In case we have one
variable integer and another one is float. And if we divide an
variable by float, the result will be of
type of float in Python, when you perform
arithmetic operations involving different
numberc types. Python implicitly converts
the types to ensure the operation can be performed
with maximum precision. This is known as type coercion. The round function and
pattern is used to round the floating point number to a specified number
of decimal places. For example, we have
a result like this, and I want to get rid of the extra digit after
the decimal points. We use the function round
and put here first number. We want to round and the second argument,
and it's optional. We put the number
of decimal places to which you want to
round the number. If this parameter is omitted, the function rounds to
the nearest integer. Let's go on with
complex numbers, which consist of two parts, real part and an imaginary part. In Python, we have imaginary part is denoted
using the suffix J. In other programming language, you may so I is used
as imaginary part. So here we see example
where three is real part. And four J is imaginary part. We have several
building functions to work with complex numbers. And here we see example where I want to
show first real part, and then output as
imaginary part. On complex numbers,
we also can use arithmetic operations
like addition, subtraction and multiplication. Why and when we should use it. Well, complex numbers are essential for fields
like physics, engineering, and
signal processing. In the next lesson, we
will consider string, list, double, and range.
7. Primitive and Reference Data Types in Python: In Python, all objects
can be broadly categorized into reference
objects and atomic objects. Reference objects don't
directly hold the value, but instead reference or point
to other objects that do. They include lists, dictionaries and custom
objects related from classes. These objects can hold
multiple other objects, which may be of different types. Atomic objects are the basic. Indivisible objects that
hold a single value. These are the simplest
types of objects in Python, such as integers, floats,
strings, and booleans. They don't reference
other objects and are the building
blocks of them. When assigning atomic objects, their value is copied, whereas for reference objects, only the pointer to
the object is copied. As a result, both variables after assigning refer
to the same value. Let's take a look at an
example to see how this works. For example, I created
atomic object, integer, and here I assigned my
object to other value, then I printed, then I change
the value of A, let B six. And if we print both, we can see the difference. When you assign an atomic
object like an integer, a copy of the value is made. Changes to the original variable don't affect the new variable. Let's consider reference object. I created reference
object, list. Then I create Y variable, and it was assigned
the reference to the list that
we create earlier. I print all two variables. Then I modify list. I quickly explain in
Python list are ordered collections that store multiple items in
a single variable. The first element of the list is assigned using the index zero, the second element with
the index one, and so on. I took the first one element
and assigned it to ten. If I print both lists, we can see the same result. When we modify list through X, the change is
reflected in Y as well because both X and Y point
to the same list in memory. So when you assign a
reference object like a list, both variables refer
to the same object. Changes to the object through one variable affect the
other variable as well, and you should be careful
and always remember it. Reference objects can be
mutable or immutable. Mutable object can
be changed in place. Modifications don't require
creating a new object. This means you can change, add or remove elements or values without
creating a new object, and of course, changes
to immutable object will be reflected across all
references to that object. We saw this very clearly
in the example of earlier when we used list
and changed the first value, lists, dictionaries, sets, and user defined objects when
attribute can be changed. These are all mutable objects. Immutable objects cannot be modified after they are created. If you want to change
their content, it will result in
the creation of a new object instead of
modifying the existing one. We have integer, floyd, complex numbers, tapo,
string, and bytes. As an example, I created immutable object like
Taple Quick explanation. A tapo and Python is an ordered immutable
collection of elements. We will explore
all data types in more detail later
in this course. Now I've taken the element from the Taple with Index zero, and this element
is the number one, and I'm trying to replace the number one with
the number ten. If I try modify this tuple,
I will get an error. I can only create new one
with different contents, but not always
you've got an error. Sometimes it may seem like you have changed the
object, but you didn't. For example, I create
immutable object, string, and then I will
make it uppercase letters at first glance, it's the same, but it's not. Upper method doesn't modify the original string A because
string are immutable, as we remember in Python. Instead, it returns a
string in upper case. We can check it with ID. We print original ID, and then we call
upper method and then print ID after per method. So we can see that these two A variables totally
different variables. The ID function returns
the unique identifier, memory address of an object. The memory addresses of A in the first case and A in the
second case are not equal. It confirms that they
are not the same object, even though they
have the same value
8. Working with Strings in Python: Welcome back. We
continue with datatype, and now we will
consider sequence type. And here we have string represents a sequence
of characters. Last represents an ordered
beautiful collection of items. Tapal represents an
ordered immutable, as we remembered
collection of items. We also have range as a
sequence type in Python. It represents an
immutable sequence of numbers and used for loops. So let's start from
string data type. As I said before, string is a sequence of characters
enclosed in quotes. It can be letters, numbers, symbols, and spaces. This is the most commonly
used datatype in programming. The most common way
to define string is by using single
or double bodes. We already saw in previous
lessons how we can do this. Both single and double quotes work the same way
for most strings. Triple codes are usually
used for multi line strings. And as we remember, string is immutable data type. This means that if
you create a string, you cannot change its content. The only thing you can do it reassigning the
variable to a new string, but the original string
remains unchanged. And we clearly saw it
in the previous lesson where we learn mutable
and immutable data types. So each character in a string has a position
called an index, which starts from zero. You can access
individual characters using these index numbers. We can take the first element, let it be N. Let's take
N or let's take K, but not only positive indexing, we also have negative indexing. We can take the last character
using the index minus one or the second
to last character, and we use index
minus two and so on. We also can use slicing. We can extract a
part of a string. We use the syntax
where we have start is the index you start from and stop the index where
the slice ends, but it's not included
in the result. If you want to start from the
beginning or go to the end, you can omit the start
or stop indices. If you try to access an index in string that doesn't exist, Python will raise
an index error. It indicates that the
index is out of range. But if you are using slicing, Python is more forgiving. Let's say like this. It won't raise an error. Instead, it will return as much of the
string as possible. Or an empty string if the slices is entirely
out of range. So let's or a bit with these
methods of this data type. We can change the case
of the string with these two methods,
lower and upper. The first converts
all characters to lowercase and upper method converts all characters
to uppercase. To remove white
spaces from a string at the beginning and
trailing space at the end, we can use strip method. We also can remove
specific characters by passing them as an argument. For example, here, I want
to remove exclamation mark. The replace method allows you to replace parts of a string
with another string. This is very handy when you need to modify specific
parts of string. For example, here, I
replaced Java with Python. Let's imagine we need to find index of the first
occurrence of the substring. And for this, we have two
methods, find and index. They are both used to find the position of
substring in a string. The first find returns the index of the
first occurrence of the substring or minus one if
the substring is not found. The index does the same but raises an error if the
substring is not found. If we need to split
a string into parts or join a list of strings
back into a single string, we can use split or
joined functions. Split function
breaks a string into a list based on a delimiter. We can choose whatever we want. Default is space, but we also can specify a
different delimiter. For example, here, I choose
coma I use join method, it takes a list of
strings and join them into a single string with
a specified delimiter. We also can choose it. And here we got string instead of list. If we need to
check, for example, file extensions or URLs
or other common patterns, we can use starts with
and end with methods. We check if a string starts or ends with a specific substrings. We have true if it really starts and falls if it's not true. And yes, we don't start
our sentence from Java. And in this case, we check the string ends
with a specific suffix. As you can see, an exact
match is required. So you need to add
an exclamation mark. If we need to know
how many characters are using in a string, for example, we can
use a an method. We can use it to
validate user input. Let's go on with
string formatting. We have three main methods
to format strings. The old style using percent, the newer format method, and the most modern approach, Fstring introduced in Python 36. Let's start with the old style, which uses the percent operator. In this example, person S is
a placeholder for a string, and person D is for an integer. We have two variable
position and salary. One variable is a string, and the second one it's integer. So I want to create message where I will
use these two variable. The result will be a
formatted message like this. It's very useful. We can
change the variable, and our message will be
changed automatically. Let's proceed with
new style formatting. Here we have the format method. It was introduced
in Python three. This method is more flexible and readable than old
style formatting. In this example, the Carl
braces are placeholders for the values and the format
method fills them in. It's more flexible and readable than using
person symbols. Finally, we have F string, which were introduced
in Python 3.6. This is the most modern
and recommended way to format strings
because it's cleaner, faster and easier to read. F strings allow you to directly embed expressions
inside Carly braces. There is no need for any
additional methods calls. Just use the F prefix,
and you're good to go. This method is faster
and often more readable, especially for
complex expressions. When you use F string
or format method, you can add formatting
types inside the placeholders to control
how the values are displayed. This is useful for specifying things like the number
of decimal places, pudding, alignment, and so on. For example, here we
have a left aligns, the name string within a
field of ten characters. And here we have right aligns the score integer within a
field of three characters. And of course, we can change it. We also can use F. It means format as a
floating point number with two decimal places. And of course, you
can change it. If we use it with comma, we have thousand separators. These formatting options help to make your output more
readable and better. We also have escape
characters in string. They are used to include
special characters in strings that would otherwise be difficult to
include directly. Using escape characters
can help us add new line tab backslash. And we also can use single
and double codes inside the string. So we got like this. We also can repeat a string a specified number of times
using the Asterix operator. I will add a space to
make it more readable. And of course, concatenation, we saw it in the
previous lesson. See you in the next lesson.
9. Understanding the List Data Type in Python: So let's continue
learn sequence types, and let's go on with list. List, it's a collection of items that can store multiple values in
a single variable. As a data type, lists
are ordered, mutable. Can be changed and
allow duplicate values. They are defined
using square brackets and can hold elements of
any datatype like numbers, string, other lists,
or even mixed types. The elements in the list
have a defined order. After creation list, you can
modify the elements inside, and as I said, in one value, you can store element of
different data types. You can access
elements of a list using their index,
starting from zero. For example, here, I get element with Index
two from List one and element with Index two from List two or let it be
element with Index one. The first two lists we
made with square brackets. You can directly assign the
elements inside the brackets, separating them by comas. But we can also create
lists like this. We can use the function
list to create a new list, the function list and the
argument string Hello. Converts the string hello
into the list by treating each character in the string as a separate
element of the list, and at the end, we will get hello consists
of five characters. The function list takes
each character and creates a list where
each character is an individual element. We also can create list
with function range. The range function generates
a sequence of numbers, starting from zero up
to but not including, let it be six in our case. Then by converting it into
list using List function, we get a list of numbers 0-5 we also can
generate a sequence of numbers starting from minus two and going up to by
not including five. Again, a list function converts this range object into a list, and we have this result. By default, step is one, meaning it increments
by one at each step. But we also can set it. For example, let's
create another one list. Here I use the function range. That generates a sequence
starting from -30 up to 50, incrementing by
ten at each step. So we have number -30 is the starting point and the second number 50
is the end point, but 50 itself is not included. And the third number ten
specifies the step size, meaning the sequence
will increment by ten. In Python, when you use plus
operator with two lists, it concatenates them, meaning it joins them together
into a single list. We saw something similar when we learned string data type. When you multiply a
list by an integer, in our case, let it be two. It repeats elements on the
list two times in our case. The original list
is not modified. Instead, U list is created
with the separated elements, and of course, we can combine
it with concatenation. If we want to add an element
to the end of the list, we can use the method append. So in our case,
the append method adds the value 33 at
the end of the list. The append method modifies
the original list directly. It doesn't return a U list. It updates the existing one. We want to delete
the element with defined index in the list, we can use the statement Dell. It modifies the list in place by removing the item on
the specified index. In our case, we delete an
element with index three. In the case when we want to insert an element in a
specific position or index, in a list, we can
use insert function. First argument is
the index where the new element should be inserted and the
second argument, let it be happy in the element that will be
inserted at that index. In case when we want to remove the first occurrence of
specified value from list, we can use method remove so
I decided to remove happy, so I put it as an
argument, and here we are. Let's also remove 4.23. What's the difference
between Dell and remove? Remove method is used to remove
an element by its value. It searches for the
first occurrence of the specified value and
removes it from the list. If the value is not found, it raises a value error. The Dell statement is used
to remove an element by its index or to delete
a variable entirely. If we specify no index, it will delete the entire list. We also have pop method. It is to remove and return
an element from a list. By default, it removes and returns the last
element of the list, but you can also
specify an index to remove and return a
particular element. So in our case, we
remove and return the last element in
our list. This is 33. As you can see, I assign this 33 to a variable just to
print it to be clearer. Then I'm going to delete
the element with index two. In our case, it will be hello. If I print the list now, we clearly see that we
remove hello from our list. Let me remove necessary thing, and let's print list six. We also can use here
slicing technique. For example, here,
I want to get slice from the second
index till the four, but of course, four won't
include in our result. It returns a new list containing the elements
starting from the Index two and ending at the index
three or let it be six. And we get the list,
start from Index two and ends with Index five. In this example, slice extracts elements from the beginning
to the list up to, but not including Index six. If the list has fewer
than six elements, it returns the entire list. This example will show
us that slice starts at Index two and includes all elements
to the end of the list. We also have len function. In Python, it used to determine
the number of items in an object as we can
see, it returns eight. It means that the list
contains eight elements. Let me print the list again, and I'm going to
use reverse method. It used to reverse the
elements of a list in place. This means that the order of the elements in the
original list is reversed and the
list is modified directly without creating
a new list. So be careful. We also can sort the elements of a list in place
using method sort. The method arranges
the elements of the list in ascending
order by default. This is not very good example. Let me replace it
with at least one. And make some
adjustments for clarity. So here we can clearly see
that our list was sorted. But if we set reverse
equals to true, the list is sorted
in descending order. The default value of reverse, of course, falls,
and we can omit it. In case when we want to remove all elements from our list, we can use the method clear. This method removes all
elements from the list, resulting in a length of zero. This method doesn't
return any value. It performs the clearing
operation on the list itself. See you in
the next lesson.
10. Exploring the Tuple Data Type in Python: Welcome back. Let's continue
to learn Python data types. And now we will look at Taples. Taple is a collection of ordered immutable
elements in Python. It's similar to List, but unlike List, it cannot
be changed after creation. And here we use parenthesis. When we create a taple, the elements in a table maintain the order in which
they were defined. And once created, the elements in a taple cannot be modified. Tapos can store
different datatypes. For example, here,
I create a Taple where we have
integer and string. Also, the elements
can be repeated. We can check that we have
tapo with type function, but I want to
notice that we also can create a Taple
without parenthesis. I remove parenthesis to show you this is known
as Taple packing. When you provide multiple
values separated by commas, Python automatically
groups them into a taple. So we have the same result as if we would use parenthesis. We can also create a taple with only one element when creating a taple with
only one element, you have to indicate
a comma after that element to ensure Python
recognizes it as a taple. Without the comma,
Python interprets it as a regular value inside
parenthesis, not a tuple. If I remove the comma, you will see that the
type was changed. And as we can see, we have just usual integer. But if I add comma, we have a table again. I will return everything as it was and remove
unnecessary thing. The elements in a
table can be accessed by using indices
just like lists. Let's take, for example, the element with index two. And here we also have slicing. We learn it in the
previous lesson. We have start the index where
the slice starts inclusive, and the index where the
slice ends exclusive. In the first example,
I got slice. I extract all elements from Index two up to the end of the taple this slice extracts all elements from the beginning of the tuple up to Index two, but not including the index two. Here we have slice that extracts elements
starting from Index one, up to Index four, but does not include the
element at index four. It means start from Index
one and stop at Index four. Here we have also
negative indexes to access elements from
the end of the tauple. As I said before,
tuples are immutable, meaning that you
cannot change at or remove elements after
the tuple is created. For example, here, I try to change the element
with index zero, and I've got an error. I create new tapo. I wanted to show you how we can concatenate two
or more taples. We can combine the elements
of the taples into a single taple using
the plus operator, and this is concatenation, like we did with lists. After this, we have a new taple. The order of elements in the concatenated
tapo is the same as they appear in
the original taples. You can repeat the elements of a tapo using the
multiplication operator. For example, here, I took tale B and use the
multiplication operator and repeat it three times. In Python, you can check
whether a specific element is present in a topple by
using the in operator. This is called
membership testing. The in operator returns a
boolean value either true if the element exists in the
tuple or false if it doesn't. So we can clearly see that
two exists in Taple B, but if I choose
22, we got false. If we need to return
the number of times a specified value
appears in the tuple, we can use method count. So I tried to count the occurrences of value
three in the topple B, and I got zero because I
don't have three at all. But if I count the
occurrences of the value two, I've got four, which is true. I will increase the topple a little just to show
you another example. To find the index on the first occurrence
of a specified value, we use the method index. For example, I wanted to know the index of five
that we just added. So I got the index, it's four. But if I try to find
out the index of elements that doesn't exist
at all, we got an error. To avoid handling the
value error expression, first, check the number of elements using the count method. If it's greater than zero, then calculate the position of elements using the index method. So I checked the
number of elements. We got one, and now I can check the index because I'm
sure that this number exists. For example, we can
use ILs condition. We will take a closer look at this construction a bit later, but I will explain briefly
the ILS construction is a control flow statement that
allows your program to make decisions and execute
certain blocks of code based on the conditions. The first condition we check if the count of value
greater than zero, and if it's true, we print the index. The s block is optional, but it will be executed if the condition in the
If statement is false. So I switch five to
15 to get false, and I will add s block. And inside, I print the message. So here we can clearly see
that the condition returns false in the first
block of code and the code inside the
IV block is skipped. That's why we had the message from the second block of code. Also can check if the
value five is present in the tapo using
this construction. So if the value is found, which it is, the
code returns true. So the first block
of code returns true and we can get its index. But if I indicate the wrong
number, we got the message. But guys, I will draw your
attention to the fact that directly
writing a number in the condition block
is not good practice. The correct approach is
to assign this number to a variable and then
work with the variable. I'll remove this
hard coded style and create a variable instead. Using a variable like
item in our case, instead of hard coding, makes your code more flexible. As you can see, I can
change the value of the variable without touching the actual AFLs code structure. Let's take a look
at the unpacking. Taple unpacking allows you to assign elements of a taple
to multiple variables. You can pack and unpack taples without needing extra syntax, which make the code
cleaner and more concise. Here I unpack the taple into individual variables
and print them. Our tapo is defined
with five elements. It contains four
integers and a string. I assign the values of the tapple to these
bunch of variables, and this works because the
number of variables on the left side matches the number of elements
in the taple. But what happens if there are too few or too many
variables if the number of variables on the left side doesn't match the number
of elements in the double, Python will raise an error. Here we have a value error, too many values to unpack. This is because table
unpacking requires the number of variables and elements
to be exactly the same. If you want to unpack some
elements, but not all, you can use the Asterix operator to capture the remaining values. This will assign the
one to B variable. Hello, we assign to R variable, and the rest of the
elements will be stored in E variable as a list. If I print the E variable, we will see this list. We can get the
number of elements in the taple using
length function. I create the variable B and assign the number of
elements to this variable, and of course, then I print it. So we have five elements in the Taple See you
in the next lesson.
11. Understanding the Boolean Data Type in Python: Welcome back. Et's look
at Boolean data types. In Python, booleans are represented by the
keywords true and false. True represents a logical
yes or positive condition, and false represents a logical
no or negative condition. I'll give you examples with expressions that evaluate
to either true or false, so you can get the point. But first, let's consider
boolean operators. Comparison operators
used to compare values and return true or false. Here we can see that four
equals to four, which is true. Then in this example, we can see that three
not equals to four, which is also true, and then five more than
one, and it's true. Then we have logical operators where we combine
Boolean expressions. These operators are essential for controlling the
flow of programs. We have three boolean operators, logical and logical
or and logical note. The end operator returns true
if both operands are true. If any of these operands is
false, the result is false. We can see the table where
we have two condition, and if two condition true, we got true result. So we have the same with false. If two conditions are false, we have false result. We already familiar
with I block of code, so I will use it
in this example. We have first condition if a more than zero and
be more than zero. In our case, it's true because
both numbers are positive. As a result, the whole
expression evaluates to true, and we got the message, both numbers are positive. In case the first
condition is false, Paton doesn't check
the second condition because we'll get the
false value anyway. Then we have logical or
the or operator returns true if at least one of
the operands is true. If both conditions are
false, the result is false. So in this example, we have that all expressions evaluate to true because at
least one number is positive. And here if the first
condition is true, Biden does evaluate
the second condition because we will get true anyway. And then we have logical not. The nod operator inverts the Boolean value
of the operand. It returns true if the
operand is false and false, the operand is true, the nod operator
is often used to negate condition
in A statements. For example, here we
have A equals to ten, and we try to check if
not A equals to zero, so it's true, A is not zero. We also can combine
logical operators. You can combine multiple
logical operators to form complex conditions. We use parentheses to group conditions and ensure the correct order of evaluation. So in the first case, we have true because both
A and B are positive. In the second condition, if we check whether the C not
equals to 15, we got false. Because actually C equals to 15. Since the or operator is used, the entire expression values
to true. Let's check it. Here we are we got true because as we can
see from the table, true or false is true. Let's consider order of
precedents for logical operators. Here you can see from
highest to lowest. In Python, when multiple
logical operators are used together
in an expression, the order of
precedents determines which operators are
evaluated first. This is essential
because it affects how the overall expression is interpreted and
its final outcome. Knot has the highest precedents
among logical operators. This means that when you
use not in an expression, Python will evaluate
the not operator before anything else. Then we have second
highest precedence, operator and has the
second highest precedence. This means that in an
expression with both, for example, and or operators, but then evaluates
the expression first, and the operator or has
the lowest precedences. Here we have example.
Naught is evaluated first. B and C is evaluated next. Here false and true
becomes false. And finally, false or false is evaluated
resulting in false. See you in the next lesson.
12. Working with the Dictionary Data Type in Python: Welcome back. Let's go
on with dictionary. Dictionary and Python is an ordered mutable
collection of items. Each item is a key value pair where keys must be
unique and immutable, and values can be any data type. We can create a dictionary by enclosing the key value
pairs in CRL braces. Each pair is
separated by a comma, and the key and value within each pair are
separated by column. Here we have keys, name age, and city and the values Alice, 30, and New York. Also, we can create a dictionary
using the Dict function. This is especially useful when you want to create
a dictionary with more complex data types or
when the key are not strings. Here we have a dictionary
using the dict constructor. You can start with
an empty dictionary and add Key value Pars later. For example, here, I
created empty dictionary, and then I'm adding Key
value Pars one by one. And if I print the result, we can see that we
filled this dictionary. The most straightforward
way to access a value in a dictionary is by using its
key within square brackets. In this example, I retrieve the value associated
with the key name. But we also have G method. It allows you to access
dictionary values, providing a way to handle situation where a
key might not exist. This method returns known
if the key is not found, avoiding a key error. If I use for these keys, square brackets,
we got the error. If you want to retrieve all keys or values
in a dictionary, you can use the keys
and values methods. Here we can see that the
keys method returns all keys and the method values returns view object displaying
a list of all values. As I said before, dictionary
in Python is immutable. This means you can change the contents of a dictionary
after its creation. You can add remove
or update values, how to add new values we saw before when I filled
totally empty dictionary. Now I want to show you how to add multiple
key value pers. For this, I will use
the update method. This method takes another dictionary or iterable
of key value pers, and we update our dictionary
with this new data, and here we can see you
can also update the value associated with an
existing key by simply assigning a new
value to that key. For example, here, I updating
an existing value age, and I change it to 31. Now let's go on with deletion. The Dell statement
allows you to delete a specific key value purse from a dictionary by
referencing the key. For example, here, I remove
a key value pair age. So we have the
updated dictionary. If I try to delete a key that doesn't exist, I got an error. We also have the pop method. It removes the key value pair by key and returns the value
associated with the key. So here I use the pop method. I specified the key city. I printed the value
that I removed. And then our dictionary. And here we can see
that I removed Chicago. If the key doesn't exist, you can specify default value to return instead of
raising an error. If I try to pop a key
that doesn't exist, I can avoid an error by
providing default value. For example, here I provide
the default value, not found. I don't have such keys, so I've got not found output. The pop item method
removes and return the last inserted Kevalu
pair from the dictionary. This method is
particularly useful when you need to clear the
dictionary incrementally. Here I add the new item. I call it last item, and then I will remove
it with pop item method. I created the new
variable, last item, and assigned my dictionary where I removed the last
item with pop item method. Then I print removed item and
my dictionary after this. So we clearly see that we
removed the last added item. If you want to remove all
items from dictionary, you can use the clear method. This method empties
the dictionary but keeps it in memory, and you can use it
for further work. After calling clear method, the dictionary will be empty, but it still exists
and we can use it. The items method in
Python is used to return a view object that
displays a list of dictionaries Key
value pairs as tuples. If you need to
store or manipulate the key value pairs
outside the dictionary, you can use the list function. If I check that type, here I've got dict items. But after list
function, I got list. It converts our dictionary into a list of key value pairs. I print that I've got, and we see list. The items in a dictionary
don't have a defined order in Python 3.7 and
later dictionaries maintain the insertion order, but they are still considered
unordered collections. Each key in dictionary
must be unique. If you use a key
that already exists, the new value will override
the previous value. We can check the
presence of key in a dictionary with operator. This returns true if the key exists in the
dictionary and false otherwise. In the first example, we have true and we print
the key exists, and in the second example, we have the false we also can check the
value in a dictionary, and we do the same way. But with the values method, it can help you to
avoid overwriting existing values in a dictionary before adding a key value pair. So you can check whether the
key or value already exists. If you need to sort the
dictionary by its keys, you can use the sorted function. It returns a list
of sorted keys, which you can then use to
reorder the dictionary. See you in the next lesson.
13. Exploring Set and Frozenset Data Types in Python: Back. We often have
the situations where we need to handle groups of unique elements efficiently. And this is when sets and
frozen sets come in handy. Set and Python is built
in data type that allows you to store a collection of unordered and
unique elements. It's very useful for tasks such as eliminating
duplicates from data or performing
mathematical operations like union and intersection. It's mutable elements
can be added or removed, and we also can create
it by several ways. The first case, we create
it with Carla braces. And the second, we create empty set with the
set constructor, if I want to add a
single element to a set, I can use add function. For example, let's
fill this empty set. The elements and set don't have the defined
position or index. This means that
you cannot access items like we did
with list or tuples. And as I said before, set has unique elements. Set automatically eliminates
duplicate entries. If you try to add an element that is already
present in the set, it will simply be ignored. I add another name to make
our set a little bit bigger. I wanted to show you
how we can remove specific element and we can use it with the function remove. Here we can see that I
removed the name Nick. If the element is not
found, it raises an error. But if we want to
avoid the key error, we can use the function discard. And here we can see if I remove an existed
object, we got nothing. But if I remove name Nik, we have the same result as
we had with remove function. We also can use the method clear to remove all
elements from the set. Let's consider such
mathematical operations like union, intersection
and difference. For this, I created two sets, and in the first case, we combine elements from two
sets, excluding duplicates. As we can see, three
was duplicated, but here we don't see it. We have totally unique elements. Okay, let's consider another
example, intersection. I retrieve element
common to both sets, and here we have three. Let's get elements present in one set, but not in another. Here I use the minus. And we got one and two, which is true because we have
one and two in first set, but we don't have
it in the second. And let's consider
symmetric difference. I retrieve elements that are in either of the sets
but not in both. So here we have unique elements, but as we can see without three, because the three were
present in both sets. You can also check if an element exists in a set using
the in operator. For example, here I have set and in block of
code with If statement, I check if the hello
string exists in my set. And if it's true, we
got this message. Let's go. And here we
have a frozen set. It's immutable version of set. But despite this immutability, frozen sets still support many of the same operations as sets. We can create frozen sets using the frozen
set constructor, and I can pass any
terable objects like staple or another set. For example, let's create
frozen set from list. Here I have simple list, and then I create
with the constructor frozen set and pass
this variable, my list. And here I printed
our frozen set that I've got from the list. I can also create new
frozen set from set. So first, I create simple set, and then I pause
it in frozen set. I also can create
frozen set from string. Since string is iterable, you can create it easily. As I said before, frozen
set is immutable, so we cannot apply the methods
like add remove or clear. Like sets, frozen
sets are unordered. We don't have specific
order of elements, and I can't take specific elements from here like we did with list or double, but elements are unique, so duplicates are automatically eliminated when
creating a frozen set. As you can see,
here we have two, but when at my frozen set, I have just one L. But we can
also check if an element is present in a frozen
set using the in or not in operators like
we did with set. For example, here I checked if H letter was present in my
frozen set and we got true. I quickly show you
frozen set operations and I start from Union. I have two frozen
sets and after Union, I have frozen set that contains all unique elements
from both frozen sets. Then I use intersection. For this, I add elements
to both frozen sets. The intersection of two
frozen sets contains only the elements that are
present in both frozen sets. The difference variation
returns the elements that are present in
the first frozen set, but not in the second. With a symmetric difference, we have the result where we have elements that are in either
one set or the other, but not in both we have unique elements
one and two from the first frozen set and
the unique elements five, six from the second frozen set. Then I'm going to check
if all elements on the first frozen set
are contained in the second frozen set
and we have false. I slightly change my first
frozen set just a bit. Now I'm going to check if frozen set two contains all elements from
the frozen set one. And eventually we have Dre. If I have to check
that two frozen sets have no elements in common. I can use the ISDs joint method. If I check right now, we got false because it's obvious
that we have common elements. I slightly change
my frozen sets. And in this case, we got true. So we considered two data
types set and frozen set, set are mutable collection
of unique elements, frozen set, and immutable
collection of unique elements. If we are talking
about performance, set slightly slower for
hashing because it's mutable, frozen set a little bit
faster due to immutability. See you in the next lesson.
14. Working with Binary Sequence Types in Python: Welcome back. Guys,
you are awesome. You've done so
amazing job so far. Keep up the great work. The most exciting
challenges are still ahead. Let's keep going. Today we will cover the last
of the built in data types and then move on to loops and
conditional statements. Well, binary sequence types. These are specialized
data structures designed for handling
binary data. They are essential for
working with files, interacting with hardware or performing network
communications, and they start with bytes. The Bytes type and Python represents an immutable
sequence of bytes, with each byte being an
integer value 0-255. A bytes object is
similar to a string, but is used for binary
data than text. And as I said, it's immutable. So once you create it, its content cannot be changed. There are several ways
to create bytes object. The first way using the B, single quotes or
double quote notation. This syntax is the most common
for creating bytes object, especially when working with
text that has been encoded. The second way, we can create the Bytes object using
the Bytes constructor, and I pass here
the string hello, but I also have to specify the argument encoding UTF eight. The bytes function can create a bytes object from
various types of data. But when we use
it with a string, we have to specify encoding so Python knows how to convert
each character into binary. And UTF eight encoding is one of the most common ways
to encode characters. I'll show you how it works. If the string contains
special characters, UTF eight would encode the
symbol as multiple bytes. In my case, it's
represented by two bytes, and other characters follow
in a similar manner. Computers store data
in binary format, so any text or
characters needs to be encoded into bytes to be
stored or processed in memory, and the UTFAight is a versatile encoding standard
compatible with ASCE. What is aski? This is American Standard code for
information interchange. We have a character encoding standard that was
originally developed to represent English characters in computers and other devices. So if the string contains
only ASCI characters, as I said, English
letters, numbers, the bite representation will look very similar to
the original text with B. Prefix to indicate
that it's now bytes. But when the string contains characters outside the
standard ASCI range, those characters will
be represented by their corresponding UTF
eight byte sequences. And here we can clearly see it. So in this way, the code
provides a way to bridge the gap between
human readable text and machine readable
binary data. ASCI plays an important role
in email communication. Email protocols use ASCI text to communicate between
email servers. This also the basis
for many file formats. Many programming languages also use ASCI for source code files. It's often used to manipulate
strings and programming, particularly for basic
string processing tasks. ASCI Values make it
easier to compare, sort and perform
arithmetic on characters, and it's also widely used in embedded systems for
communication and control. I hope I made it clear for you. We also can create bits
object using integer. When you pass a single integer
to the Bytes constructor, it creates a Bytes object filled with null bytes and
the specified length. In this example, five produces a sequence
of five null bytes. It looks like this. This is often used in low
level programming. If we need binary
representation of list, we also can make it. When I call this constructor, Python processes each
integer in the list and we got the integer one
becomes the byte like this, the integer two becomes the
byte like this, and so on. Eventually, we have
the final object, that is a sequence of bytes. That is immutable, and it means that you cannot change
individual bytes in this object. Despite the fact that we had a list that could have
been changed initially. ASCI characters are
numbered 0-127, and each character
corresponds to a unique number in this range. We can see it here on
the side in the table, the first 31 characters and the 127th character unprintable. These are control characters. They are not represent
written symbols, but we have printable
characters, and these represent
letters, numbers, punctuation marks, and symbols that you can
see and use in text. Let me show you. When you call Bytes Constructor and
pass here list like this, Python takes these
numbers and creates a Bytes object that represents
a sequence of bytes. If the values in the list
are in the range 0-127, Bython interprets
them as ASCI codes, and as a result for numbers
in the range 32-126, as I said before, these numbers correspond to printable
ASCI characters. We will see on the screen that Python shows them as characters. And then if I print the result, you can see the string
representation hello. We converted the integers into their corresponding
characters according to the ASCI standard. Numbers outside the range of ASCI table will be represented
as hexadecimal codes. We can check it, and
we can clearly see that 72 corresponds to H, 101 corresponds to E and so on. Understanding and working
with Bytes and Python is crucial for various applications programming and
data manipulation. Who knows where you
will use Python? It can be useful
for file handling or cryptography and security, or you can use it for image and audio processing or
network programming. So let's consider some
methods for Bytes. To convert Bytes back into
the original list of numbers, you can use the list function
on the Bytes object. This function will
return a list of the integers that were used to create bites in
the first place. We can obtain a slice from
a Bytes object in Python. For example, here we have Bytes object from
the string hello. For example, I want to take
the bite at index one. When you access a specific
byte by its index, Python returns the corresponding integer value of the byte, which is its ASCI code, and in our case 101 corresponds to E. But when I got slice
of the Bytes object, as a result, I have
another Bytes object, which is displayed
with the B prefix. Slices of Bytes objects are widely used in programming
for various purposes. For example, we may
need to extract specific sections of
the data for analysis. Or, for example, if we are
processing image files, we may want to
extract the header or specific bite sequences that represents certain
properties of the image. Bytes support most
string methods. However, some string functions don't work correctly
with the bite objects. For example, the LN
function returns the number of bytes that strings
occupies in memory, not the number of characters as we could see when we
worked with string. For example, here I
have Bytes object, that will be Hello which
consists of five Ask characters. Each character in hello is represented by a single
byte in UTF eight. So I got five. But since the characters
are non asci, they will take more
than one bytes each. In UTF eight, each
of the character in our new data is represented
by multiple bytes. So we got the total length of the bite representation,
12 bytes. If we need to transform it back into human
readable strings, we can use decode method, and of course, specifying
UTF eight, again, indicates that we
want to convert the bytes back to a string
using the Sam encoding. So let's continue with Bt array. It's a built in data type that represents immutable
sequence of bytes. Each element in bitary
is an integer ranging 0-255 representing
a byte of data. Unlike the bites type,
which is immutable, bitary allows you to modify
individual bytes and adjust the length of the bite sequence
after it's created. Let's create an empty bitary
object with no elements, and we can do this
with such constructor. If I print it, we can see empty Bary we also can
create Bary from string. In this example, I created
a bite array by encoding a string in a specified
encoding format, UTF eight. A Bary can be created from various sources
and it's mutable. Here I'm creating a bitar array from a list of integers where each integer represents
a byte value in the range of 0-255, as I said before, each number corresponds to the ASCI
code point of character. We can create Bt array from
an existing Bytes object. Since Bt array is mutable
and Bytes is immutable, this allows you to create a modifiable version
of Bytes object. This approach is
very useful when you receive data from
the form of bytes, but you need to
modify its content. Speaking about
modifying content, you can access or modify
individual bytes by their index, just like with lists. You can also extract a range of bytes using slicing syntax. So we are dealing with
mutable data type, so we can add a single byte
to the end of the byte array. And for this, I use
the method append. And of course, byte to append should be an integer
in range 0-255. In my case, 33 is the ASCI
code for exclamation mark. In case to add multiple
bytes at once, we can use Extend method. And here we added world. We can concatenate bitary with another bitary or bytes
using the plus operator. It's very similar like
we did with string. Let's continue with
deleting and popping. You can remove bytes by
index using the dell keod. This deletes the bite at
the specific position. And in my case, I
delete the byte with Index two or let it
be the index one. To remove and turn the last byte or bite
at specific position, I use pop method
and we already know that 111 corresponds
to O in ASCI table. We can also specify an index. In this case, we
remove Android turn and the bite with index 101. To get the index of the first occurrence of a byte sequence
within a byte array, I can use the find method. Here we got starting index
of the world and date six. If you need to convert the Bt array back to an
immutable bytes object, we can use a Bytes constructor. For example, here
I have Bt array, but then I decided to make
it an immutable data. So I use the constructor bytes. And here we are when you've
been working with bytes, but then you want to display or further process
the data as a text, you can decode it and you
use for this decode method. This converts the bitary
back into a string. Here's the table that outlined the key differences between
bytes and Bt array in Python. We use bytes when you want to work with data that
should not change. And we use Byte array when we need a mutable
sequence of bites, that can be updated or modified. For now, that's all. See
you in the next lesson.
15. Working with Loops and Conditions in Python: We've already encountered
with this operator before. Now let's take a
closer look at it. The I operator in Python
selects actions to be executed during the
program's operation depending on conditions. If the result of the first
condition check is true, then the first block
of code is executed. Otherwise, the second
condition is checked, and if it returns true, then block two is executed, and so on until a condition
that returns true is found. Or the else condition
is reached. If else is absent and all
condition checks return false, then none of the branching
blocks will be executed. The code block following the
If statement is mandatory. If it's necessary
to indicate that nothing should be
executed in that block, the pass statement is used. This is placed where
the command should be, but it performs no actions. In Python, you can use a shorthand for the
statement called the ternary conditional
operator or conditional expression to write an eval statement
in a single line. This shorthand is
particularly useful for simple conditional
assignments or operations. Okay, let's try. Imagine we want to check
if a number is even or odd and assign a result
based on that condition. The expression is used to
check if a number is even. The modulus operator returns
the remainder of a division. If the remainder of a division equals to zero, so we got true, then the number is even, or if it's false, then the number is odd. Using the ternary operator,
it looks like this. We have expression
at the condition, and if the condition is true, we have even, and it's
assigned to result. These two expressions
are absolutely equivalent and produce
the same result. But if the condition
is false, we got odd. Let's look at for
loop and Python. This is a control flow
statement used to iterate ovary sequence like a
Les Apple dictionary or string or even range. It executes a block of code for each item in the sequence. It's commonly used when you
need to perform an action a specific number of times and want to process each
item in a collection. The four loop and python takes
each item from a sequence one by one and assigns
it to loop variable. The indented block
of code following the column is executed once for each item
in the sequence. So let's start with a simple
example of four loop. I created list,
called it numbers. Then I created four loop, and I iterated through numbers
and print each number. So the loop iterates
over the list. For each item in the list, the loop variable
number takes on its value and then
print statement, output the value of
number in each iteration. We can do the same thing,
but I will use range. Range function is
often used with four loops to generate
a sequence of numbers. It's useful when you want to loop a specific number of times. This function has three
parameters, start, stop, and step, but we can
pass only one argument. In my example, it will be five. So the range generates number from zero up to but
not including five. The loop variable I
takes on each value 0-4 and then we'll print it. You can also specify start
value and step value. And in this example, function range generates
numbers starting at two, ending before ten and
incrementing by two. You can use four loop to iterate over characters
in a string, which is helpful for task that requires string
manipulation or analysis. In this case, each character
of the string Python is assigned to the loop variable letter in
each iteration, and then I printed one
by one, each character. As I said before, the four loop can iterate over any
iterable object, including lists, tables,
dictionaries, and sets. Here we can see
example with lists. The same we can
have with taples. The four loop is a powerful tool for iterating over sequence. It's commonly used with
the range function to loop a specified
number of times. With a four loop,
you can iterate over lists, strings, tuples, and other iterables, making it a versatile choice for
many programming tasks. We also have wild
loop and Python. It's also control flow
statement that allows code to be executed repeatedly
based on a given condition. The loop continues to run as long as the specified
condition is true. It's particularly useful when the number of iterations
isn't known ahead of time and you want to continue looping until a certain
condition changes. The wild loop checks the
condition before each iteration. If the condition is true, the code inside the
loop block is executed. Once the code has been executed, the condition is checked again, and if it's still true, the code block runs again. This process repeats until the condition
evaluates to false, at which point the
loop terminates and the program moves to the
next section of code. So here we have condition. This is logical expression
that returns true or false. When true, the loops continues, when false, the loop stops. And then we have a loop body, the indented block of code that runs repeatedly as long
as the condition is true. In this example, the loop
starts with count one. Then the condition count less
or equal five is checked. Since count is initially one, the condition is true
and the loop executes, and we print count is one. Then it incremented by one. The loop then checks
the condition again and this repeats until
count is greater than five. We can create infinite loop. It will continue indefinitely if the condition
never becomes false. To stop this loop, we can press Control
plus C or common plus C. Depends on your
operation system and settings. In the next lesson, we
will cover the break and continuous statements for
controlling the flow of loops.
16. Control Flow in Python: Break and Continue Statements: Welcome back, guys. Now we're going to learn break and
continue statements in Python. These are used to control
for and while loops. Each statement has
a specific purpose. Break immediately terminates
the loop it's in and the program continues with the next statement
after the loop. The break statement is
used when you need to exit a loop before it
naturally finishes. This is helpful
when you encounter a specific condition and no longer need to
continue looping. So imagine we have a
list of numbers and we want to stop looping as soon as we encounter
the number seven. For this, I use four loop, and inside, I write condition. If number equals seven, we bring the message. And then I use the
break operator. But until we meet the condition, we will print the numbers. If I run this code, we will get like this. The loop iterates over each
number in the number list, and when number equals to seven, the if condition is true. So the break
statements executes. The loop stops immediately, so number eight, nine,
are never printed. The continuous statement
is used to skip the current iteration of the loop and proceed
with the next iteration. This is useful when certain
conditions are met, and you don't want to execute the remaining code of the
loop for that iteration. So here we can
clearly see we start the loop and met the condition. If the condition is true, the continuous
statement is executed, and the print function
is not executed. We won't print anything. We return to the
beginning of the loop, skipping the print function. But if the condition is not met, we execute the print
function within the four loop and
only after that, we will continue the loop. I slightly change the
code we had before. I replace break
operator with continua, a bit rewrite message. And run again the code. And here we can see that we
skipped the number seven. The second print function with number didn't print anything. We see on the message that
we should skip the seven. And if I remove this print, because it was just
information for us. And now I run again the code, we see that condition was
met and seven is not here. We printed the
numbers as long as the condition was not met
and we kept getting false. As soon as we got through, the continuous statement was executed and we
skipped the print, but we only skipped
printing the number seven. The loop continued to run, and we printed the
other numbers. You can use break and
continuous statements to control the flow of a le loop. Here, I initialize a variable I and set it equals to zero, this variable will serve as a counter to control the loop. The while loop will continue
to execute as long as the condition I less
than ten is true. In this case, the loop will keep running until I equals to ten. I print the value I
for each iteration. Inside the loop, we have
condition to break this loop. This I statement checks whether the value of I is
equals to five or not. If this condition is true, the break statement is executed, and it immediately
terminates the loop and here I increment the
value of I by one. Here we can see the
plus equal operator. It's an example of a compound
assignment operator. It's a shorthand way
to both add a value to a variable and assign the result back to that
variable in one step. In other words, in
place addition. It's a quick way to say increase the value of this variable
by a certain amount. We also have the
minus equal operator. It works similarly. But instead of adding, it subtracts a value
from the variable and assigns the result
back to the variable. The loop starts at I equals to zero and increments
I by one each time. When I equals to five, the break statement is
executed, ending the loop. I'll replace the shortened
version with the full one, and we'll get the same result. Even though the condition
is still I less than ten, the loop stops early
because of brake operator. Let's move on to the
continuous statement. I slightly change the code. If I run this code, we can clearly see that
the loops keeps printing five because continua is
called when I equals to five. This operator stops
the current iteration, so print I is not executed
when I equals to five. Our loop then moves to
the next iteration, continuing with I equals to six, and here we can clearly see it. By using break and continua, we can control the
loops execution. You can handle many different
scenarios with the le loop. But not only with while
also with fore loop, combining for and while loops
with nested if conditions, provides flexibility
and control, enabling you to iterate efficiently while
applying logical checks. Both break and
continuous statements are used to control
the flow of loops. Whether you're using while
or for loop, it does matter. They help manage the
execution within loops, but behave differently
in terms of how they influence
the loops operation. The break statement is used
to exit a loop permanently. The continuous statement
is used to skip the current iteration
and move on to the next one without
terminating the whole loop. It causes the program to jump to the next
iteration of the loop. The choice between break
and continue depends on the logic and flow
control requirements of your specific tasks.
17. Working with Nested Loops and Conditional Statements in Python: Welcome back, guys. We
are going to dive into an important concept in programming nested
loops and conditions, why they are useful and how you can apply them to solve
more complex problem. In programming,
loops allow us to repeat a set of actions
and conditions, let us make decisions in our code based on
certain criteria. We learned it before in
the previous lesson. But when we talk about
nested loops and conditions, we mean that one loop or condition is placed
inside another. Let's start from four loops. I'm going to create two lists. The first one will be animals, and it contains three species. The second will be habitats. We will use these two
lists to generate combinations of each
animal with each habitat. So in the first loop,
we iterate over each item in the animals
list one by one. During each cycle of the
four animal in animals loop, the variable animal
will take on the value of each element in
animals and sequence. On the first iteration,
animal will be lion. On the second iteration, animal will be penguin. On the third iteration, animal will be elephant. Then we have inner loop. This loop is nested
within the outer loop. For each animal, the
inner loop will run through every habitat
in the habitats list. Each time the inner loop runs, the variable habitat will
take on the value of each element in the
habitats list one by one. This inner loop repeats for every single animal
from outer loop, meaning it will go through
all habitats for each animal. For example, when
animal is lion, the inner loop will
iterate three times, setting habitat to Savanna, then Antarctica, and
finally grassland. Print statement runs each
time the inner loop executes. It will print the current
values of animal and habitat. We will see it
together as a pair. This creates all
possible combinations of animals and habitats. So the first iteration of
the outer loop animal is lion and then inner loop
with animal as lion, we have habitat is savanna, so it brings lion savanna. Next, habitat is Antarctica, so it brings lion Antarctica. Finally, habitat is grassland, so it brings lion grassland. Second iteration of the outer
loop animal is penguin, and then inner loop
does the same job. We have penguin savannah, penguin Antarctica,
and penguin grassland. With third iteration, we
have animals as elephant, and the inner loop again,
does the same job. The result of this
masted loop structure is a combination of every
animal with every habitat. Let's go on with
nested conditions. Nested conditions allow us to check for multiple
levels of criteria. For example, here I initialized
two variables A and B. These varias will be used to evaluate the conditions
in the code. Then I write the code
with nested conditions. This is the first condition, and here we check if the value
of A is greater than ten. This condition is true, so the code inside this
Eblock will be executed. Now that we are inside the
I A greater than ten block, we encounter a
second I statement. The second I is nested
within the first one, meaning it only runs if the first condition A
greater than ten is true. In this nested IF, we check if B is
greater than two. Since B is five, which is indeed
greater than two, this condition is true as well. So the code inside this
nested I block will execute. Since both A greater than ten and B greater
than two are true, the program reaches this line, and it will print the message. A is greater than ten and
B is greater than two. This output indicates that
both conditions were met. This s clause is attached to the inner condition if
B greater than two. It provides an
alternative outcome if the nested E statement, if B greater than
two, had been false. In other words, if a
greater than ten is true, but B greater than two is false, the program would execute
this Ls block instead. Since B greater than two is
actually true in our case, this s block is
ignored in this run. The ELs block is attached to the first outer If statement. The initial check, A
greater than ten where false meaning A was ten or less, the program would skip all the inner code
and jump straight to this els block and it would print A is not
greater than ten. Since A is indeed
greater than ten, in our case also, this els block is also ignored in this run. Let's consider
nested wile loops. This nested loop structure is useful for tasks
that involve grids and tables where
you need to iterate over both rows and columns. So the first I initialize the variable role
and set it to zero. This variable will represent the role in our grid
like structure. Then I write outer wile loop. This loop runs as long as
row is less than five. Since row starts at zero, the loop will continue
until row reaches five for each iteration
of the outer wire loop, we go through a complete
cycle of the inner loop. We inside the outer loop, and before we enter
the inner loop, we set column 20. This reset ensures
that for each neuro, we start the columns
from the beginning. So the column equals to zero. As we understand,
the column variable represents the column
position in our grid. Now, let's write
the inner i loop. This inner loop controls
the column values. It will run as long as the
column is less than four, meaning each row will have
four columns, ranging 0-3. For each row, this
inner loop will run four times greeting four
column values for each row. Inside the inner loop, this line prints the current
row and column values. This gives us positions
for each cell in the grid with the
format, row and column. Since the inner loop runs
four times for each row, it will print four column
values for each row value. After printing the
current row and column, we increment column by one. This increment is crucial
to move to the next column. Without it, the inner
loop would repeat infinitely since column
would always stay at zero. Once column reaches four, the inner loop condition, column less than
four becomes false, ending the inner loop and returning control
to the outer loop. And here when we return
to the outer loop, we increment role by one
to move to the next row. This process repeats
until row reaches five, at which point the condition row less than five in the
outer loop becomes false, and the entire program stops. The output for this
code will print all possible combinations
of row 0-4 and columns 0-3, creating a grade of coordinates. Nested wi loops can be used
to manage the game state, iterator possible moves, or check conditions
across the board. See you in the next lesson.
18. Using Loops with Nested Data Structures in Python: Welcome back, guys. Today, we will be working with
nested data types, and we start from nested list. Nested list is a list that contains other lists
as its elements. We can think of it
as a list of lists. In this example, we are
using the nested list called grid that represents
three by three grid. Each inner list
represents a role, and each individual element in these lists is like
a cell in the grid. Here in the grid has three
rows and three columns. In this structure, grid zero
accesses the first row. Grid one accesses the second row and grid two accesses
the third row. The axis individual element within a specific
row in a column, you can use double indexing. For example, grid zero, zero, retrieves the element at the first row and first
column, which is one, and grid one and two retrieves the element at the
second row and third column, which is six. To print each
element in the grid, we can use the nest for loop that we saw in
the previous lesson. The outer loop iterates
through each row, while the inner loop iterates through each
element within that row. The print statement prints value followed by a space
rather than a new line. This is to ensure
each rows value are printed on the same line. After the inner loop
completes for each row, print is called
without any arguments. This line break ensures that each rows value are
printed on a new line. And here we can see the result. This output shows the values in grid printed in three
by three grid format. Nested lists allow for multidimensional
data representation. But we also have
other data types. Let's continue with
NASDA dictionaries. NASDA dictionary is
a dictionary that contains other dictionaries
as its values. This structure is
helpful when you want to store related data about multiple entities and need a way to organize
complex information. In this example, we have
a dictionary called users where each key represents a
unique user ID like user one, user two, and each value is another dictionary containing
details about that user. This dictionary, the
outer dictionary keys, user one, user two, user three, identify
different users. Each value associated
with these keys is an inner dictionary that holds the user's details like
name, age, and location. We can get access. For example, User one returns name, Nick, and all information about this
user to access Bob's age. For example, we can use users User two ag and it returns 30. To print each
user's information, we can use nested loops
to iterate through both the outer and
inner dictionaries. Here's how it works. Users items returns
each key value pair in the user's dictionary. We saw it in the
previous lesson. And in this loop username holds the outer dictionary
skey User one, user two, and details holds
the value for each user, which is the inner dictionary containing that
user's information. The loop iterates over
each user in users. So the code inside the
loop will run three times, once for each user. For each user, we print the
user name value as a header. The user name gives us the Ater dictionary skey
representing a unique user ID. Then we have inner loop. Here we have details items, and it returns the key value pairs in the
inner dictionary. For each key value pair
in the inner dictionary, key holds the specific detail
type name age location, and value holds the
actual detail value. The inner loop will
run three times for each user since each user
dictionary contains three keys. Inside inner loop, we print each user's detail
in a formatted way. Key capitalize, we used to capitalize the detail name
for more readable output. And then after the key,
we printed the value. This allows us to
print the user's name, age, and location
in readable format. After the inner loop completes, we use print with no arguments
to add a line break, visually separating
each user's details. So the first outer
loop iteration for user one and we
have username user one. Then we have details like
name, age, and location, and inner loop prints it then second outer loop
iteration for user two, and we got user name user two and then inner
loop prints details. And the same we see with third outer loop itration
just for user three. We can use NAT and dictionaries for structured storage
of complex data. It makes easy to access and display information
hierarchically. Let's dive deeper,
and now we are combining nested lists
and dictionaries. We can combine various data
types in a nested structure. For example, here we have
a list of dictionaries. The products variable is a list containing
multiple dictionaries. Each dictionary represents
a single product and contains key value pairs for different attributes
of that product. Specifically, each
dictionary has three keys, name, price and stock. Which store details
about the products name, price and quantity and stock. So in products, we
have two dictionaries, each representing a unique
product with its details. The four loop iterates or each dictionary within the list, and here product is a
variable that will take on each product in
products one by one. And during each iteration, product refers to
one dictionary. Inside the loop, we use the product dictionary
to access each attribute like name price and stock of the current product
dictionary by its key. Product name accesses the value associated with the name key. The same we have with product
price and product stock. And eventually, here we have
outputs in readable format. And let's work with
a list of taples. I initialized the
variable grocery items, and this will be
a list of taples. Each tauple contains
two elements, the name of the grocery item, and its corresponding price. The outer structure is a list, which allows us to store multiple items in an
ordered collection. Each item in the list is a double data structure
that can hold multiple vos. Tables are immutable, and we know that from previous lesson, it means that once
they are created, their varios cannot be changed. This is very useful for
representing fixed pairs of data. Each double in the list had
the following structure, item name and price. And then I write the four loop. The four loop iterates over each double in the
grocery items list. Here item and price
are variables that will take on the
variables of each tuples, first and second elements respectively during
each iteration. This unpacking process means
that for every iteration, item receives the name
of the grocery item, and price receives its
corresponding price. Inside the loop, the code uses the unpacked values to
print formatted output. Item inserts the name of
the item into the string. Price inserts the price
from to two decimal places, ensuring that it always
display as a monetary value. When this loop runs, it will produce the
following output for the given list
of grocery items. Combining different
types of nest and data is particularly useful in
real world application. By utilizing NSTA structures, we can create complex and
organized data representation that enhance our ability
to manage information. For now, that's all. See
you in the next lesson.
19. Understanding List Comprehensions in Python: Welcome back. List
comprehension is a powerful feature in Python that enhances code efficiency, readability, and
expressiveness, making it preferred choice
for many developers when working with lists. This can make your code
cleaner and more readable. It allows you to express complex list generating logic
in a single line of code, significantly
reducing the amount of code you need to write. So I initialized a new value, and here we have four loop. This loop iterates or a range of numbers generated by
function range 11, which produces the
integers 0-10. The variable X takes on the value of each integer
in the sequence one by one, and the append method
is used to add each value of X to my list. After each iteration, a new number will be
added to my list. The loop runs 11 times csponding to the numbers
zero through ten. As a result, we can print
and see we've got list. However, we can write this
in a much more shorter, faster and more readable way. I initialized the
variable Mil list two. List comprehension is a
concise way to create lists by combining a loop
and an expression in one line. We have X. The the value that will be
included in the new list. It represents each
number in the sequence. For each number in the range, the value X is directly
added to the Mist two. In my case, since there is no additional
condition or transformation, the list comprehension
simply creates a list of the values generated
by function range. And if I print this code, we see two totally
identical results. We can generate a
new list by applying an expression to each element
in an existing iterable. It can be tuple,
string, or list, and optionally, we can filtering elements
based on a condition. This approach combines
both the duration and conditional logic in
a single readable line. But if I slightly
change this code, I can show you how we can
apply an expression to each value one by one and create new list from
function range. But instead of previous result, I can see not just value
X added to the new list, but square of X. There's expression
that specifies what each element in
the new list should be. In this case, X calculates
the square of X. The operator in Python is
used for exponentiation. So here we have square numbers. The syntax is not
that complicated. Whatever we have on
the left will be added to the list with each
iteration of the four loop. Let's consider more examples. Here are created a list named towns that contains
five strings, each representing a
different city, New York, Los Angeles, Chicago,
Houston, and London. Then we initialize an empty
list called new list. This list will eventually
hold the cities from towns that don't match the
specified city in New York. And here I write for loop. This loop iterates over each
element in the town's list. The variable X takes
on the value of each town in the list
during each iteration. Inside the loop, we check if
X is not equal to New York. If the condition is true, we execute the next line. If the condition is met, we use the Band method to
add X to the new list. This builds new list by including all towns
except New York. If I run this code,
I've got like this. Here we have new
list where we can see all cities, except New York. But let's rewrite it. This operator is
comparison operator. In programming that
stands for not equal two. It's used to compare
two values or expressions to determine if they are different
from each other. It the values being
compared are not the same. The operator returns true value. In other case, it returns false. So let's write our first
list comprehension. This line does the same
thing at the four loop, but in a more concise
and pythonic way, the general structure
of list comprehension. And here X is the expression. For X in towns, it reads over the towns list
just like the four loop. Then we have row
that filters out any towns that are
New York this line creates a new list
directly using the tons that meet the conditions specified
in the list comprehension. So if I print this code, we can see the result. Let's make the task a
bit more challenging. Here I write is comprehension
with condition. X is the value that
will be included in the new list, as I said before, then we have four loop that does the same
thing as previous. But then we have condition. This is a filter
that only includes numbers where the expression
evaluates to true. In this case, it checks if X is even by using
the module operator. It checks if the remainder that X is divided
by two is zero. The list comprehension
it reads through each number generated
by function range, which are zero, one, two, three, and through ten. If X is even, it's included
in the new list events. If X is odd, it's excluded. As a result, we have like this. I can rewrite it as a
traditional for loop, but in this case, we have
included If statement. And only after this condition, if it's true, we abandon
e value to new list. So if I run this code, we will see totally
identical results. Here I create a list called words containing three strings. Hello, world and Python. Let's imagine I want to know the length of every
word in this list. I want to have the final
output as a list of integers representing the number of characters in each
corresponding word. And for this, I can use
the list comprehension. From the left, we
have the A function that calculates the length
of each string in the list. This function returns the number of characters in the string. Then we have iteration
for word in words. This part iterates over each
element in the words list. The variable word
takes on the value of each string in the list
during the iteration. So the comprehension iterates
over the words list. For each word, it calculates the length
using an function. The resulting length are collected into a new
list called length. And we have for hello, the length is five, for world, the length is five, and for Python,
the length is six. Let's go with advanced example. I want to consider an example of an advanced list
comprehension that uses multiple conditions
within a single line. I initialized empty
list numbers. Then I have four loop that
iterates over numbers 0-50, and inside four loop, I have several statement. The first one will check
if X is divisible by two. The second we'll check if
the X is divisible by three, and the third one checks if
the X is divisible by five. If X meets all condition, it's added to the numbers list. The output would be like this. How can we write
it in shorter way? From the left, we have entity that we will
add to the list. Then we have four loop where
we generate number 0-50, and then we have
conditions one by one. I'll make it more
readable like this. At the end, I print numbers
and we have the same result. Let's consider another
example where we will have two
included four loops. I define variable grid, and here I save two
dimensional list. Then I initialize my list. It's just empty list. That will hold the
flattened result of grid. Eventually, my goal is create from two dimensional list
just one dimensional list. In the first four loop, I iterate over each sub list. So in the first iteration, I will have the first
row one, two, three. In the second iteration,
I will have row four, five, six, and in the third iteration,
seven, eight, nine, In the inner loop, I iterate over each element
in the current row. So here, my variable column takes the value of the
current element in row. For the first row, my variable will be one then
two and then three. For the second row, it will be four then five, then six, and for the third, it will be seven, eight, nine. And then I use the
append method that adds each element from row
to my list one by one. By the end of the nested loops, my list will contain
all elements from grid in a single flattened list. Now it's time to write it
as a list comprehension. I initialized, again, my list. As always, on the left, I specify that will be added to my new list with each iteration. Then following the logic
of code execution, I specify the two loops
as described above. I just copy it. And here we are, we have the same result. Both methods give the
same final output, but least comprehension
is often preferred for its conciseness and readability when dealing with simple
transformation like this. The last but not least example
of list comprehension with ELs block that
categorizes a list of numbers based on whether they are divisible
by three or not. I initialized variable numbers. Then I created an empty
list to store the results. The first row, it's for loop. It iterates over each
number in the number list, and then I have If Ls condition. If X is divisible by three, the string divisible by three is added to categorized variable. Otherwise, we will add the
string not divisible by three. And at the end, we print
the result for each number. In the last, we obtained the corresponding
value indicating whether the number is
divisible by three or not. Now, let's write it as
the least comprehension. It may seem a bit confusing, but it's not difficult. Look, we have from the left the entity that we are going
to add to the new list, but it's not just entity. We're going kind of filter it. And here we use
EFLsblock condition. This conditional
expression checks if each number X in numbers
is divisible by three. Then we have loop that iterates over each
element in numbers. We have left and right
parts of expression. On the right, we have a loop, and with each iteration, we check the value of X. On the left side,
we have AFLsblock. Depends on X is divisible
by three or not. Divisible by three or no divisible by three will
be added to the new list. Eventually we have new list
where we just replaced every value depends on it is
divisible by three or not. With this example, I just
wanted to demonstrate how the AFL statement works
in list comprehension. See you in the next lesson.
20. Using the input() Function in Python: Welcome back, guys. How can we receive information
from a user? In programming, there
is input function. It allows the user
to provide data that the program can then use
to perform calculations. For example, a program
might ask for user's name and then greet them personally
based on that input. The purpose of input is to
make applications interactive. Input is used to read the value, enter it from the keyboard. For example, I write
the input function, and inside, I use the prompt. It can be optional. It's string that is displayed to the user to guide
them what to enter. In my case, it will be name. Now, if I run this code, my little program will
ask me enter my name. I input my name, press enter, and for now, my program
has finished working. I'm going to assign this
line of code to a variable and I will call it name and
then I'm going to print it. Here I use concatenation. You already know what it is. Now I run this program again. Again, I input my name, presenter, and here we
are, we got greeting. The input received from the user is always
treated as a string. That's why we use here
concatenation and it works. Therefore, if numerical
input is needed, we need some preparation. We need type conversion. Let's consider such example. The first line of code
is a title or a label to indicate to the user that the program will perform
an addition operation. Then the second and
the third lines of code will ask user
to enter two values. Then I print result. This result I assigned
to the new variable. If I run this code, you will see something strange. I remind you that two
inputs treated as a string. So here we had not adding
bad concatenation. Here we need to
convert the input from string to integer
using function. But if I run this code, I will get an error again
because here in print function, we concatenate two
different types of data, string and integer. If I convert result again to the string, I will work again. But we can omit this step. We can use not concatenation but commas to output as a taple. It separates values by commas
in the print function. And when we using commas
in print function, it automatically converts each item to a string as needed, so there is no need to
use string function. This is often more convenient, especially when combining
different data types. As I said before, Python reads user input as a
string by default, but we can convert
this input into various data types
depending on what we need. We just considered
integer conversion, but we can use other
types of data as well. For example, let's consider
boolean conversion. It's often used when
you want to interpret the user's input as
true or false value. Typically in cases where the
input is yes or no question. So here, user is
asked a question, I initialize a variable to
store the user's response. As a prompt, I use simple question, do
you want to continue? Usually, the answer
is yes or no, but it can be different
type of yes or no. So I'll create the
second variable and here will be conversion. Here I use lower function. This part of the code
is used to ensure that the users input
in lower case. This is important
because user input can vary in capitalization, and by converting it
all to lower case, we can treat all
variations consistently. After converting the
response to lower case, the expression checks if the lowercase input in the
list of accepted true values. You can expand this list
with your own options. If the lowercase input
matches one of these values, the expression returns true. Otherwise, it returns false. This logic is a quick way
to handle various forms or positive responses
without needing multiple if statements for
each possible variations. The Boolean variable
is continua, now contains either
true or false, based on the user's response. When print function is called, Python automatically
converts the Boolean value stored in Ictinua into
string representation. If I run this code
and I input one, I will get true if I input, Yep. I also will get true. And eventually if I input
no, I will get false. As an example, I
also want to show you how to convert into float. If you want to handle decimal values such as
prices or measurements, it will help you to know how to convert into floating
point number. Here the program
asks the user to enter price one and price two, and then I calculate
final price and print. The input which is
captured as a string, then convert it to
floating point number using the float function, and here I use it
for two prices. This allows me to perform numerical calculations
with the value I will get. So let's run this code. I entered two values
with decimal part. Press center, and
I've got the result. Eventually, as a result, we performed a
mathematical operation to calculate the total cost of two products by first
converting user inputs into floating point numbers. In the next lessons,
we will see how these can be used in more
complex code structures. And for now, that's all. Thank you. See you
in the next lesson.
21. Introduction to Functions: How to Create Functions in Python: Welcome back, guys.
We are starting a new section where we
will work with functions. In programming, functions are fundamental building blocks. It's a reusable piece of code that performs a specific task. Let's start from simple example. We already familiar
with print statement. Let's imagine I won
a grid three times. If I run the code, we will
see three greeting messages. Let's rewrite it a bit. The function in Python
is defined using the DEF keyword followed by function name
and parenthesis. Here's the basic syntax. Then I write the
body of a function. It's the block of
code that contains the instructions
that the function will execute when it's called. It's defined by the
indentation level following the
function declaration. The code within the
function must be properly indented to indicate that it belongs to the
specific function. If the body of the function
is not indented properly, Baten will raise an
indentation error. In this case, body of the function will be
only print statement. I define a function named grid that when called will print a message
to the console. If I run the code now, we won't see anything. To execute the code
inside the function, you need to call it by using its name followed
by parenthesis. If I now run this code, we will see the greeting. And this function will print the greeting as many
times as I call it. This saves me from having to copy the greeting repeatedly. In Python, parentheses are essential when
calling a function. They indicate that you are
invoking the function, and without them, the
function will not run. If you indicate the parenthesis, Python understands that you're requesting the
function to execute. If you attempt to call a function without
parentheses like this, this doesn't call the function. For example, here I
called function only two times and the third
one didn't execute. Function can also
accept arguments. Arguments are values that you can pass to a function
when you call it. This allows you to provide input data that the
function can use. For example, in my
greeting function, I can add a parameter name, and then I can use it
inside the function. If I run this code, right now, I will get an error. When a function in Python is defined with
parameters, in my case, this parameter is name, it
means that the function expects certain values to be
provided when it's called. If I attempt to call
a function that has parameters but don't
provide the required arguments, Python will raise an error
and we see it clearly now. So I pass the argument Helga
and Valla everything works. Now we have flexibility. The function produces
different outputs based on the
argument we provide. Each time I call the function, I can pass different name. The function will use that
name in its operation, allowing it to generate
customized greeting. I can specify
multiple parameters. This means that the function can accept more than one
argument when it's called. In my case, I add a number and then I use
it inside the function. However, when calling
the function, it's crucial to provide all
the required arguments. If I call nowadays function,
I will get an error. Because I have only supplied the first parameter name and have omited the
second parameter number. So I add it, and if I run this code
now, everything works. At this point, we have only considered printing
inside the function. But let's consider
the return statement. The return statement in Python is used within a function to exit the function and send the value back to the place
where function was called. It effectively terminates
the function's execution and can pass data
back to the color. Well let's start
from simple example. I write the function
that adds two numbers. The add function is defined
to take two parameters, A and B, and it
returns their sum. When you call the function
with five and three, it performs the addition and returns the result
which is eight. However, it's important
to note that simply calling the function does not display anything
in the console. This is because the
function's return value is not automatically printed. The return value is generated, but unless you
capture or print it, it remains invisible
to the user. To actually see the
result in the console, you need to assign
the return value to a variable and then print it. In this case, we will see
the result in the console. Let's look at more
advanced example. I want to create a function
that takes a name as input and returns it with the
first letter capitalized. I call it capitalized name and it takes one parameter name. Inside the function, the
capitalized method is used to make the first letter uppercase
and the rest lowercase. We learned this method when we worked with
string data type. Then the function returns
the capitalized name. Then I call the function and capture the return
value to the result. I intentionally passed
the name in lowercase. And eventually I print it. The function works
with any name, ensuring that only the first
letter is capitalized, making it easy to
format names properly. One of the main
thing to understand, we can use the function
within another function. This is called function
composition using one function inside another to build more complex behavior. Suppose we want to create a
greeting function that uses our capitalized name function to ensure that name is
properly formatted. So the grit user function uses
capitalized name function to make sure that name is capitalized before it
creates the greeting. After capitalizing the name, Grit user function creates a greeting message with
the formatted name. So I initialized the
variable formatted name and assign it to the result returned by the function
capitalized name. This is made possible by
the return statement, which provides the result
of the functions work. I remind you that this
result is capitalized name. Then I used this capitalized
name in greeting message, and at the end, I return
greeting message. So I can call the main function. In my case, this
grit user function. I initialize the
variable message and call the function grit user. It calls capitalized
name function first to get capitalized name, then build the greeting. By composing functions
in this way, you can keep your code
organized and reusable. Each function performs
a specific task, and you can combine them to build more complex
functionality. If I run this code, I will
get formatted message. Functions in Python are
first class object. They can be assigned
to variables, stored in data structures, passed as arguments
to other functions, and even returned as values
from other functions. They can accept an
arbitrary number of arguments or
take none at all. Function allow you to use the code encapsulated
within them as many times as needed during the execution
of a program. If I comment out the
function capitalized name, I will get an error
when I try to call capitalized name
in grit user function. This is because
grit user function depends on capitalized
name function, but with the function
comment out, capitalized name
no longer exists causing an error when grit
user tries to call it. The past statement in Python is a placeholder that does
nothing when it's executed. It's often used in function
as a temporary body. When you need to
define a function but haven't yet written
any code for it, it allows the code to
run without error. Even though the function
is currently empty, the past statement allows
you to define functions or control structures that don't do anything yet without
causing errors. It's useful for keeping
code organized and error free while you're building or planning parts of your program. Because the capitalized
name function currently does nothing, it only contains
the past statement. There is no value
being returned. When a function doesn't
explicitly return a value, Bydn automatically
returns none by default. When we call grit user, the grid message got none, and here in the console, instead of the
name, we see none. In the next lesson,
we will discuss the different types
of arguments and functions and how
to work with them. For now, that's all. See
you in the next lesson.
22. Positional and Keyword Arguments in Python Functions Explained: Welcome back, guys.
Now we will discuss what positional and keyword
arguments are in a function. Let's start with
positional arguments. So let's imagine you
have a function and you want to pass data
to it for processing. However, how will the function know exactly what
data you are passing? This is where
positional arguments of the function come into play. Let's imagine you are writing a program to add two numbers. This function should take two numbers and compute the sum. So I write the function, then I initialize the variable
and call the function. The first pass value becomes the first number for
sum calculation, and the second pass value
becomes the second. If I print the result, we will see the sum
of these two values. So positional arguments
are passed in the same order as they are specified in
the function call, when calling the function, values are passed without
specifying the parameter name. What's the parameter name, and here we move to
key ort arguments. Keyort arguments are given as key value pairs where each
argument has its own name. This allows you to pass
arguments in any order, even if they were defined in a different order in
the function itself. This means that we specify not only the values
of the arguments, but also their names. When calling the function, this approach allows us
to pass arguments in any order and clearly communicate the purpose
of each argument. Now let's consider an example. Suppose we have a
function that calculates shipping costs based on
weight and distance. We can use keyword arguments to pass these values
to the function. So I write the function itself. I'll call it calculate shipping cost and pass weight
and distance as arguments. First, we will define the rates, one 4 kilograms and
another 4 kilometers. Then using a simple formula, we will calculate
the shipping cost by multiplying the
weight by the rate per kilogram and adding the distance multiplied by
the rate per kilometer. Finally, we will return
the calculated result. Now, let's call the function and plus the key ord arguments
for weight and distance. We will also print the
result we received. If I run this code, we will get the
result delivery cost. So in this example, we pass the values weight and
distance to the function, which we called
calculate chipping cost as keyword arguments. This allows us to clearly see which value is used for
what when we read the code. If I remove here
name of keywords, actually, I will mix position
on keyword arguments. I can pause the weight as just a number and pass the distance as a
keyword argument. In this case, ten will be passed as a positional
argument weight, and 100 will be passed as a
keyword argument distance. And this is called a combination of positional and
keywort arguments. We can use both positional
and keyword arguments when we calling a function. However, positional
arguments must come first. They must always be
before keyword arguments. You cannot specify
keyword argument first. For example, if I
write late equals to ten and then just specify
distance and put it 100, this would be an error
because in this case, the first argument
is keyword argument and the second positional, and we know that
we cannot do this. I will return everything
to how it was. Keyword arguments make our code more flexible and
understandable, especially when dealing with
more complex functions where you may not always remember
the order of all parameters. In the example
above, we discussed positional arguments where we
must remember their order. When using positional arguments, the value we pass must
match the parameters declared in the function and we cannot confuse the
order of the arguments. With keyword arguments, however, we have more flexibility. For example, we can
specify distance, first, when passing it as a keyword
parameter and then provide the weight regardless of the order in which
we specify them, our data will be assigned to the corresponding parameters
based on their names. I'll give another example
to make it clear. I'll create a function grit, where we will pass
parameters name and message. And here we will
return a greeting. In the first time, I'll pad
the parameters as positional. They need to be
passed in the order in which they are
defined in the function. I'll remove the extra
and print the result. As a result, we have greeting, then name and
message at the end. We swap our parameters, we will get an invalid result. And in this case, we just received not very valid message. But if this applies
to some formula, we can get a completely
unexpected result. According to the algorithm, we were supposed to greet a specific person by name first, and then the message parameter
should have followed. What we actually did was
greet with the message. Well, it somewhat
fits the meaning, but imagine if the
message were completely different and then jot the
person's name at the end. Not very but if I pass these
parameters as keywords, that is I specify name
and message specifically, then regardless of
the fact that they are not in the same order
as passed to the function, we will get a valid result. I will remove the
print statement from the previous function
so it won't interfere. Here I added a
default parameter. Keyword parameters can be optional if they
have default values. If I already specified
the message in our function and I delete this parameter
from function call, we will still get
a valid result. But with this parameter hello
that I specified before, that will be the default value. If we don't specify
the message at all, it will be used in
any functional call. Let's quickly go over
the key concepts and differences between positional
and keyword arguments. Positional parameters are
passed to the function in the order in which they appear in the
function definition. If you pass these parameters
in reverse order, it will affect the result. They are also mandatory, meaning they must be provided in the order in which
they are defined in the function and always before any keyword arguments if
both types are present. On the other hand,
keyword parameters are passed to the function
as key value pairs. Each argument has its own name. Keyword parameters don't need to be passed in a
specific order. They can be provided
in any sequence. They can also be optional
if they have default value. This means that if we specify keyword parameter
with a default value in the function definition, we can emit it altogether
when calling the function. While positional arguments,
if defined must be passed to the function in the exact order specified
in the function definition. As you can see, understanding the difference
between keywords and positional parameters is crucial for effective use of
Python functions. And for now, that's all. See you in the next lesson.
23. Using Argument Packing and Unpacking in Python Functions: Welcome, Ben guys. I apologize
for the inconvenience. I switched from
the sd to Pycharm. I had to change my workspace. This was a necessary measure. I hope it doesn't cause
too much disruption. Now I'm going to
discuss about packing and unpacking the
arguments in a function. Packing, the process
is gathering multiple arguments into a
single tapple or a dictionary. Unpacking the process of
distributing or extracting those arguments from a taple or dictionary when passing
them to a function, or when calling a function
with a list or dictionary. When you use Asterix arcs
in function definition, it allows the function to
accept a variable number of positional arguments which
are then puged into a taple. You can pass a varying number of arguments to a function
in the form of list, tuple or set, and
all of these will be converted into a taple through
the Asterix arcs operator. Similarly quarks allows for unpacking of keyword
arguments from a dictionary. The key value pairs
in the dictionary are unpacked and passed as keyword
arguments to the function. So let's go practice. I initialized the
variable my list. Containing four elements
one, two, three, four, I remind you that lists are
ordered collection in Python, meaning that the items have a defined order and can be
accessed by their index. The second line of code
performs unpacking. It assigns the values from
my list to the variables A, B C, and D. Here's how
the unpacking occurs. A gets the value one, the first element of my list, B gets the value two,
the second element, C gets the value three and
D gets the value of four. The number of variables on the left side of the assignment ABCD must match the number
of elements in the list. If they don't match, Python raises a value error. Now let's print the result. I run the code, and here we are. We obtained the result in the same order as we listed the variable in
the print function. If I print A, we will get one. If I print D, we will get four. Let me show you
what will happen. If there are fewer
variables on the left, then there are
objects in the list. We got an error, too
many values to unpack. In case we don't know
how many elements will follow after unpacking, but only need the
first two elements, we can use the operator asterix. This way, the first two elements will be unpacked into
the first two variables, and the remaining elements will be assigned
as a list to the variable C. Let's print
it and we see the result. The same applies if I only
want to get the first value, I'll keep the variable A
for that first element, and the remaining elements will be backed into the variable C. Let's consider an
example with tuple. It works exactly the same. Here I defined tuple. It contains three elements. I remind you that tuples are ordered collections in
Python and are immutable, meaning their elements cannot be changed after
they are created. And here I'm unpacking the
elements into three variables. Let's bring the result,
and here we are. We will look at
the case where we want to get the first
value during packing, leaving the remaining elements
in a variable as a list. But what if we want to get the last variable
during unpacking? The Asterisk seperator
placed in front of A, collects all elements,
except the last one while the last
element is assigned to B. A will capture the elements
two and three in a list. B will capture the last
element, which is four. Now let's look at how
this works in functions. Here I defined the function. The arcs parameter in the
function definition allows funk to accept any number
of positional arguments. The asterix before arcs pugs all extra positional arguments
into a tuple named arcs. This means that when funk is called with
multiple arguments, they are collected in arcs as
a taple. Then I have loop. These loop iterates over
each element in arcs, and I represent each argument
passed the funk one by one. Here I'm calling the function. I passed four arguments, but it can be as
many as you want, and also it can be any datatype. If I run the code, we
will get all arguments. Thanks to this construction, I can pass any number
of positional arguments to the function without
causing any errors. Let's consider another example. So I write the function. The quarks parameter in the
function definition allows the function to accept variable number of
keyword arguments. Double asteris before
quarks means that any additional keyword
arguments passed to the function will be packed into a dictionary called quarks. These quarks will contain
the names and values of the arguments passed where
the keys are argument names, and the values are the
corresponding argument values. We already know that
items method on a dictionary returns
pairs of keys and values, which allows the loop to access each keyword argument and its corresponding
value and quarks. Inside the loop, I print each key and its associated
value on a new line. Then I call the function and
pass the keyword arguments. When funk is called, quarks becomes a dictionary. And then for loop iterates over quarks items and printing
each key value pair. This technique is
useful for creating flexible functions that can handle different sets
of named arguments. You can use it in
this situation where you are building
functions that work with dynamic data
or collecting data without knowing in advance
what field will be passed. Let's consider another example
with dictionary unpacking. Here I defined a dictionary. This line creates
a dictionary named data with three key value pairs, name age and city. The dictionary holds
data that will later be passed as
arguments to a function. Then I define the function and this function will
need three parameters, name age and city. Inside the function,
I will print the values of these parameters
in a formatted string. Let's unpacking the dictionary
and calling the function. When calling funk, the double asterix
data syntax is used. It unpacks the dictionary so
that each key value pair in data is passed as a separate keyword
arguments to function. Essentially, double asterix data translates the dictionary into individual
keyword arguments as if you had written like this. Here's what happens
during the unpacking. The dictionary key name is matched with the
name parameter. Name gets the value own. The key H is matched
with the H parameter, so H gets the value 23. And the key city is matched
with the city parameter, so City gets the value LA. You can use it if you have
a function that requires multiple arguments
and you want to pad them from a dictionary
with matching keys. This technique is called dictionary packing and makes your code flexible and
more maintainable, especially in
complex applications where data often comes
as a dictionaries. For now, that's all. See
you in the next lesson.
24. Understanding Lambda Functions in Python: Welcome back, guys. Let's look at Lambda functions in Python. Lambda functions in Python
are powerful feature that allows for creation of small anonymous
functions at runtime. It provides a way
to write short, throwaway functions
without the need for a formal function definition
using the DEF keyword. Lambda Keyword indicates that you are defining a
Lambda function. Then we have the arguments. These are the input
parameters for the function. You can have multiple
arguments separated by commas and expression. This is a single expression that the Lambda function
evaluates and returns. Unlike regular functions, amda
functions can only contain a single expression and cannot include multiple
statements or annotations. They don't have a name and
it makes them anonymous. This is useful for
short functions that are not used elsewhere. Let's consider the
first example. In this example, is a Lambda function that
takes two arguments, X and Y and returns their sum. The Lambda function is
called with three and five, resulting in eight
if I run this code, we will get the result. If I rewrite this function as usual function, this
can look like this. Here we have two
identical functions. Let's consider Lambda
function with no arguments. You can also create
a Lambda function that takes no arguments. The Islamdafunction
return a greeting message and can be called
without any parameters. I want to note that
the entire function is assigned to the
variable grid. This line essentially saying, store the Islamda function
in the variable grid. At this point, grid
is not holding a standard value like
a string or number. Instead, it's holding a reference
to the Lambda function. Because grid holds
a Lambda function, it's considered callable. In Python, any variable
that can be called a function using parenthesis
is considered callable. When I place
parenthesis after grid, Python treats this as a function call and attempts to call
whatever grid refers to. We cannot do this
with usual variable that refers to string or float. It works only this way. So here we can clearly see that Python treats function as
a first class object and allows variables like grit to be treated as a function names when they reference functions. Let's bring the function
to see the result. Lambda functions are
often used with built in functions like MP
filter or sorted. Let's consider the map function. In Python, map function is a built in function
that used to apply a specific function to each
item in an interval like staple or set and return
a uterable with a result. We can use this function
when we want to perform the same operation across multiple items in
the collection. In my case, as an
iterable, I have a list, and as a function that I'm
going to apply to this list, I write Lambda function. In this example,
the Lambda function squares each number in the list. As you may have noticed, I
also used the list function. By mapping MAP function
with list function, I force the map
object to generate all its items and
store them in a list. This lets me see the output immediately and work with the results like any other list. If I run this code, we will see the result
immediately all at once. Let me quickly show
you what this code would look like without
using a Lambda function. I will use the same number list, but I rewrite it as
a usual function. First, I rewrite
the Lambda function and I call it square. Then I initialize the
variable squared, and it will be empty list. I will save here new elements. Then I write four loop, go through every
number in my list, make it squared, and
append to the new list. Let's bring the result These
should be the same values. So if everyone is
good, here we are. The Lambda function allowed all this code to execute
in a single line, and sometimes it's
very convenient. I will slightly
increase the list. Let's consider the example
with filter function. The filter function is also
built in function in Python, and it used to filter items from an iterable like list or temple based on a
specific condition. The basic syntax is
almost the same. But in this case, I will use the expression that
will check if X is even so we define a list
numbers containing integers. We created a Lambda function that checks if a number is even. We use filter to apply this Lambda function across
all items in numbers, which filters the list to
keep only even numbers. Then we convert the
filtered result into a list and store it
in even numbers. Finally, we print the
list of even numbers. So if I run this code, we will see the new list
with only even numbers. Lambda functions in Python
are powerful tool that allows to create small
anonymous functions. Since Lambda functions
are anonymous, they are typically used
for short term tasks where a function doesn't need to be reused elsewhere
in the code. Lambda functions can only
have one expression, which means you cannot include multiple statements or
complex logic in them. This makes them less suitable
for tasks that requires several steps or
detailed operations where it's better to
use regular function. But Lambda functions
are especially helpful in functional
programming because they can be
used as arguments in functions like map,
filter, and reduce. This makes it easy to write
clear and concise code for transforming
and filtering data without a lot of extra lines. So for now, that's all
save in the next lesson.
25. Understanding Scope in Python: Welcome back, guys.
In this lesson, we will explore what
scope means in Python, and let's define right away that there is variable scope
and scope resolution. Variable scope is a
fundamental concept in programming that dictates where variable can be accessed and its lifespan in the program. Scope defines where a
variable can be accessed. A variable declared
within a specific scope is not visible or accessible
outside that scope. For instance, a
variable defined within a function cannot be accessed
from outside that function. And we call this local scope. The lifetime of a variable is the duration for which
it exists in memory. A variable remains in
existence and can be accessed as long as it is
within its defined scope. Once the program
execution leaves the scope where the
variable was defined, the variable typically becomes unavailable, freeing up memory. We have four types of scope, and in general, it
has abbreviation LGB. It stands for local scope. Variables declared within a
function or block are local to that function or block and
can only be accessed there. Then we have enclosing scope in cases where a function is defined inside another function. The inner function can access variable from the
outer function. Then we have global scope. Variables declared at the
top level of a script or module can be accessed from
any part of that model, including inside functions
and built in scope. Certain names are predefined by the programming language
and are always accessible, such as functions and constants like N or print in Python. By clearly defining where
variables can be used, scope helps prevent situation
where two variables with the same name might lead to confusion or errors
in the program. Understanding the scope makes it easier to read and
understand the code as developers can
quickly determine where and how variables
are being used. Let's move on to practice. Let's imagine I have
two simple functions. I write the first function and
here I defined variable A. Then I write the
second function, and here I defined variable B. These two functions
print their variables. If I call these two functions
and then run the code, we will see these
variables in the console. But what if I want to print the variable A in
the second function? When I call the second function, it will raise a
name error because the variable A is not defined in the scope
of second function. I defined in the scope
of the first function, so we cannot use it here. In a Python, when a
variable is referenced, the interpreter searches
for it in a specific order, starting from the
most local scope and moving outward until
it finds the variable. This order is often
called the LEGB rule. In the second function, Python tries to print
the variable A. However, A is not found
in the local scope, so the Python moves to the
global scope to search for A. But since A is not
defined globally, Python raises a name error. Here we looked at local scope. Let's explore further. I'll rename the functions for better clarity and I will
add an indentation to make the second function
nested within the first and notice how the error warning underline
on variable A disappeared. This is because now we
have enclosing scope, and the inner function
can have access to the A. If I run this code, it works. The variable A is defined
in outer function. This means A is enclosing
scope of inner function, making it accessible
to inner function, even though A is not defined directly inside
the inner function. Now let's consider global scope. Here I defined global
variable A with the value 33. Then I created two functions, functions one and functions
two that brnt this variable. Note that the initialization of variable A is not present
within the function themselves, but in the print function, it's not underlined as an error. Now, I call these two
functions and run this code. When function one is called, it prints the value
of A, which is 33. When function two is called, it also prints the same value
of A, which remains 33. That's what we have
two identical variable here in console. But if I define A
inside each function, each function will create
its own local variable A. These local variables will
shadow the global variable, and that means that
the function now reference their own versions
of A instead of global one. And if I run this code now, we will see totally
different picture. The function one will print its local variable and it's 22, and the function two will print its local
variable that is 11. But let me remove the local
variable for function two. Now, if I run this code, the function two will print
the A that equals to 33 because function one says only local A variable
that equals to 22, but in function two, there is no local variable is defined. This means that when print is
called inside function two, Bython follows its
usual LEGB rule to resolve the variable name. Since there is no local
A in function two, Python checks the
global scope next, where it finds A equals to 33. In Python, the global keyword is used to specify
that a variable inside a function
should refer to a global variable instead
of creating a local one. So if I here in
the function one, write the global keyword, I will make this
variable as a global. When I use the global
inside a function, I tell Python to use the
variable from the global scope and allow modifications to that global variable from
within the function. So here in the function one, the variable A equals to 22 modifies the global A
that was equals to 33. And if I run this code, we will see that output is
22 because in function two, we don't have local
variable at all, and the function searches the variable in
the global scope. But in function one, we modified the global variable A so
that we have 22, two times. But modifying global variables directly within the function, it's generally considered
poor practice in programming. A better approach is
to pass variables as arguments to function and
return any modified values. Understanding variable scope is essential for writing clear, maintainable and efficient code. In Python, the scope defines where variable
can be accessed or modified with
distinct levels such as local and closing
global and built in. This hierarchy determines how Python searches for
variables within function, nested functions
and overall module. For now, that's all. See
you in the next lesson.
26. Handling Errors with try-except in Python Functions: Welcome back guys.
We have already encountered errors several
times when running code, which were displayed
in the console. In application development,
it is important to handle such errors and not
display them to the user. Instead of showing
row error messages, developers should implement
error handling mechanism to provide user
friendly messages. This not only improves
the user experience, but also protects
sensitive information that might be exposed
through the error message. Proper error handling can help maintain the stability
of the application and guide users
towards resolving issues without revealing
technical details. And now we will
learn about error handling the try and
accept construct. Let's start from
a simple example to better illustrate
how this works. For example, I will
try to divide by zero. I will write regular
function that takes two numbers and
performs division. Then I initialize
the variable result, called the function, and
eventually I print the result. If I run the code,
I will get an error indicating that division
by zero is not allowed. Now let's use the construct
to handle this error. The dry block contains code that we want to try to execute. In my case, it will be division. Then I have block except. This block contains code
that handles the error. This block will catch any exception that occurs
in the trib block. Here, the function
division attempts to divide A by B within
a trib block. Then when I call division
with three and zero, the function tries to execute
three divide by zero. In pan dividing by zero
raises zero division error. Since the division by
zero results in an error, the program control immediately jumps to the except block. In my case, the except block doesn't specify any
particular exception type, like, for example,
zero division error. Instead, it catches
all exception that might occur
in the tri block. When the error occurs, the except block
executes and prints the message error occurred
cannot divide by zero. This is a user friendly message designed to inform the user that the operation could not be completed due to
division by zero error. Let's run the code
to verify this. In this case, we won't see the usual Python traceback
error message in the console. Instead of the technical
error information, the user sees a friendly message indicating what went wrong. I replace zero by one
and run the code again, and I won't see anything because I forgot
to print the result. That's much better.
Now everything works. In general, we ask the program to try to execute
this block of code. If something went wrong, we explain this for user, and Python exceptions
are events that disrupt the normal flow
of program's execution. Python provides a
rich set of build in exception classes that represent various
error conditions. By using specific
exception classes, you can catch and handle
errors effectively. For example, in my case, I can rewrite this
code like this. Here I use zero division error. This pot of code allows me to store the exception
instance in a variable E. This variable gives me
access to details about the exceptions such as type and any associated
error message. It can be useful for
logging, debugging, or providing more informative
response to the user. The zero division
error exception is raised because dividing
by zero is not allowed. By catching it with
this block of code, I assign the actual
exception to the E variable. Now E contains the error message that explains why
the error occurred. But by specifying
zero division error, we make it clear that we are only handling
division by zero errors. This avoids
accidentally catching other exceptions that might
be raised like type error, name error, or any other error unrelated to division by zero. For instance, if I mistakenly pass a string
instead of a number, Python would raise
and type error. My block of code would ignore this and let
the program fail. Because in this example, we should use type error
in the except block. In this case, using type error in the except block
is useful because it helps handle cases
where one or both inputs to the function
are of the wrong type. We also can use multiple
except blocks to handle different types of exception
in a single tri block. Let's rewrite this part of code. This is useful because
different kinds of errors may require different handling by specifying
multiple except blocks, each for a different
exception class. We can handle each error type in a way that makes sense
for that specific error. Here we handle the
situations with zero division error and
type error separately. This approach has
several advantages. Each exception type gets
its own specific handling, so the program can respond appropriately to
different errors. Each block can print or
log specific messages, making it clearer to
understand what went wrong. And finally, by
specifying exceptions, we ensure that only
the relevant errors are caught and handled, leaving other potential
errors unhandled, so they can be
fixed or addressed separately. But that's not all. The complete error handling
construct looks like this. We also have Ls and finally
blocks Ls block runs, if no exceptions were
raised in the tri Block. Finally, block that
runs no matter what, whether an exception
was raised or not, let's consider an example. So here I write L's block. It executes only if no exceptions were raised
in the trib block. Here it simply
prints division was successful to indicate that
the operation succeeded. Then I add final block. It runs regardless of whether an exception
occurred or not. It will print execution of the division function is complete to indicate
the function's end. Note that the print statement in the s block is now
a different color. This is because this
block is supposed to execute only if no
errors are raised. However, in the tri block above, we have a return statement. This means that if
there are no errors, we will simply return
the result immediately and the s block will
never be executed. Therefore, we initialize
the variable result here. Now that we have the complete
construct, let's test it. The first time we have
incompatible types. In this case, we got
the message from the except block
with type error, and we have message
from final block. If I replace by zero, we got the message from the second exception block
and from the final block. Let's test the valid input. I replace zero by one. In this case, no
exceptions were raised in the trilog and the s
block was executed, of course, final block. So in Python, there
is a wide range of building exceptions that can be used to handle
errors effectively. These exceptions cover a
variety of errors types, including logical
errors, runtime errors, and system related errors. Complete list of these
building exceptions can be found in the official
Pattern documentation. Let's summarize this
construct briefly. Use Triblog to wrap code
that may raise an exception. Utilize except block to catch specific exceptions and
handle them appropriately. Use the Else block
for code that should execute only if no
exceptions occurred. Use final block
for cleanup tasks. That should happen no matter
if an error occurred or not. By understanding and utilizing this exception
handling mechanism, you can write cleaner, more readable and
maintainable code. Later in the section OP, we will learn how to create custom classes for
error handling. For now, that's all. See you in the next lesson.
27. Introduction to Modules in Python: How to Work with Them and Use the Import Statement: Welcome back, guys. Let's
explore what modules are, how to create them, and
how to work with them. In Python modules are a way
to organize and reuse code. A module is simply
file containing Python definitions and
statements like functions, classes or variables,
which can be imported and used in
other Python programs. Modules help in
structuring your code and keeping it clean,
maintainable, and modular. To use a module, you
need to import it into your current Python script
using the import statement. Let's see how it works
with the example. We'll be working with three
separate Python files. These files located in a single root directory
called Project one. Let's create Project Spacewar. This will be a program
designed to manage and display player statistics for
a functional game called Space Wars. The project is divided
into three modules, as I said before.
First, gaming stats. This module will contain
functions for calculating a player's score based on their achievements
and in game score, and also will
determine their rank. Then player Utils. This module provides utility
functions for creating player profiles and formatting their game statistics
in readable format. And the third file main dot pi. This is the main file that
ties everything together. It creates a player profile,
retrieves their statistics, and displays a
formatted summary, including their rank and score. Here we will learn how
to use Python modules, where each module
is responsible for a specific part of the
functionality and how to import and use
functions and constants from other modules to build
a cohesive application. Let's start from gaming stats. This module will contain
two important functions, calculate score and get rung. The doc string explains
what the function does. It calculates the player's score based on their achievements
and in game score. We already know that doc
strings are written inside triple quotes and
are typically placed immediately after the
function or class definition. So I create calculate
score function. It takes two arguments, achievements and score and calculates the score by dividing achievements
by the score. If the score is null, the player gets a bonus
score based on achievements. And here I use the command
bonus for zero scope. It's an example of a single
line command in Python. Single line commands
are used to explain specific line or
section of code. It helps developers understand what that part of code is doing. This is especially useful when the logic might be complex, non obvious, or when you want to provide additional context
for a particular operation. If score not equals to zero, we return calculation
the player's score based on their achievements. The round is a built
in function in Python. That rounds floating point
number to the nearest integer. Then I create the second
function, get Rank. This function will
take an argument, score this argument, we will get from the
calculate score function. I remind you that this
function return this value. Here I also create doc string. The rank will range from
beginner to professional. I'll add an emoji for
a more visual example. The score is compared against
a series of threshold, 200, 150, and 100. And depending on the
value of the score, corresponding rank is returned. These rank are represented
by string that include both the name of the rank and an emoji to visually
represent the rank. Then I use the comment
again to indicate that the following variables are constant that are defined
in the module level. In Python, constants are usually written in per case.
We already know it. Constants are values
that are meant to remain unchanged through the
execution of the program. The first constant represents the default value for a
player's achievements. It's useful for initializing player profiles when no achievements have
been earned yet. For example, when a new
player starts the game, their achievements can be set
to zero as straight point. The second constant represents the default score for a player. Similar to default achievements, this value initializes
a player's score to zero when they start
playing the game, assuming they haven't
scored any points yet. The third constant stores
the name of the game. We will reuse these constants
across the project. The second module is
responsible for creating player profiles and
formatting player statistics. The first line of code, we use an import statement which is used to bring
function variables or classes from one module into another so that they can be
used in the current script. From as a keyword, used to specify the module
from which you want to import. Gaming stats in the
name of the module. I want to import
from in my case, gaming statts dot pi is a Python file that
contains functions and constants related to calculating scores and
determining player ranks. Actually, the module
gaming stats could contain various
functions or constants. We are specifically importing
only certain items from it. You can think of it as
selecting what you need from the module instead of bringing the entire module into
the current script. Here, I create function,
create player profile. It will take two
arguments name and age. Add dog string. Inside, I create
a dictionary that store player information
such as name, age, games played
and achieve status. Name, this is the player's name, age, it's players age. Then we have games played zero. This sets the default value of how many games the player
has played to zero, meaning the player
is new or has not played any games
yet. Is active true. This sets the player's
status to active, meaning the players is
currently active in the game. Then I create the function
format player stats. It will take three arguments, name achievements and
score. Create docstring. Docstring was for the function, and this in line command, I create to explain
the purpose of the following lines
of code where the function calculates
score and get rung. This function
formats the players statistics into human
readable format, which can be displayed
to the player used in reports or logged
for tracking purposes. So I calculate score using the calculate score function
imported from gaming stats. This function calculates the
player's calculated score based on the logic defined
in the gaming stats module. Here we use the
arguments that we get. These arguments were defined in the calculate score function
in the module gaming stats. Here we just use it. Then the Get rank function is called with the
calculated score. It also imported from
gaming stats module. Based on the score, the function return a rank, such
as professional, expert or so on, depending on the thresholds defined in the
gaming stats module. The function returns
a formatted string containing the
players statistics. Here we use formatted
multiline string to create a readable well
structured block of text. This means the string
can span multiple lines, which is especially useful for formatting large block of text, such as report messages. We already know what F
string formatting is. I won't repeat it.
We worked with this. As a result, we have
formatted message. And finally, mine module. First, I import
modules and functions. First line, import
specific functions, calculate score and get rung. By importing specific elements, you can use them
directly in your code without needing to prefix
them with the module name. Also I import constant game name also from gaming stats module. The second line imports the
entire player Utils module. To use any function or
variable from this module, you need to prefix it with
the module name player Utils. When we use selective input
as the first line of code, it saves typing and improves code readability by allowing direct access to
functions or variables. It's useful when you only need
specific part of a module. Full module input
keeps the name space organized by requiring you
to prefix the module name. It helps avoid
name collisions if different modules have
functions with the same names. So here I'm creating
the main function. This function is the
entry point of the script and coordinates the
execution of various tasks. Create a command for
the understanding. This line of code, create a dictionary that represents
the user profile. Note that the dictionary is created using the create
player profile function, which we can call from
the imported module. Since we imported
the entire module, we access this function using the module name followed by
a dot and the function name. Here we pass name
Nik and age 25. The profile includes
default value like games played zero
and is active true. Then I display
welcome message using the constant game name imported from the
gaming stats module. And here I just
print the line of dashes to visually separate
sections of the output. Here I define example values for the player's
achievements and score. Achievements
represents the number of achievements earned by the player and score represents a metric related to the
player's performance, such as the number of games
played or performance rating. Here I call the function
format player stats. This function imported from the player Utils module
takes the player's name, achievements and
score as arguments. The reason we use the
module name before the function is because we
imported the entire module. As I said before, by
importing the whole module, all the functions
defined inside it need to be accessed using the
module name followed by a dot. And here we get name
accessing a value from a dictionary by
writing player name, we are accessing the
value associated with the key name in the dictionary, which in this case
would be Nick. It uses calculate
score and Gt run functions from gaming starts to generate formatted report, which it then returns
as multiline string. And then I print the result. I output the formatted player
statistics to the console, let's demonstrate direct
usage of imported functions. Here I call the
calculate score function from the giving stats module, which was imported at the
beginning of the script. Here achievements, the number of achievements
the player has earned. And score a performance metric, such as the number of games
played or the row score. Inside the function calculate
score, we have formula. This formula divides the
number of achievements by the score and multiplies
the result by 100. Then rounds the result
to the nearest integer. This gives a calculate score based on the player's
achievements and performance. Then I call the G Runk
function that is used to determine the player's rank based on their calculated score. The rank is determined by the calculated score value
passed into this function. Here the function checks
the calculated score against various thresholds
and assign rank. Then we print the player's
rank using an F string. I'll remind you that these
two functions mentioned above were imported from the gaming stats
module separately. Therefore, when calling
these functions, we don't use the module
name with a dot. We can use this function
directly without module prefix. This is one of the most
important constructs in Python. It's used to control how
Python screen behaves when executed directly
or imported as a module. Name with double underscore is a building
variable in Python. That gets assigned a value depending on how the
script is being used. When I run a Python
script directly, the varie of name is
automatically set to main. If the script is being imported into another Python script, Import main and so on, like we did before,
then value of name is set to the name of
the module itself. In this case, main, the condition that we see in this line of
code is a way to check whether the
Python script is being executed directly or
imported as a module. If the script is being
executed directly, and I'm going to
run it directly, name will be equal to main, so the condition becomes true. If the script is being imported as a module into another script, name will be the name
of the script module. So the condition becomes false. I place the main execution
code inside the structure. This ensures that the code is
only run when the script is executed directly and not
when it's imported elsewhere. It prevents unwanted
code execution when the module is imported. The code will be executed only when the script
is run directly. Guys, quick recap. A module in Python is
a simple file with Pi extension that
contains Python code, including functions,
variables, classes. In our example, we
have three modules, gaming stats, player
Utils and main. Organizing code into modules helps keep your
program organized. You can import an entire module using the Import
module name syntax. Or alternatively, you can
import specific functions or variables from a module using from module name,
Import functions name. This allows you to either use the full module name or
just the function name. But this is not all in pattern. You can import modules,
functions, classes, or variables using an alias. This allows you to give a shorter or more convenient
name to an important item, which can be particularly
useful when working with long module names or if you
need to avoid name conflicts. You can import entire modules, specific functions
or variables using the as keyword to
assign an alias. It's commonly used
in large projects where modules or functions may have long names or when you want to keep your
code clean and readable. Using aliases in imports is a
standard practice in Python that improves the efficiency of coding and enhances
code readability, especially when working with third party libraries
or large codebases. So guys, see you in
the next lesson.
28. Using Decorators as a Design Pattern in Python: How to Implement and Apply Them: Welcome back, guys. Let's
look at decorators. In pattern, decorators are powerful and versatile tool that allows you to modify the
behavior of a function or class. There are essentially functions that wrap another
function to extend or rather its functionality without modifying other original
functions code directly. Think of decorators as
wrappers that you can place around a function
to enhance its behavior. Let's see how it
works on the example. A decorator is simply
a function that takes another function as an argument and returns a new function. The first step, I define a decorator function that takes another function
as an argument. Then I define an inner
wrapper function inside the decorator function. Here I print message because wrapper will add
some functionality and I'm going to show it. This message will indicate
that we wrap the function, call the original function
from within the wrapper, because it's wrapper,
so I'm going to add some behavior after
function is calling. I print another message
that will indicate that function was calling
and return wrapper function. Here's what's happening here. My decorator is a
decorator function that takes a function
funk as an argument, and inside my decorator, a new function wrapper
is defined where I calls the original function funk and adds extra behavior,
printing messages. Finally, my decorator returns
the wrapper function. Your first simple decorator
is ready. Let's use it. I'm going to apply
this decorator to another function
using the add syntax. Here I define the function, say hello, simple function
that prints hello world. And I use my decorator. At the end, I'm
calling the function. If I run this code,
we got this message. We can see here that the
message printed by our say hello functions is wrapped with two additional messages
from the decorator. However, using the add
syntax is optional. What the syntax does is simply a shortcut
for the following. This code will perform the
same way as we saw earlier. Python automatically passes say hello function to the
M decorator function. The same say hello now refers to this new
decorated function. And instead of using the
add M decorator syntax, you explicitly pass the
function to the decorator. This line takes the
original halo function and passes it as an argument
to the M decorator. My decorator returns a
new decorated versions of the the hello function. And if I run this code, we can see the same result. But the add syntax is more
concise and readable. So for the upcoming examples, I'll be using add syntax. I comment out previous code
and start write new one. My first step I import time. This line inputs
the time module, which provides various
time related functions. In this case, I will
use time method to measure the time before and
after the function executes. This function timer will
be decorator function. It takes a function
funk as an argument. This is the function that we
want to decorate or modify. The timer function returns
a wrapper function that will replace funk when
the decorator is applied. This wrapper function
adds functionality. In this case, measuring
the time taken by funk around the
original function. Wrapper function
takes any arguments and keyword arguments
that funk would receive. This allows the decorator to work with any
function signature. We define the start time, then we call the original
function and defined end time, and then we print
the elapsed time. Finally, it returns the
result of the function, and at the end,
the timer function returns the wrapper function. This is the key part of the decorator because
rubber will now be called in place of the original
funk. So let's test it. The line timer above slow function is a shorthand
for applying the decorator. Slow function is a
simple Python function that performs two tasks, simulates a delay using time slip and print a message indicating that
the function has completed. The time slip function is part of Python built
in time module. It pauses the execution of the program for the
specified number of seconds. In this case, I
choose 2 seconds. It means that when I
call slow function, the program will
slip or do nothing for 2 seconds before
proceeding to the next line. When I call slow function, B then actually calls the wrapper function returned
by the timer decorator. The start time is recorded before calling the
original slow function. Then slow function is executed. This causes the program
to sleep for 2 seconds. After slow function completes, the end time is recorded. The lapse time is
calculated and printed showing how long slow
function took to execute. So if I run this code,
we will get the result. The first line
function complete, it's indicating the
function executed. The second line
the time taken is approximately 2 seconds
reflecting the sleep duration. Python also comes with several built in degrators
in the OOP section, we will see Python's built
in degrators in action. For now, that's all. See
you in the next lecture.
29. Building a QR Code Generator for Social Media with Python: Now let's combine learning with practice and create
something useful. Nowadays, almost everyone has a personal website
or online presence. Whether it's Instagram or YouTube Channel or
another platform, if you own a business,
for instance, you might need a QR code to help people quickly
access your resources. Most people are
familiar with QR codes, but here's a quick reminder
for those who aren't. QR code is a two
dimensional barcode that can be scanned by smartphone or QScanner composed of black squares on
white background, I stores various types of
information such as text, links, phone numbers, and more. QR codes are popular for their convenience
and versatility. Whether you are sharing
contact details or product information, they offer a quick solution with additional marketing and
promotional potential. Let's get started. I've created a file where using Python, we will generate a QR code that links to my digital
program live channel. You can use this
for any resource. Your website, specific
page, whether you choose, will be using the
Python library QR code, which is a powerful
tool for generating QR codes directly
in Python file. And first, we will install the
library using the Command, PIP install QR code, and then input it. Here we have the message. There was an error checking
the latest version of PIP. It usually appears when PIP Python's package
installer tries to verify if a newer version
of itself is available. This error is not critical
and won't prevent you from using PIP to
install or manage packages. It simply means that PIP couldn't check for its
own latest version, but it will still
function normally to install update or uninstall
packages as needed. There doesn't have
impact on my workflow, so for now, I leave it as it is. Now let's write a function
called Generate QR code. This function will
take arguments such as data, it's our content. File name and error correction. F two arguments are positional and the third
one is keyword argument. The data parameter will be the link of resource
we want to encode, while error correction will define the error
correction level, which affects the QR codes
resilience to damage. I will make it a bit readable. When each parameter
on its own line, it makes it easier to scan and identify
individual arguments. The QR code library in Python includes error
correction settings, which manages how much
data in the QR code can be recovered if part of it
is damaged or obscured. It offers four levels. Low, recovers up to
7% of data, medium, up to 15%, quartile and high. We will use the low
error correction level capable to recovering up
to 7% of damaged data. For now, it's enough. Next, I will set the box size. This parameter sets the size in pixels of each individual
square in the QR code grid. A larger box size makes each square in
the QR code bigger, resulting in an
overall larger image, let it be ten and this parameter specifies the thickness of the white border around the
QR code measured in boxes. By default, a border of four
is recommended to ensure scanners can easily distinguish the QR code from its background. Then inside the function, I initialized the variable QR and create a QR code object, then add our data in
the QR code library, QR code is a core class used to create and
customize QR codes. This class provides
a structure and functionality needed to
define a QR codes contents, design, and error
correction level. When creating the
QR code object, we pass error correction, box size, and border
as Keyword arguments. Since these arguments are
provided in the function call, they'll be used in QR code
object with the values we set. Don't worry. We will cover what classes are
in all P section. For now, the main thing is to understand how functions
and libraries work. Then I call Add Data function. This function allows you to add the actual content or data like URL or text that you
want to encode in QR code. You can add multiple
pieces of data with this function and the QR
code will store all of it. Then I use M function. This function generates
the QR code image based on the data provided. When parameter fit
equals to true, the QR code automatically adjusts its size
to feed the data, ensuring that all information is stored without overflow
or truncation. Then make image
function generates a QR code image object based on the data and settings previously defined in
the QR code object. Parameters that we have here, the first one fill
color. I set it black. It sets the color of the QR
code itself, the squares. The second parameter, back
color, I set it white. It sets the background color
behind the QR code squares. Eventually, we save it. The filename argument specifies the file paths and name,
including the extension. We already covered
this construction of code in previous lessons. This line of code ensures that the code runs only when
executed directly, not if imported as a module. Next, we will call our function and pass all the information. The function call here generates a QR code linking
to a specific URL. In my as it's my channel link. And I save it as QR code PNG. So here we have the code that demonstrates functional
programming principles by encapsulating the QR
code generating process within a single function,
generate QR code. By putting all the steps within generate QR
code functions, the code is reusable
and modular. Let's check if it works. Now, if I run this code, I will have a file. If I open it, will
see our QR code. Let's test it. While it works. Try creating your own QR code. You can use any link you'd
like and get some practice. You can try generating a QR code for your
Instagram profile, for example, for
now, that's all. See you in the next lesson.
30. Introduction to Object-Oriented Programming (OOP) in Python: Classes and Objects: Welcome back, guys.
Congratulations. We are moving on to the most
important part of learning Python object oriented
programming or P. This powerful concept
will help you write cleaner, more organized code and make
your program more flexible. Enjoy the learning process. In Python, everything
is an object. This means that every
piece of data in Python, whether it's a number, a string, a list, or even a function, it's treated as an object. In OP, a program is made
up of a collection of objects that interact with one another to accomplish tasks. Object oriented programming
offers several advantages. One key benefit
is that OOP helps organize code better
and encapsulate it. By grouping related data and methods together
within an object, your code becomes more modular
and easier to maintain. Another advantage of OOP is that it enables the creation
of reusable code. By defining classes that can be instantiated
into objects, you can create code
that can be used in different parts of a program
or across multiple programs. In Python, all P is
implemented through classes. Classes define the properties
and behaviors of an object. While objects are instances of classes that contain
data and methods. We can think of a class as a blueprint for
creating objects. They define the structure
and behavior attributes and methods that objects
created from them will have. Method, on the other hand, are functions defined within
a class that describes specific actions or behaviors
this object can perform. So class defines the blueprint, while methods define actions for object created
from that blueprint, you can think of an object as a real world entity that has
characteristics and actions. Let's take a simple example
to illustrate this concept. Let's create a simple
class called dog. It will be the blueprint
for the instances. I'm going to create later. Here I use the it method. It's a special function
called a constructor. It automatically runs each time you create a new object
or instance of the class. Here I pass the
parameters name and age. Self a refers to the specific
object being created, so you can assign data
specifically to that object. These two lines of code, self name equals to name
and self age equals to age. Create two attributes name and age and set their values based
on the provided arguments. This allows each
instance of the class to have its own name
and age values. You will see it in a minute. Next, I create the
function bark. This function in
the dog class is a method that defines a behavior for instances
of the dog class. This is a simple
function that returns. Now we have our
first simple class. This is our blueprint. Next, I'm going to create
instance from this blueprint. I initialized variable my dog. Then I call my newly
created class dog with the following parameters. It will be Lu and three years. When I call this line,
the following happens. Python allocates memory
for a new dog object, and the net method, as I said before,
constructor is executed. Here we have the self
name that will be Lu and self age that
will be three years. The self parameter
allows the method to refer to the instance
that is being created, making it possible to
set its attributes. The init method runs only once during the
creation of an object. This means that each time you create a new
instance of dog, the it method runs to initialize that specific instance with
the provided attributes. In my case, the
dog's name and age, but it could be
different attributes. So here, newly
created dog instance is assigned to the
variable my dog. When I created instance of dog and stored it
in my dog variable, I can call its method. This line of code
calls the bark method, which returns the
string and prints it. Let's run the code
and see the result. When you want to call a
method on an instance, you use dot notation. This means that to access any method or attribute
of the instance, you always use the
instance name followed by a dot and then the
method or attribute name. I also want to draw
your attention to the fact that when you
initialize an instance, you must provide all
the required arguments specified in the constructor. Otherwise, you
encounter an error. Let's consider class attributes. These attributes can be shared
across all instances of a class and can be accessed using the class
name or through an instance. In my Dog class example, the class attribute species
holds the value canine. I will add the method info. Where I'm going to use
all three attributes, one of them will be
class attribute, and another two will
be our name and age. Just to show you what's the difference between
these attributes. The self parameter
allows the method to access the specific instance
on which it was called, making it possible to retrieve the instances attributes
like name and age. And here I use self species. In this case, it refers
to the string keine. Since species is defined
as a class attribute, every instance of dog can
access it through self species, even though it's defined
at the class level. So what I'm going to do next, I create two instances
of the dog class, let it be a little low and rick. Then I print the info where
we use all parameters. I print info about u
and info about Rik. Almost forgot about
second attribute that we defined in constructor. Now, if I run this code, we will get two messages
for each instance. And as we can see, we
have different names, different years, but
the same species. That's because species
is a class attribute. We already saw that we can call the method
using dot notation. In Python, we can also access
attributes on an object, both instance attributes
and class attributes, and we can do this also
using dot notation. This means we write the name of the instance or the
class followed by a dot and then the name of
the attribute. Look at this. I called the class
attribute on two instances. And if I run this code, we can see that I've
got species twice, and twice it was nine. This is because species
is the class attribute, and all dog instances
share this value. But the attribute name and
age are created inside the init method using
self name and self age. These are instance attributes, meaning they are specific for
each instance of the dog. Accessing name through
instances my dog and my dog two will give
us two different names. But it's important to
understand the difference between class attributes
and instance attributes. And the main difference
class attributes are accessible on both the
class and its instances, like we saw before, here I call class attribute on the
class and everything works. Instance attributes are
only accessible through individual instances as they represent data unique
to each instance, trying to access instance
specific attributes like name or each director on the class will return an error because these attributes don't
exist at the class level. They are only created when a specific instance
is initialized. So we've now covered
what glasses, Wan instances, and we've been introduced to class attributes
and instance attributes. We've also considered
the NIT method. The NIT method is
a special method, sometimes called a
magic method that is automatically called
when a new instance of a glass is created. Similarly, before an
object is destroyed, a destructor is called, which in Python is named Dell. This is another
one, magic method. But since the interpreter
automatically manages the release of
resources used by the object, having a destructor is
not very important. Therefore, I will not delve
into this method right now. We won't focus on magic
methods in detail right now. We'll cover them
more deeply later. For now it's essential
to understand that init is the constructor
method used to initialize each instance
with the unique values or configurations where the
object is thus created. When you create an
instance of a glass, Python automatically looks for the init method in the class. This method usually takes
self as its parameter, which represents the
instance being created along with other parameters to set up the instance
with specific data. The last thing I will mention in this lesson is that in Python, class names are conventionally written in a capitalized
first letter. This style is known
as camel case. For example, class names
might be written as dog, person or animal species. This capitalization
helps distinguish class names from other
variables or functions, which typically start
with a lowercase letter. In the next lesson,
we will explore the different types of
methods in classes. So for now, that's all see
you in the next lesson.
31. OOP in Python: Working with Class, Static, and Instance Methods: Welcome back guys.
Let's continue. In object oriented programming, especially in
language like Python, classes can contain
different types of methods, instance methods, class
methods, and static methods. Let's start from
instance methods. These are the most common
type of methods in classes. They operate on an
instance of the class, an object and can access and modify the
instances attributes. We saw this in the
previous lesson. We had bark and info methods. They have access to the
instance attributes defined in the it method. And the first parameter of an instance method is
typically named self, which refers to the instance of the class calling the method. Here I can say that bark
not an ideal example for instance method because we
don't use here the attributes. I will use the info to
show you how it works. An instance method in Python
can access class data, class level attributes, but
it is primarily designed to be called on an instance of the class, not the class itself. I will remove the call to the bark method.
And run the code. I'm sorry, I forgot about the second attribute
and it's important. It's age. Now it works. As I said before, this method has access to the
class attributes. But if I now call this method on the class,
I will get an error. Yes, instance methods
can access class data, but they are meant
to be called on instance, not the class itself. You actually can
technically call it on the class.
I'll show you how. However, it's not
recommended since it's not typical usage
pattern for instance methods. Here I call the in, for instance, method
directly on the class dog, but I manually passing
my dog an instance of dog as the first
argument self. It leads to confusion because instance methods are
designed to operate on instances and calling them on the class directly doesn't clearly indicate this behavior. So I don't recommend it. I just showed you how it can be. Now that we've covered this, let's move on to
the class methods. Class methods are
methods that belong to the class itself rather than
any particular instance. The first parameter
of a class method is typically named CLS, which refers to
the class itself. They can access and
modify class attributes, variables defined
in the class level. They are marked with a
decorator class method. Decorator in Python is
a special function that modifies the behavior of
another function or method. In this case, decorator is
used to define a class method. When you use class method, it tells Python that the method should receive
the class itself, CLS as its first argument. Instead of an instance
of the class self, this allows the method to access and modify
class level data, but not instance specific data. Here's how we can
call this method. By using the class
name directly, we're calling a method
at the class level. Then the dot operator, it used to access attributes or methods
within a class or instance, and then the class method get pieces we defined
in the dot class. At the end, the parenthesis that we are used to
call the method. Without parenthesis,
this line of code would just be reference
to the method itself, but not actually call it. So when I run this code, Biton locates the dog class. It uses the Get species
method in dog class because class method allows it to be called on
the class itself. Get species is executed with the dog class as
its argument CLS. The method performs
its function, which typically
involves returning a class level attribute. In my case, it's species, and then we print the result. So we call the class method on the dog class two or three with shared class
level attribute. Let's go on with static method. These are methods that don't modify the glass
or instance state. They are defined using the
static method decorator. First, I return the bark
method that we had before. In this example, I can
convert the bark method into a static method by adding
the static method decorator. This is an excellent
example because bark method doesn't
need to access or modify any instance
specific data or any class specific data. So static methods are
ideal for functions that perform tasks independently
of the classes state. It can be something like
utility or helper functions. By marking bark method with
static method decorator, I'm indicating that
this method doesn't require reference to an
instance or the class itself. So I don't have here
self or CLS arguments. This gives Bark simple
and communicates to other developers that it's
a standalone function, not dependent on any data
inside the class or instances. I can call Bark directly
on the dog class. And also I can still call
bark from an instance of dog. First, I create
instance of dog class, and then I call the static
method on this instance. Regardless of how we
call a static method, we get the same result. Let's summarize,
instance methods are the most common type of the methods in object
oriented programming. They operate on an instance
of the class and have access to the instances attributes
and other instance methods. The first parameter of an instance method is
typically self. This refers to the instance of the class that the
method is called on, allowing the method
to access and modify the instances attributes. In Python, the self
parameter is not a keyword and can technically be replaced
with any name you prefer. However, it's widely
accepted convention to use self at the name for the first parameter of
instance method. This is because it helps improve the readability of the code and makes it clear that the method operates on an
instance of the class. Then we have class methods. These are methods
that are bound to the class itself rather
than an instance. They are used for operations
that affect the class. The first parameter of a class
method is typically CLS, which refers to
the class itself. This allows the method to
modify class level attributes. In class methods,
CLS is a convention, much like self is used
in instance methods. It refers to the class itself, as using self is
important for clarity and consistency when dealing
with instance data. Sticking to the
CLS convention in class methods is
considered best practice. These methods are useful
when you need to operate or modify data that is shared among all
instances of the class. Class methods can
be called both on the class itself
and on an instance. But this behavior is
not usually necessary. It's possible because
class methods are bound to the class and
not to an instance, meaning that they
can be accessed from both the class
and the instance. Sometimes it might be more convenient to call a class
method on an instance, especially if you are already working with an
instance and want to access a class level method without referring
to the class name. But typically class
methods are meant to be called on the class itself,
not on an instance. Then we have static methods. These are methods that don't depend on class
or instance data. They are completely
independent and don't have access to
instance or class. We can use it for utility
functions that are related to the class but
don't depend on its state. Static methods don't take CLS or self as their first argument. They are self contained. We can call these methods both on the class and on an instance. Though typically they are
called on the class directly. Each method type serves
a specific purpose. Instance methods for
interacting with object data, class methods for
class level data, and static methods for utility functions that don't depend on class or
instance attributes. By understanding
these differences, you can better structure
your classes and methods. So for today that's all, see you in the next lesson.
32. OOP in Python: Single and Multiple Inheritance Explained: Welcome back, guys. We have
studied what a class is, what a class object is, and the type of
methods in classes. Now it's time to get
acquainted with one of the main concept of
OOP inheritance. When explaining
inheritance in Python, it's helpful to start with the foundational concept
that all classes in Python inherit from a build in root
class called object. Understanding this helps clarify the structure of class
inheritance in Python, especially since all classes, even those that don't explicitly inherit from
any other classes, still derive from object. In Python, object is the most fundamental class from which all other classes
implicitly inherit. This means that every class in Python is a subclass of object, making object the root of
Python's class hierarchy. Since all classes ultimately
derive from object, the object class is always at the top of any
inheritance hierarchy. Before I show and
prove this to you, let's cover a bit of history. Python, there was a
significant shift in the design of classes
with the release of Python 2.2 when the concept of new style classes
was introduced. Before Python 2.2, Python only had what were
called style classes. They were simpler and they did not inherit from the
root object class. Instead, they formed their
own separate class hierarchy. All style classes are no longer used at all
in modern Python. Python three, the concept of old style classes was
completely removed. So all classes are now
new style by default. Python 2.2 introduced
new style classes as an improvement over
old style classes. The biggest change was that all new style classes
inherit from object, creating a unified object
model where everything, both user defined classes
and build in types, was part of the same
inheritance hierarchy. When Python three was released, the designers of
Python decided to make all new style
classes by default. This means that in Python three, the old style class concept
was completely removed. Any class defined in Python three inherits from
object automatically, even if it doesn't explicitly declare so now
writing class I class and class I class inherited from object are equivalent
in Python three, both creating new style classes. When creating classes, we
can explicitly inherit from object to clarify that our class is derived
from the root class. This is often done to ensure compatibility with both
Python two and Python three. You may encounter this need when working on a
legacy project. If you don't know what it is, legacy project and
programming refers to an older software system or applications that's
still in use, often without data technology or code that requires
maintenance or updates. Let's start from the
single inheritance. I'll create two classes, the animal and the dog. The animal will have one
instance method, just eat. The dog class inherits
from the animal class. And we have also one
instance method bark. Single inheritance occurs when a subclass inherits from
only one superclass. We call parent class
as a superclass. In our case, it's animal. The animal class can be as a general template
or base class, and in our example, it represents any kind
of animal that can eat. We can imagine other
animals like cats, lions that could
inherit this behavior. Then we defined dog class that inherits from animal class. This means that dog
will automatically have all properties and
methods of animal class. Inside the dog class, there is a new method, bark. This method prints barking
by inheriting from animal. Dog not only inherits
the ability to eat, but it can also have its own
behavior such as barking. Here we can see method
inheritance and extension. Dog gets the eat method
from animal class, and additionally, dog defines
a unique method bark, that's specific to dogs. Now, I create an instance of the dog class because dog
inherits from animal. Dog is also considered as
an instance of animal, which means it can access all methods and
attributes of animal, as well as those defined
specifically in dog. So here I'm calling the
inherit eat method. This line of code
calls the eat method. Since dog is an instance of dog and dog inherits
from animal, Python looks for the eat
method in dog first. Then Python moves up the
inheritance chain to the parent class animal where
it finds and executes it. And here I call the bark method, which is defined directly
in the dog class. Since bark method is
unique to the dog class, Python doesn't need to check parent class animal at
all for this method. It directly calls bark from dog, and here we have the result. Here we defined the method
bark in the subclass. This method is specific
to the dog class and doesn't exist in the
parent class animal. So here we call the inherited It method from animal class, and here we call
the bark method. That was defined specifically
in the dog class. Let's proceed with
multiple inheritance. Multiple inheritance
is a feature in object oriented programming. That allows a class to inherit from more than one parent class. In Python language, multiple
inheritance is supported, which means you can create
a class that inherit attributes and methods from
multiple base classes. I will extend the
animal and dog classes by introducing a new
class called PAT, which represents behavior
specific to pets like playing. Then we will use
multiple inheritance to create a dog class. That inherits from
both animal and pet. So now what we have, we have the animal class
that has one method it, which prints eating when called. This class represents
general behavior common to all animals, such as the ability to eat. Then we have pet class that has one method play,
which prints playing. This class represents
behavior specific to pets, such as the ability to play. We have the dog class. This class inherits from
both animal and pet. This allows dog to inherit methods from both
animal and pet, so it will have access to it from animal and
play from pet. Additionally, dog defines
a unique method bark, which is specific to dogs and not present in
either parent class. Then we created instance
of the dog class. Since dog inherits from
both animal and pet, the dog instance will
have access to methods from both classes as well
as its own method bark. Here we already
have the method E, we called it, then we
called method bark, and now I just add method play. The method from the PED class. As I said before, since dog
also inherits from PED, it can use this method too. And if I run this code, we will clearly see the result. In multiple inheritance, Python
needs a way to determine the order in which it searches parent
classes for methods. This order is known as method
resolution order, MRO. The MRO defines the sequence in which Python will look up
methods and attributes. In this example,
the dog class will first look in its own
class for a method, then in animal, and
finally in PAT. Following the order, they are listed in the Dog
class declaration. You can see MRO
for a class using the MRO attribute or MRO method
like this, I'll show you. Here we see the dog. First, the animal, the
second, the third, we see PAD glass, and at the end class object. Why order matters? If both animal and pet define a method with the same
name, let it be sound. Python would use the MRO to
determine which one to call. Python uses C three
linearization, also called the C three super class
linearization algorithm to establish the order for MRO. So here I defined two
identical methods, sound, and here I
call this method. The MRO for dog in
this case will be dog, animal, pet, and object. So Python finds and executes
sound in animal first. For now, that's all. See
you in the next lesson.
33. OOP in Python: Multilevel and Hierarchical Inheritance: Welcome back guys. In
the previous lesson, we learned about
single inheritance and multiple inheritance. We also learned about
method resolution order, which determines the order in which base classes are searched
when executing a method. Now I'm going to explain what
multi level inheritance is. Multilevel inheritance
is a type of inheritance in which a class is derived from another class, which is already derived
from some other class. This forms a chain of
inheritance where each subsequent class inherits
from its predecessor. For example, animal
is the base class. Mammal inherits from animal, making it a derived class. Dog inherits from mammal, which makes it a
subclass of mammal and indirectly a
subclass of animal. Let's now write this out
in the form of code. Here I extend the
animal and dog classes and add class mammal. So here I have animal
as the base class. It has single method ET. Any class that inherits from animal will have access
to this ET method. Then I created mammal is a subclass that
inherits from animal. By inheriting from animal, the mammal class gets all
functionality of animal, which means mammal has
access to the eat method. Additionally, mammal
defines a new method called breath, which
outputs breathing. And eventually, we have dog is a class that
inherits from mammal. Since mammal inherits from animal and dog
inherits from mammal, the dog us inherits all the methods from
both mammal and animal, and dog has its own
method called bark. So here I created
an instance of dog. The dog object has access to all the methods defined
in its class hierarchy, which includes methods from
dog, mammal, and animal. The first I call eat method, Python checks if dog
has an eat method. If not found, it
looks in mammal, the parent class of dog. If not found there either, it looks in animal, the parent class of mammal. The Mod is found in animal, so eating is printed. Next, I call breath method. Python checks if dog
has a breath method. If not found, it looks in
mammal. The method is found. Mammal, so breathing also is printed and with bark
method, very easy. The method is
defined directly in the dog class. Here
we have barking. We can see the method
resolution order by using the Amro method. In the previous explanation, we used MRO to check the
method resolution order of the Dog class using MRO
method with underscores. Both approaches provide
the same MRO information, but in different
data structures. This can affect
how you work with the results such as
iterating over them or modifying them because the first one returns a
couple of classes. And in our case right now, it returns a list of classes. So it depends what you
are going to do next. Let's return to the
multi level inheritance. And it will be good to use. Multi level inheritance
allow you to logically group
related behaviors. For example, all
animals can eat, but only mammals can breathe. You can easily extend the hierarchy by adding
more subclasses. For instance, we could
add a cat class that also inherits from mammal but has
its own specific behaviors. Multi level inheritance allows
you to build a chain of classes where each class
inherits from its predecessor. It's useful for creating a structured and reusable
class hierarchy, but should be used with
care to avoid complexity. Another type of inheritance
I would like to introduce you is
hierarchical inheritance. Hierarchical inheritance is
a type of inheritance in which multiple
subclasses inherit from a single parent class. This means that one base
class parent can have multiple derived
classes children that inherit its
attributes and methods. For example, here, the animal in the base class parent class, cat and dog are subclasses. Both cat and dog
share the properties of animal but also have
their own unique methods. Let's take a look at
this in practice. So here I removed the class
mammal and add the cat class. Animal will be the
parent class for both cat class and dog class. Cat is derived class that
inherits from animal. This means that cat
automatically has access to all the methods defined in
animal like eat method. Additionally, cat defines
its own method, Mo. In dog class, I replace
mammal to animal because dog is
another derived class that inherits from animal. Like cat, dog class also inherits the eat
method from animal, and also additionally dog defines its own
unique method bark. Then I create the instance
of the dog class. The dog object can call eat
method inherited from animal, and it can also call
its own method bark. Then I create an instance
of the cat class and the cat object can also call the method ID that was
inherited from animal, and it can also call
its own method Mu. As we can see, both cat and dog classes inherit the eat
method from the animal class. This allows for d reuse. The eat method is defined only
once in the animal class, but can be used by any subclass. While both cat and dog
share the eat method, each class also has its own
unique method, mile and bark. This allows each
subclass to have specific behaviors in addition to the shared functionality. This structure promotes
a clear hierarchy where multiple subclasses can extend the functionality of
a single base class. This makes it easier to manage related classes that share
some common behavior, but also have their own
distinct behaviors. Common methods and
attributes can be defined in the base class and re
used by all subclasses. This reduces code duplication and enhances maintainability. Hierarchical
inheritance allows you to logically groups classes
that share common behavior. But we should remember that subclasses are tightly
coupled with the base class. If the base class
animals changes, all subclasses, cat,
dog, may be affected. If a subclass needs to override a method
from the base class, it can lead to confusion, especially if not
documented properly. Here we will continue
with method of writing, what it is and how
we can do this. See you in the next lesson.
34. OOP in Python: Composition: Welcome back, guys. Now let's talk about composition and OP. In object oriented programming, Op composition is a
design principle where one class is composed of the one or more object
from other classes. This achieved by including
instances of one class within another class as its attributes rather than using inheritance. When we learned inheritance, we clearly saw that these
models is a relationship. For example, a dog is an animal. It allows one class to inherit attributes and methods
from another class. In composition, we have
models has a relationship. For example, a car
has an engine. Instead of inheriting
from another class, the main class contains an
instance of another class. Let's look at this using the code from the
previous lesson. I slightly rewrite
my original example to use composition
instead of inheritance. The animal class
remains the same as before with a name
attribute and it method. Instead of inheriting
from animal, the dog class now
contains an instance of the animal class as an
attribute, self animal. Now self animal is an
attribute of the dog class. This attribute is assigned an instance of the animal class. When creating this instance, we pass name as an argument
to the animals classes init method because the animal class expects a name parameter
when it's initialized. This means that when we create a new animal instance
inside the dog class, we must provide a name value, which will be used to set the self name attribute
of that animal instance. Because the dog class creates an animal instance during
its own initialization, the dog class must also
accept a name parameter. This is why dog classes. Method looks like this. Here we add name. When you create a dog object, you now need to provide a name, an age, and a breed. The dog class has its
own attributes like age and breed in addition
to the animal attribute. In the dog class, we have an attribute self animal that holds an instance of
the animal class. Self animal name accesses the name attribute of the animal instance
contained within dog object. We use it in formatted string to produce a
personalized message. By calling self animal eat, we are delegating the
eating behavior to the animal instance contained
within the dog object. When I create an
instance of dog, I pass in the name,
age, and breed. The name is used to
create an animal object, which the dog class
uses internally. So after all of this, what
benefits did we gain? Changes to the animal class are less likely to
impact the dog class. You can easily
swap out or modify the behavior of the animal class without affecting the dog class. For example, you can
replace animal with another class or add more functionality without
changing the dog class. By using composition, you're following the single
responsibility principle. The single responsibility
principle states that a class should have
only one responsibility or reason to change. This means each class should focus on a single task
or functionality, making the code easier to understand, maintain,
and extend. The animal class is responsible for general animal behaviors, while the dog class handles
dog specific logic. With inheritance, the dog class was tightly coupled
to the animal class. If the animal class changes, it may break the dog class. We also had less flexibility. It's harder to change
or extend behavior, especially if you have a
deep inheritance hierarchy. I showed you an example of
composition by refactoring the animal and dog
code so that you could better understand the difference between
the approaches. However, the example with animal and dog is not the best fit because the relationship
between a dog and animal is more suited
for inheritance. Composition is when we build an object from
different components. It's like a car made
up of various parts. Now imagine that you need
to write code from scratch, where you would
apply composition. Let's create a very
simple example of composition using a car
and engine scenario. In this example, the car class will use an instance
of the engine class. The engine class has
a type attribute and a start method to print a
message when the engine starts. This class is independent and can be reused in other contexts. Then we have car class that has a model attribute and
an engine attribute. Instead of inheriting
from engine, it contains an
instance of engine. The car class has its
own start method. I created an instance
of the engine class. Then I create a car object with the model Mustang and the previously created
engine object, and then I call start method. This method called
on the car object. If I run this code, I will get the message
starting the Mustang car. For now, in the car class, we have self engine attribute, but it's not just attribute. It's an instance variable. That was initialized when
a car object was created. This self engine
holds a reference to an engine object that was passed as an argument
to the car constructor. When we create an instance
of the car class, we provide an engine object
to the engine parameter, and this engine object is then assigned to the self engine attribute of the car instance. As a result, self
engine refers to the engine object that was
passed when car was created. After the engine object is assigned to the self
engine attribute, the line self engine start calls the start method of
the engine object stored in the self engine. This is an example
of delegation, where a car class delegates the task of starting the
engine to the engine class. And here we see that after the engines start
method is executed, this line is executed in
the car class itself. It prints a message indicating
that the car is starting, including the model of the car. By passing the engine as a parameter during the
creation of the car object, you gain flexibility in how and when the engine
object is created. You might want to create
a custom engine or modify the engine before
passing it to the car. For example, you could add extra configuration
to the engine before using it in the car. If the creation of the
engine is done separately, you can have more control
over its life cycle. For instance, you
could decide to reuse the same engine across multiple cars without
duplicating the creation logic. Let's summarize and
identify all advantages and disadvantages of both approaches inheritance
and composition. Let's start from inheritance. Inheritance models
is a relationship, meaning the child class is
a type of the parent class. For example, if you
have a class animal and a class dog that
inherits from animal, then dog is an animal. This implies that the
subclass should be able to do everything the parent class
can do plus possibly more. It's a hierarchical
relationship where the subclass is a specialized version
of the parent class. Composition models
has a relationship, meaning that one class has another class as part
of its structure. For example, car has an engine. The car isn't the
type of engine, but rather uses an engine
as one of its components. This means that the
composed object like the engine in a car is just a part of the
larger hole and can be replaced or changed
independently of the main class. Inheritance is less
flexible because it tightly couples the child
class to the parent class. Any change in the parent class can ripple through
all subclasses, making it harder to
modify or extend your code without affecting
existing functionality. For example, if I change a
method in the animal class, it might break or change the
behavior of the dog class, especially if the
method is overridden. Composition is more flexible because it loosely
couples the components. The main class can contain
and use instances of other classes without being dependent on their
internal implementation. This means you can
easily change or replace parts of your
system like switching out an engine object
in a car class without affecting other overall
behavior in your class. Inheritance allows code
reuse by subclassing, where the child class inherits methods and attributes
from the parent class. This can be efficient, but can also lead to issue
if the hierarchy gets too deep or if you inherit
unwanted behaviors. For example, if a subclass only needs a few methods
from the parent class, but inherits everything, it may lead to
unnecessary complexity. Composition promotes code reuse through
object delegation, meaning that a
class can delegate specific tasks to its
component object. This allows you to combine
functionalities from different classes without
inheriting everything. This way, you can mix and match different components to
create new behaviors, making your system more
modular and reusable. When we use inheritance, changes in the parent class have a broad impact on
all subclasses. If you modify method
in the parent class, it affects every subclass
that inherits that method, which can introduce bugs
or unexpected behavior. In composition, changes
are more isolated. Since classes are
independent and communicate via their
public interfaces, modifying one components does
not directly affect others. So inheritance
best used when you have a clear hierarchical
relationship between classes. Like bird is an animal or
manager is an employee. It's suitable when you want to take advantage of polymorphism, like in cases where subclasses share a lot of behavior with their
parent class, but may also extend or
override some methods. Composition ideal for cases
where you want to build complex behaviors by combining simple
independent objects. For example, a robot that has multiple interchangeable
parts like battery or arms. It's great for scenarios
where you want to follow the single responsibility
principle and keep your system flexible,
modular, and maintainable. I hope I have explained
the difference in detail. For now, that's all, see
you in the next lesson.
35. OOP in Python: Polymorphism: Welcome back, guys. Let's get acquainted with the
concept of polymorphism. Polymorphism is a
fundamental concept in object oriented
programming that allows objects of
different classes to be treated as objects
of common superclass. It literally means many
forms, and in programming, it refers to the ability to
use a single interface or method to represent different
types or behaviors. In Python, polymorphism
is implemented through method overwriting
and method over lodging. However, Python
doesn't support method overloading directly like some other languages
such as Java, but polymorphism can still be achieved through dynamic typing, inheritance, and
method of writing. We already know that method of writing occurs when
a subclass provides its own implementation
of a method that is already defined
in its superclass. In Python, the subclass method replaces the parent
class method, but we can still call
the parent class method using super function. Let's break this down
with a simple example. So I start by defining a
base class called animal. Inside the animal class, I define a method sound. When this method called, it prints the string some
generic animal sound. This method is intended
to be overridden by subclasses to provide
specific sounds for different types of animals. Then I create dog class and it inherits from
the animal class. In this subclass,
the sound method is overridden to print sound, which is specific for the dog. When we create an instance of the dog class and called
the sound method, it will print a woof instead of generic animal sound defined
in the animal class. Then I define the cat class, which also inherits
from the animal class. And here we also
have sound method. Similarly to the dog class, the cat class overrides
the sound method, but this time to print mio. I create a list called animals, which contains instances of
three different classes, dog, cat, and animal. Dog creates an instance
of the dog class, where sound method prints wolf. Cat creates an instance
of the cat class, where sound method prints Mo and the animal
creates an instance of the animal class where sound method prints some
generic animal sound. I look through the
list of animals, and for each object animal, I call the sound method. Yes, we can do like this. When I call animal sound, Python will
dynamically determine the actual type of the object, dog, cat, or animal at run time, and invoke the
corresponding method. This is polymorphism
or method of riding, where the method sound behaves differently based
on the object type, even though it's called in
the same way for all objects, first iteration, and
we have dog instance. The first object in the list is an instance of the dog class. The first iteration,
we have dog instance. The sound method
of dog is called, so it prints woof. The second object in the list is an instance of the cat class. In the second iteration, we have cat instance. The sound method of
cat prints mile. And the third object is an
instance of the animal class. So in the third iteration, the sound method of
animal is called, so it prints some
generic animal sound. Polymorphism allows
the same method sound to behave differently depending on the type of the object. In this case, even
though we are calling the same method sound for
all objects in the list, the actual behavior that gets printed depends on the
specific class of each object, dog, cat, or animal. Here we have the dog and cat classes that inherit
from the animal class, which means they inherit
its method, sound. However, they override
the sound method to provide specific
implementation. The sound method is called on the same way on all objects. But the actual method that gets executed depends on the
type of the object, which determine at runtime. The sound method gives different results based on the
type of object calling it. This makes the code more
flexible and reusable because different classes can use the same method name but have
their own unique behavior. Polymorphism helps make Python code more
flexible, reusable, and easier to maintain, as it allows the same
method to handle different types of objects in
a clean and efficient way. For now, that's all. See
you in the next lesson.
36. OOP in Python: Encapsulation: Welcome back guys. Let's dive into the concept
of encapsulation. Encapsulation is one of the fundamental principles of object oriented programming, along with inheritance,
polymorphism, and abstraction. In general, encapsulation means combining data
like variables and the methods like
functions that works on that data into one unit
which is called a class, and it also restricts direct access to some of
the objects components, which can prevent the accident
modification of data. This is achieved through access modifiers that control the visibility of
the class members. In Python, encapsulation
is done using classes. You can control access to the data by using
different access levels, public, protected and
private, public members. These are accessible
from anywhere, both inside and
outside the class. Then we have protected members. These are indicated by single underscore prefix
and they're meant to be accessed only within
the class and its subclasses and
private members. These are indicated by double underscore
prefix and are not accessible directly
from outside the class. So let's dive into this concept with
detailed explanations. By default, all members attributes and methods in
Python class are public, which means they can be accessed
from outside the class. For example, here I define
a new class named car. This class has a
constructor method that's automatically called when an instance of the
class is created. Brand and model are parameters that we pass when creating
a new car object. Self brand and self model are public attributes
of the class. Python, as I said before, all attributes are
public by default, which means they can be accessed and modified from
outside the class. The values passed as arguments brand and model are assigned to
these attributes. For example, if we create the car object with
car Toyota Camry, then self brand will be Toyota and self
model will be Camry. Then I create the
method display info. It's a public method, meaning it can be called
from outside the class. This method prints the
car's brand and model using the public attributes
self brand and self model. Then I create instance
of the car class. I pass Toyota at the brand and Camry at the model when
creating this object. As we know, the it method
is automatically called initializing car one brand to Toyota and car one
model to Camry. And then I call
the public method display inflow on
the car one object. This method uses the
value stored in the car one brand and car one model
to display the information. And the main thing we
should remember right now, since brand and model
are public attributes, we can access them directly
using the dot notation. So here, car one dot brand and car one dot model,
print the result. So here we have
the example where brand and model are
public attributes, and I can access and
modify them directly from outside the class using
the instance of the class. And here's how I can do this. Of course, here I
can replace model. The method display
in for is public, meaning it can be
called from outside the class of any
instance of the class. Now we directly modify
the public attributes, brand and model of the car one object because
attributes are public, they can be modified freely from outside the class
without any restriction. So after this, we have brand
Honda and model Accord. Now, if we call display in
for method again on car one, the method will print
totally different message. I mean, we will get new
brand and new model. So quick recap, public members, attributes and methods are fully accessible from
outside the class. This is useful for
attributes and methods you want to expose
for general use. However, if you need to protect certain data from being
accessed or modified directly, you would use protected
or private members. I will comment out
the previous code, and let's continue with
protected members. So here we have totally
different logic. I create class person the person class
represents a person with two protected
attributes name and age. The living underscore
in the attribute names, name and age is a convention to indicate that these
are protected members. This convention tells
other developers that these attributes
are meant for internal use and shouldn't be directly accessed from outside
the class or subclasses. Here we have method display info and it's also a
protected method. It follows the same
naming convention with a leading underscore. It prints out the name
and age of the person by adding the protected
attributes name and age. Then I create the employee class that inherits from
the person class. It means that it can use the attributes and methods
of the person class. The NIT method in the
employee class calls the NIT method of the person
class using super function. This initializes the
name and age attributes. The employee class
introduces a new attribute, employee ID, which is
specific to employee. The method show details in
the employee class calls the protected
method display info from the parent person class. This is allowed because
the method is protected, which means it can be accessed from the
subclass employee. So here we are calling the protected method
of the parent class. Show Details prints
the information about the employee's name, age from the parent class, and the employee's ID. Then we create an instance
of the employee class named P and passing John at the name 30 at the H
and e123 at the employee ID. The constructor of
the employee class calls the person classes
constructor using super setting name
to John and he to 30 the employee
ID is set to e13. Then I call the method show
details on the MP object. Inside show details, the method
display Info method from the person class is called which prints name John and H 30. After that, the employee's
ID e123 is printed also. So the main point in Python protected members
are attributes or members that are
intended to be used only within the class
and by its subclasses. They are indicating by a single leading underscore before the attribute
or method name. Protected members are
not strikely private. Python's naming convention with a leading underscore
signals to other developers that these members are meant for internal use and should not be accessed directly from
outside the class. However, this is
only a convention. Python doesn't enforce
access restrictions. As you can clearly see
here, in the person class, we had two protected
attributes name and age and a protected
method, display info. These members are
not meant to be accessed from outside
the class directly, but they can be
accessed if needed, since Python doesn't enforce
strict access control. So I create an instance of
the person class, person one. And I access person one
name and person one, each directly outside the class. Even though it's
generally considered bad practice to access protected members
outside the class, but then allows it
because it doesn't have strict enforcement
mechanism like other languages like
Java or C plus plus. I also called display info director from
outside the class. Again, it's allowed, even
though it's protected. So why we should
follow the convention? If I follow the
convention and treat protected members as
internal to the class, it keeps the design of the
code clean and modular. External code should not rely on the internal
structure of a class because this makes future changes to the
class more difficult. By following this convention, you prevent the internal
details of the class from being accessed
or changed directly, which helps avoid bugs. So quick recap. In Python protected members
are a convention, not an enforced rule. You can access them outside
the class or subclass, but doing so breaks the intended structure and can lead to bad software design. Let's proceed with
private members. I comment out previous code. I create the bank
account class that has two private attributes
account number and balance. Private members in Python are prefixed with
double underscores. I also create the
method display balance and it's a private method too. It's only intended to be
used within the class. The double underscore prefixes
also applies to methods, which means this method is not accessible from
outside the class. Then I create the
withdrawal method. It's a public method. It can be accessed from
outside the class. This method interacts with the private attributes balance and calls the private
method display balance. It checks if there
are sufficient funds for the withdrawal. If, yes, it deducts the
amount from the balance. Otherwise, it prints
insufficient funds. After processing the withdrawal, it calls the private
method display balance to show the updated balance. I create the instance
of the bank account with account number and
initial balance of 1,000. When I call account
withdraw 200, it will deduct 200 from
the initial balance 1,000, resulting in a new
balance of 800. If I run this code, we'll see a message showing that
200 was withdrawn, leaving a balance of 800. If I try to access
account balance or account display balance
from outside the class, it will raise an attribute error because these members are private and private
members are attributes or methods that are intended to be hidden from
outside access. The same error I
will get if I try to access the private
method display balance. By keeping attributes private, you can provide
controlled access through public methods
like withdraw, ensuring that the data
remains in a valid state. Despite the fact
that Python uses private attributes to enforce the principle of encapsulation, where the internal state of an object is protected
from direct modification. PyTN also has a mechanism
called name mangling that allows you to access these private attributes
in a special way. This means you can still access the private attributes if
you know the mangled name, but it's considered bad practice because it breaks the
encapsulation principle. This line of code allows to access the private attribute
using its mangled name, and it's one underscore, class name, to underscore
attribute name. Yes, it's harder, but it's possible to access
outside the class. And this mechanism is
called name mangling. So quick recap about
private attributes. In Python, they
are created using double underscore and they are meant to be hidden
from direct access. But Python uses name mangling to change the name of
private attributes, making them harder, but not impossible to access
from outside the class. You can still access private attributes
using the mangled name, but it's bad practice. The preferred way
to interact with private data is through
public methods, which ensure data integrity
and maintain encapsulation. For now, that's all.
In the next lesson, we will cover
getters and setters, see you in the next lesson.
37. OOP in Python: Getters, Setters, and Properties: Welcome back, guys.
Let's dive into detailed explanation
of encapsulation using Getters and
errors in Python. We already know what
encapsulation is, Encapsulation also
provides a way to control access to the data by making certain attributes private and exposing them
only through methods. Getters and setters are methods used to access and modify
private attributes of a class. They provide a controlled way to interact with private data, ensuring that the data is
handled properly and securely. The purpose of using
Getters and setters is to protect data from
direct external access and modifications. Let's see how this
works in practice. I start from initialization
class employee, and this class has two private attributes,
name and salary. These attributes are prefixed
with double underscores, making them private and not directly accessible
from outside the class. It's time to use
Getter for name. This method returns the value of the private attribute name. By using a Getter, you can control how the
attribute is accessed. For example, you could add logging or additional checks later without changing
the rest of your code. And after Getter,
we will set setter. This method updates the value of the private attribute name. It directly assigns the new
value without validation, but you could add
checks if it needed. I also create Getter for salary. Similarly, this method returns the value of the private
attribute salary. And setter for salary. This method includes
validation to ensure the salary is positive before updating the private
attribute salary. If an invalid value like
negative salary is passed it prints an error message and
doesn't update the salary. So then I create an
instance of employee class, set the name Alice
and the salary 4,000. And now I can use Getter. I will use Get name. Method retrieves the
name of the employee. Let's test setter for salary. This method updates
salary to 6,000, and also I will use Gary Getter. This method retrieves
the updated salary. So when I run this code, the get name method is called, and it returns the
private attribute name. Since name was said to Ellis, the output is Allie. Then the set salary
method is called with 6,000 at the argument. Since 6,000 is
greater than zero, the setter updates the
private attribute salary to 6,000 and then get
salary method is called, and it returns the updated value of the private attribute salary. Since the setter updated
salary to 6,000, the output is 6,000. So quick recap, Getters
and setters are methods that provide controlled access to
private attributes. They help in achieving
encapsulation, data validation and flexibility. Use Getters to retrieve
private data and setters to modify it with
optional validation. Removing setters can make
an attribute read only while removing getters
can make it we only. This approach ensures that
your code is cleaner, more secure and
easier to maintain. By using Getters and setters, you can protect
the internal state of your object and ensure that data integrity is
maintained throughout your application.
But that's not all. In Python, you can use the
property decorator to simplify the creation of getter
and setter methods for private attributes. This allows you to access and modify private attributes in more Pythonic way without explicitly calling Getter
and Setter methods. The property decorator
allows you to define methods that act
like attributes, giving the illusion of direct attribute access
while still allowing you to define custom behavior for getting and
setting the values. This approach is
more concise and clean compared to the traditional
Getter setter method. Simple property decorator is used to create a getter method. It allows you to access
a private attribute in a controlled way without
calling a method explicitly. Setter decorator uses
slightly different signature. It's used to define
a setter method. This allows you to modify
a private attribute while also adding logic
such as validation. So a slightly modified
employee class example to use property decorators
for getters and setters. Here after creating an
instance of employee, I can access private
attribute via Getter. Name. Then I can modify
private attribute via setter and I set
6,000 valid salary. I also can access
the updated value. If I attempt to set
an invalid salary, I will have the message
invalid salary. What are the advantages of
using property decorators? Cleaner syntax. You can access and modify
attributes using the same syntax as
public attributes without explicitly calling
Getter and Setter methods. The internal state of the
object remains protected and you can control access to it through
Getters and setters, adding validation
logic if necessary. You can easily add
or modify logic in the Getter setter
methods without changing the external
interface of the class. Using property decretors in
Python allows you to achieve Getter and Setter behavior in more concise and elegant way. It simplifies the code and this approach is cleaner
than manually defining getter and setter methods and helps maintain good
encapsulation practices. For now, that's all. See
you in the next lesson.
38. OOP in Python: Aggregation: Welcome back, guys. Today we
will look at aggregation. Both aggregation and
composition are types of associations that
define relationships between objects in object
oriented programming. Composition we discovered
in the previous lesson. Now let's continue
with aggregation. Aggregation has a relationship where one object can
contain other objects, but those objects
can still exist on their own separate
from the container. The contained objects can exist even if the container
object is destroyed. For example, consider a
university and its students. If the university
object is deleted, the student object will
still exist independently. Let's continue with example. I'm going to create class book. It has the method in it is a special method in
Python, constructor. It initializes a
new book object. Each book object will
have a title attribute. Then I create class library. It also has constructor. It creates an empty
list named books. This list will be used
to store book objects. The library class has a
list of book objects, but it doesn't own them. The books can exist
independently of the library. Then I define a method named at book in
the library class. The method takes two arguments, self, the instance of library, and book, instance of book. This method adds the book object to the book list within
the library instance. So then I created two book objects with
the different titles. Then I create library
object named Library. The method addBook is called twice to add Book one and Book two to the
library's books list. What we have now, we have
library with several books. Let's add the method
display books. This method allows you to see all the books that have
been added to the library. This method simply checks if there are any books
in the library. If books are present, it displays their titles
using a four loop. Self books is a list that
belongs to the library object. It was empty before
we add two books. Each item in this list
is a book object. The line for book in self
books means take each item from the self books list one by one and assign it to
the variable book. The loop will
continue until there are no more items
left in the list. In our example,
this happens twice, and then we print
the book title. Here we have the title attribute of the current book object. We already know
that we can access the attributes of an
object using dot notation. This means you can refer to an object properties
and methods by writing the object's
name followed by dot and then attribute name. In our case, we
have book object, and also we have title like it's attribute. Let's test it. If I now run this code, we will get the list of books. Let's delete the library. Method deletes the library
object from memory. This means that the library
object and its attributes, like the list of books is contained are no
longer accessible. Despite deleting the library, the Book one and Book two
objects still exist in memory because they were created independently
of the library, and we can access it. For example, here I
print book one title, and here we have the result. I'll comment out the
previous unnecessary code because we don't have the
library object anymore, and here we clearly
see the result. And this demonstrates
aggregation. The library has a
collection of books, but it doesn't own them. The book objects are not destroyed when the
library is destroyed. I still can work because book one exists independently
of the library. Book two exists
and we can use it. The library class
contains a list of book objects, self books. However, these book objects are not tightly bound
to the library. They can exist on their own
independent of the library. And on the other hand,
the book objects, Book one and Book two are independent of the
library object. Deleting the library object doesn't affect the
existence of book objects. This code structure helps in designing modular and
flexible programs where components like
books can be reused independently of the main
container like a library. Quick recap,
aggregation used when the one object uses another object temporarily
and doesn't own it. Composition used
when one object is completely dependent on
another for its existence. The contained object cannot
exist without the container. For example, human,
heart or car and engine, but the overall
approach in code for both composition and
aggregation is similar. Both use classes containing
references to other objects. The key difference is
that in composition, the contained object depend entirely on the parent and
are destroyed with it. While in aggregation, the contained objects can exist independently
of the parent. I hope I was able to clearly
explain the difference. For now, that's all. See
you in the next lesson.
39. OOP in Python: Abstraction: Welcome back, guys.
Now, let's dive into the concept of abstraction in object oriented programming. Abstraction is one of the fundamental concepts
that helps in maintaining the complexity of code by hiding unnecessary details and exposing only the essential features. Input and abstraction
allows you to focus on what an object does rather
than how it does it. It's the process of hiding the implementation details
of an object or function, and showing only the necessary and relevant features
to the user. This is the concept of
modeling real world objects in a way that focuses on their essential
attributes and behaviors, ignoring any relevant details. For example, consider
a car object. You only need to know
how to start the car. You don't need to understand
the internal mechanism. In programming,
abstraction helps us to represent
these objects and their interactions without going into the complexity of their
underlying implementation. In Python, abstraction
is achieved by using abstract classes and
abstract methods. Abstract classes, these are classes that cannot be
instantiated directly. They can only be used as a
blueprint for other classes. Abstract methods, methods
that are declared, but contain no implementation. Any subclass inheriting from an abstract class must provide an implementation
for these methods. To implement
abstraction and Python, we use the ABC abstract
base class module, which provides the ABC class and the abstract
method decorator. Let's see how this
works in practice. So the first step, I import the abstract base
class from ABC module, and also I import
abstract method. Then I define an
abstract class by inheriting from ABC class. Here's I define abstract method. It will be make sound. Right now we have animal
like abstract class and make sound like abstract
method with no implementation. Now I'm going to create
subclasses that inherit from the abstract class and provide implementations for
the abstract methods. I create the dog class. Here I implement Mesund
then I create cat. Cat inherits also from the
abstract class animal. These two classes provide implementation of the
make sound method. Then I create instances of
the subclasses, dog and cat, and I print make sound for dog instance and print make
sound for cat instance. If I run this code, we see that the
output is different. Trying to instantiate
the abstract class directly will raise an error. I cannot do like this. I cannot create an instance
of an abstract class directly because it contains one abstract method that
has no implementation. Trying to do so will
result in a type error as Python doesn't know how to execute this
implemented method. By inheriting from
an abstract class, subclass is saying
I will provide the specific implementation for all abstract methods
of the parent class. Once the subclass implements
the abstract methods, you can create an instance
of the subclass and use the complete behavior defined by the abstract class and
its own additional logic. Let's consider this
with another example. I commented out previous
code so I create account. It's an abstract class
that inherits from ABC. This makes account an
abstract base class. The need method initializes an instance with an
initial balance attribute, which represents the
money in the account. This balance is stored as an instance variable
self balance. And then I create two abstract methods
deposit and withdraw. These methods are defined but not implemented in the
abstract class account. The abstract method
decorator ensures that these methods must
be implemented by any non abstract
subclass of account. Deposit a method that will increase the account balance
by the given amount. Withdraw a method that will decrease the account balance
by the given amount. Even our funds are available. Since account is
an abstract class, these methods are
only decorations and no functionality
is provided. Any class inheriting from
account must implement these methods to define the actual deposit and
withdraw behavior. Then I create saving account is a concrete class that
inherits from account. It provides specific
implementation for the deposit and
withdraw methods. The logic here is when deposit
is called with an amount, the balance of the account
is increased by that amount, and then we print
the updated balance. The withdrawal method
checks if the amount to be withdrawn is less than or
equal to the current balance. If there are sufficient funds, the amount is subtracted from the balance and the updated
balance is printed. If there are insufficient funds, the message is printed
to indicate this. This implementation of
saving account ensures that users cannot withdraw
more than balance available, enforcing the rule
for saving accounts. Checking Account is another
concrete subclass of account, but it implements the
deposit and withdraw methods differently than saving account. The deposit method works the
same as in saving account, adding amount to the balance and printing the new balance. In this case, the
withdraw method doesn't check for
sufficient funds. Instead, it allows overdraft, meaning the balance
can do negative. When amount is withdrawn, it's simply subtracted
from the balance, even if the balance
becomes negative. This behavior represents a
typical checking account where overdraft are allowed, and the bank might
provide a credit limit or charge fees for
such transactions. The abstract class account
enforces that any subclasses, like saving account
or checking account, must implement the deposit
and withdraw methods. This way, all account types
have a consistent interface, allowing you to handle them
the same way in your code. So you can deposit or withdraw
from any type of account. The abstract class provides
a common interface, while the subclasses
implement the specific logic. The structure makes the code easier to maintain and extend. For example, if
you wanted to add another type of account
like business account, you could easily
do so by creating a new subclass that implements
the required methods. A saving account object starts
with the balance of 1,000. You can deposit and withdraw, but withdrawals can
exceed the balance. Here we had 1,000,
then deposit 500. And then I try to withdraw 2000. And if I run this code, we will see the message
sufficient funds. While a checking account object starts with also 1,000 as well. But it allows overdrafts. So you can withdraw
more than balance, resulting in a negative balance. And if I run this code, I will get an error because sorry misspelling,
that's much better. Now it works. This
code shows how abstract classes can
be used to create a general template for
different types of accounts. It allows flexibility
by letting subclasses customize specific methods
like deposit and withdraw. This approach uses
polymorphism and ensures that all subclasses
implement necessary methods, making this code more reliable
and easier to maintain. So quick recap. Abstraction is a core concept in object oriented programming
that focuses on hitting complex
implementation details and showing only the essential
features to the user. It allows you to define
a blueprint for a class, specifying what
methods should do, but not how they should do this. In Python abstraction
is achieved using abstract classes and
abstract methods. An abstract class is a class that cannot be
instantiated on its own. And is used only as
base for other classes. An abstract method is a method then declared abstract class
without any implementation. Subclasses must provide their own implementation
of these methods. Abstraction helps to reduce complexity by simplifying
the instruction with objects and force a
consistent interface for subclasses and encourage
code reuse and modularity, making the code easier
to maintain and extend. Congratulations on
completing this section on object oriented programming. Let's move on to other topics.