Hello World!
<class 'str'>
Zero to Py
Part 1: A Whirlwind Tour
Chapter 0: System Setup
Skipping this section as I have a working instance of python install and feel comfortable using it.
Chapter 1: Fundamental Data Types
Variables
To reference a given value in python you do so by assigning values to variables using the assignment operator =. Variables are references to values and the data type of the var is the determined by the value it is assigned to.
Variable Naming Rules
- Can only contain letters, numbers and underscores no specials characters such as
!, @, #, $
- Must begin with a letter or underscore
- Names are case-sensitive so
x
is different thanX
- Python has some reserved words that cannot be used for names such as
False
,True
,None
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
In python there is no concept of constants meaning that variables can be reassigned whenever changing the value they reference. A common naming convention for vars that are suppose to be constant is to name the var in all caps PI = 3.14
Strongly Typed Language: Operations which you can do on variables are dependent on the variable’s type.
Primitive Data Types: also referred to as scalar data types, represent a single value and are indivisible. Examples include
bool
,int
,float
.
Container Data Types: also known as non-scalar data types, represent multiple values, and are divisible. Examples include
list
,tuple
,set
,dict
.
str
is a special data type that has properties of both primitive and container data types.
Mutability: refers to the ability to be able to modify the value after they are created.
For example, a list
in python is a mutable data type where you add, remove or change elements in a list after it is created. On the other hand a tuple
is an immutable data type, so you cannot modify anything about it after it is created.
tuple
immutable ordered collection of items (1, 2, 3)
list
mutable order collection of items [1, 2, 3]
set
mutable unordered collection of unique items {1, 2, 3}
. You can use frozenset(my_set)
to create a immutable set.
Objects
Everything in python is an object, which means that each element in the language is an instance of a specific *class. This includes built in data types such as str
, int
, list
.
When a variable is assigned a value in Python, that value is a reference to an object in memory. This reason for this is because the standard implementation of Python is built in C-based infrastructure, where the basic unit of memory that holds the value of a python object is a C structure called a
PyObject*
. Thisstruct
contains information about the object’s type, its reference count, and the actual value.PyObject*
is a pointer toPyObject
and it can be used as an opaque handle to the python object, allowing the implementation to abstract the details of these objects and providing python developers a consistent way to interact with any kind of python object, regardless of its type.
Each object in python has its own unique id assigned to it when created. You can use the built in id() function to find out what the id is for a particular object
This is interesting, I thought these two values would have the same id. I then read the next paragraph which calls out specific values in Python that are singletons meaning there will only ever be a single instance of this object. The integers -5
through 256
are singletons along with None
, True
, False
.
References to Objects
In python, variables are references to objects meaning when you create a variable and assign a value to it, the variable does not store the value directly, instead stores a reference to the object that contains that value. It is important to note when working with immutable data types if you assign the value through a second variable both variables reference the same mutable object in memory.
Chapter 2: Operators
Operators: Special symbols in Python that carry out various operations.
Operand: The value a given operator operates on. For example:
4 + 5 = 9
4
and5
would be the operands.
Python supports various types of operators: arithmetic, assignment, comparison, logical, membership, bitwise, and identity.
Arithmetic
- Used to perform mathematical operations.
- The order of these operators follow the same rules in math PEMDAS.
- It is recommended though to use parentheses to make the code more readable.
The most common arithmetic operators:
+
addition-
subtraction*
multiplication/
division//
floor division%
modulation**
exponentiationPython unpacking operators unpack a sequence into multiple arguments
my_tuple = (1,2)
print([0, *my_tuple, 3, 4])
dict_one = {"fizz": "buzz"}
dict_two = {"foo": "bar"}
print({**dict_one, **dict_two})
[0, 1, 2, 3, 4]
{'fizz': 'buzz', 'foo': 'bar'}
Falsy objects:
False
,None
,0
,0.0
, Empty collections i.e.[]
,()
,{}
,set()
The identity operator is used to compare the memory address of two objects to determine if they are same object or not:
Chapter 3: Lexical Structure
Lexical Structure: The smallest units of code that the language is made up of, and the rules for combining these units into larger structures. In other words, it is the set of rules that define the syntax and grammar of the language and they determine how programs written by developers will be interpreted.
Line Structure
- Each logic line is represented by a
NEWLINE
token. - A logical line is created by combining one or more “physical” lines (the lines in a file you create by hitting the “enter” key.)
- Two or more physical lines in python can be joined into a single logical line using the backslash character
\
Chapter 4: Control Flow
iterable: Any object which has the potential to sequentially yield items per-iteration. e.g. container types (
[]
,()
,{}
)
- Interesting, you can have a
for/else
where theelse
only gets executed if the for loop doesn’t exit early from abreak
statement. (very uncommon to do this and can be replaced by a simpleif
statement after thefor
loop) - Exceptions can be raised which will modify the control flow of the program. Exceptions can be caught using an exception handler
try
except
else
finally
. - Python now has
match
coordinate = (0, 1)
match coordinate:
case (x, 0): # x here means it can be any value
print(f"coordinate is on the y axis, x={x}")
case (0, y): # y here means it can be any value
print(f"coordinate is on the x axis, y={y}")
case _: # _ here is a catch all if it doesn't match any above patterns
print(f"coordinate is on neither axis")
coordinate is on the x axis, y=1
- Interesting to learn about how match works with array types and how you can use unpacking
location = {
"city": "Scottsdale",
"state": "Arizona",
"country": "US"
}
match location:
case {"country": "US", **rest}:
print(f"The city {rest['city']}, {rest['state']} is in the US.")
The city Scottsdale, Arizona is in the US.
- Matching based on types and values
item = 1
match item:
case int(0 | -1):
print(f"{item} is either 0 or -1")
case int():
print(f"{item} is an int, not zero or -1")
case _:
print("This is a catch all")
1 is an int, not zero or -1
guards: Additional conditions for pattern matching can be included with guard statements. Is an
if
statement contained in the case which checks the truthiness of an expression.case (0, y) if y == 1
- The pipe operator can be use to match multiple patterns
|
Chapter 5: Functions
- Function Signature is the combination of the function’s name and its input parameters.
- Positional Argument is a function argument which is assigned a name in accordance to the order which the names are defined in the function signature.
- Keyword Argument is an argument which is explicitly assigned to a specific name using
key=value
. - Positional arguments must be listed first before keyword arguments.
- When assigning default values to a function it is recommended not to use a mutable data type because the same object is used in every function call which effectively creates shared state across all function calls.
- Local Scope or namespace is best explain by an example where variables that are defined in a function block are only accessible within that function block and can’t be referred to outside of the block. The more formal definition is the space where a given variable name is assigned.
- Block scopes can be nested. Any nested block scope can read variables from scopes which enclose the nested scope.
- LEGB rule stands for Local, Enclosing, Global and Build-In. This is the order the interpreter searches for a variable reference in a nested rule for searching scopes.
- *Closures are functions which have access to variables in an enclosing scope even when the function is invoked outside that scope. This allows the function to “remember” the values of variables from its enclosing scope, and to continue to access them, even after the enclosing scope has exited. This is useful for callbacks, decorators, and encapsulating state.
Closures are not really clicking for me. Come back and spend some more time understanding them.
def outer_function(message):
def inner_function():
print(message)
return inner_function # what why would I return a function?
my_closure = outer_function("Hello")
my_closure()
# I really do not understand this pattern and why you would do something like this
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
my_counter = make_counter()
print(my_counter())
print(my_counter())
Hello
1
2
- Closures are ultimately a mechanism for exposing functionality without (easily) exposing state.
- Decorator takes the original function as an argument and returns a new function that will replace the original function. They are a way to modify the behavior of an object by wrapping it within a function.
def my_decorator(fn):
def wrapper():
print("Entering the function...")
fn()
print("Exiting the function...")
return wrapper
@my_decorator
def my_function():
print("Inside the function...")
my_function()
Entering the function...
Inside the function...
Exiting the function...
- Decorators are typically 2 or 3 functions deep. Instances of 3-deep functions allow you to configure the context of the decorator
@my_decorator("this")
by wrapping the decorator in another function (a closure) and returning the decorator function.
Rest Of Book
I ended up reading the rest of the book while I was traveling for the holidays so I didn’t really have a good way to take notes. Here are some highlights I made in Kindle while reading.
Polymorphism: Feature of object oriented programming that allows objects of different classes to be treated as objects of a common superclass… Python is a dynamically-typed language, which allows for a more flexible approach to Polymorphism, know as “duck typing” which is based on the idea of “if it quacks like a duck, it’s a duck.”
- I am not a fan of explaining duck typing with that quote I much prefer the explaination the author gave in the next paragraph
In other words, the type of the object is irrelevant to the execution of your program, so long as you can operate on an object through an expected interface, your code will execute.
The
str.casefold()
method is a more powerful version of the str.lower() method that is specifically designed for case-insensitive string comparisons.
I was not aware of
str.casefold()
!The
__main__.py
will run when you usepython -m my_package
. This must be how venv works e.g.python -m venv .venv
Multithreading is best used for IO-bound and high-level structured network code, while multiprocessing is used for CPU-bound and low-level code.
Thread: Is a separate execution flow that runs in parallel with the main program. Threads share the same memory space as the same program, which allows them to access and share data.
Threads use a preemptive task scheduling model, where the interpreter schedules and switches between threads.
The interpreter can interrupt a running thread at any time, in order to give another thread to chance to run. Python use a Global Interpreter Lock to achieve this: only the thread which has acquired the GIL may execute.
Process: Is a separate execution environment, with its own memory space and Python interpreter. This allows the ability to take advantage of multiple cores on a machine, and to work around the GIL that prevents multiple threads from executing Python code simultaneously.
Processes run in their own execution environment which means its difficult to share data between them. You have to establish an interprocess communication, using IPC mechanisms to send and receive data.
asyncio
library is a redesign how to use threading in python to handle I/O and network related tasks. One of the differences isasyncio
relies on cooperative task switching paradigm where tasks voluntarily yield control to the scheduler. Each tasks is responsible for releasing control back to the scheduler.
coroutines: Tasks in the
asnycio
library which use anawait
keyword to yield control to the event loop. The event loop schedules the execution of coroutines and switches between them based on their current states and the availability of resources.
- Cooperative task switching is more efficient and less error-prone than preemptive task switching, because it allows tasks to run for as they need without interruption. The downside is it requires more explicit coordination between tasks, and it can lead to issues such as deadlocks and livelocks if not implemented correctly. Preemptive task switching can be more complex to implement and it can lead to race conditions and other synchronization issues, but it can also prevent tasks from monopolizing resources.
Profiling: The process of measuring the performance of your code to help identify bottlenecks.
Summary
Zero to Py was good read and it felt like a more digestible way to read through the python documentation. Some areas that I really like that I was less familiar with were: pattern matching, type hinting, multithreading, multiprocessing, async and how to build python c extensions. Without a doubt I will be referencing this book in the future.