every time we use an operator, Python calls the corresponding special
method for us. For example: 1 + 2 equals to
(1).__add__(2) but way more complex and unreadable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
classNumberOperator: def__init__(self, number: int): self.number = number
def__add__(self, other): v = self.number + other.number return NumberOperator(v)
defadd(): one = NumberOperator(1) two = NumberOperator(2) three = one + two print(f"one = {one.number}") print(f"two = {two.number}") print(f"three = {three.number}")
Traditional assignments built with the = operator don’t
return a value, so they’re statements
but not expressions.
In contrast, assignment expressions are assignments
that return a value. You can build them with the walrus operator
(:=).
1 2 3 4
# this is an assignment expression, so it's syntactically correct. defassignment_expression(): while (line := input("Type some text: ")) != "stop": print(line)
1 2 3 4 5 6
# this is a normal assignment, it doesn't return a value, so it's syntactically incorrect defassignment(): # Unresolved reference 'line' # while (line = input("Type some text: ")) != "stop": # print(line) pass
Unpacking
Unpacking an iterable means assigning its values to a series of
variables one by one.
Iterable Unpacking
1 2 3 4 5 6 7
# Once Python runs this assignment, the values are unpacked into the corresponding variable by position. one, two three, four = [1, 2, 3, 4]
a = 200 b = 100 # using the unpacking syntactic sugar, we can swap values between variables without assigning temporary variable a, b = b, a
*args and
**kwargs
1 2 3 4 5 6 7 8 9 10 11 12
# 1 # (2, 3, 4) # {'hello': 'hello', 'world': 'world'} # args will be bound to positional arguments, and kwargs will be bound to keyword arguments defargs_func(number:int, *args, **kwargs): print(number) print(args) print(kwargs)
if __name__ == '__main__': args_func(1, 2, 3, 4, hello="hello", world="world")
# I suppose that there are two different types of arguments in Python's arguments, # The first type is named argument and the other type is anonymous argument. # In this situation, "name" is a named argument, and "hello" and "world" is anonymous arguments, # so "**kwargs" will be bound to anonymous arguments rather than being bound to all arguments defshow_args(number: int, *args, name: str, **kwargs): args_func(number, *args, **kwargs) print(name)
dict1 = {"k1": "v1", "k2": "v2"} show_args2(list1, list2, dict1=dict1) # both of list1 and list2 are bound to *args, it's worth noticing that each list is an entirety which being passed to *args # ([1, 2, 3, 4], [3, 4, 5, 6]) # {'dict1': {'k1': 'v1', 'k2': 'v2'}}
show_args2(*list1, dict1=dict1) # using *list1 will decompose list into a series of variables bound to *args # (1, 2, 3, 4) # {'dict1': {'k1': 'v1', 'k2': 'v2'}}
Loops and Comprehensions
Exploring for Loops
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
defsimple_loops(): numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] for number in numbers: print(number, end=', ')
Decorators are functions that take another function as an argument
and extend their behavior dynamically without explicitly modifying it.
In Python, you have a dedicated syntax that allows you to apply
a decorator to a given function: In this piece of syntax, the
@decorator part tells Python to call
decorator() with the funcobject as an
argument.
1 2 3
@decorator deffunc(): <body>
The following code produces a TypeError called ** 'NoneType' object
is not callable** since decorator function prev returns nothing to be
executed:
before f() executing, the decorator prev()
was executed;
the execution of prev() returns nothing which means the
() in f() has nothing to execute;
# args = (<function f at 0x10b944ca0>,) # kwargs = {} # running
Here is a bizarre statement that we never call f() but
its decorator prev() was invoked automatically by compiler.
It's because decorator is loaded during loading
modules
Introduced with PEP 255,
generator functions are a special kind of function that
return a lazy
iterator. These are objects that you can loop over like a list. However,
unlike lists, lazy iterators do not store their contents in memory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
defgen_infinite_sequence(): num = 0 whileTrue: yield num num += 1
if __name__ == '__main__': gen = gen_infinite_sequence() print(gen) for i inrange(5): print(next(gen), end=' ')
Python yield statement and the code that follows it.
yield indicates where a value is sent back to the caller,
but unlike return, you don’t exit the function
afterward.
Instead, the state of the function is remembered.
That way, when next() is called on a generator object
(either explicitly or implicitly within a for loop), the
previously yielded variable num is incremented, and then
yielded again.
Profiling Generator
Performance
1 2 3 4 5 6
defprofiling_yield(): import cProfile cProfile.run('sum([i * 2 for i in range(10000)])')
if __name__ == '__main__': gen = multi_yield() print(next(gen)) print(next(gen)) # first string # second string
Using Advanced Generator
Methods
method
description
generator.send()
Resumes the execution and “sends” a value into the generator
function. The value argument becomes the result of the
current yield expression. The send() method
returns the next value yielded by the generator, or raises
StopIteration if the generator exits without yielding
another value.
generator.throw()
.throw() allows you to throw exceptions with the
generator. I
generator.close()
As its name implies, .close() allows you to stop a
generator.
The with Statement
Python’s with statement is a handy tool that allows you
to manipulate context
manager objects. These objects automatically handle the
setup and teardown phases whenever you’re dealing with
external resources such as files.
1 2 3 4 5 6 7 8 9 10 11
withopen("hello.txt", mode="w", encoding="utf-8") as file: file.write("Hello, World!")
# equals to file = open("hello.txt", mode="w", encoding="utf-8")
List comprehensions provide a concise way to create lists. Common
applications are to make new lists where each element is the result of
some operations applied to each member of another sequence or iterable,
or to create a subsequence of those elements that satisfy a certain
condition.
1 2 3 4
squares = []
for x inrange(10): squares.append(x**2)
we can obtain the same result with
1
squares = [x ** 2for x inrange(10)]
This is also simliar to the following code, except that the following
code return <map object at 0x100d01760> which is
a stream of data that is lazily initiated when the it's called.
1 2 3 4 5 6
squares = map(lambda x: x**2, range(10)) print(squares) # <map object at 0x100d01760> for s in squares: print(s, end=' ') # 0 1 4 9 16 25 36 49 64 81
A list comprehension consists of brackets containing an expression
followed by a for
clause, then zero or more for
or if
clauses. The result will be a new list resulting from evaluating the
expression in the context of the for
and if
clauses which follow it. For example, this listcomp combines the
elements of two lists if they are not equal:
1 2 3 4 5 6 7 8
deflc2(): arr = [(x, y) for x in [1, 2, 3] for y in [3, 1, 4] if x != y] for item in arr: print(item, end=' ') # (1, 3) (1, 4) (2, 3) (2, 1) (2, 4) (3, 1) (3, 4)
The forementioned code is equivalent to the following code but way
more readable:
1 2 3 4 5 6 7 8
for_arr = [] for x in [1,2,3]: for y in [3,1,4]: if x != y: for_arr.append((x, y)) for item in for_arr: print(item, end=' ') # (1, 3) (1, 4) (2, 3) (2, 1) (2, 4) (3, 1) (3, 4)
If the expression is a tuple (e.g. the (x, y) in the
previous example), it must be parenthesized.
1 2 3
[x, x**2for x inrange(6)] # File "<stdin>", line 1, in <module> # [x, x**2 for x in range(6)]