1. 控制结构

控制结构就是控制程序执行顺序的结构,而 Python 有三大控制结构,分别是顺序结构、分支结构(选择结构)以及循环结构。

1.1 分支结构

基本语法

分支结构又称为选择结构,程序代码根据判断条件,选择执行特定的代码。如果条件为真,程序执行一部分代码。否则执行另一部分代码。在 Python 语言中,选择军结构的语法使用关键字 if、elif、else 来表示。

常用的语法有如下几种:

1
2
3
4
5
if
if...else
if...elif...else
if...elif...elif...else
if 嵌套

举个例子,下面判断 requested_toppings 列表是否为空

1
2
3
4
5
requested_toppings = []
if requested_toppings:
print('OK')
else:
print('Are you sure you want a plain pizza?') # 将执行这行代码

三目运算符

如果一些判断条件比较简单,则可以通过三目运算符来精简代码,例如下面两段代码判断的条件都是一样的。

1
2
3
4
5
6
7
8
9
score = 80
# 普通的if else语句
if score > 80:
level="A"
else:
level="C"

# 三目运算符方法
level = "A" if score > 80 else "C"

1.2 循环结构

循环结构是使用最多的一种结构,循环结构是指满足一定的的条件下,重复执行某段代码的一种编码结构,Python 的循环结构中,常见的循环结构是 for 循环和 while 循环。

1.2.1 for 循环

Python for 循环可以遍历任何序列的项目,经常与字符串、列表、字典等数据结构一起使用。

基本语法

for 循环的语法格式如下:

1
2
for iterating_var in sequence:
statements(s)

来看几个实际的案例。

1
2
3
4
5
6
7
8
9
# 字符串遍历 - 打印字符串中的每一个字符
str_data = 'hello world'
for str_d in str_data:
print(str_d)

# 列表遍历 - 打印列表中的每一个元素
list1 = ['C', 'C++', 'JAVA']
for els in list1:
print(els)

enumerate( )

如果想要获取到索引值的话,可以使用 enumerate 函数同时获取到索引和元素值。

1
2
for index, value in enumerate(list1):
print(index, value)

1.2.2 range 函数

我们可以不自己定义列表,而是通过 range 直接帮我们生成一个数值列表,并且可以用于 for 循环的遍历。

基本语法

range(1, 5) 会得到 1, 2, 3, 4 但没有 5,右边数值不会包含进去。也可以只指定一个参数,这样它将从 0 开始。例如,range(6) 会返回数 0~5。

1
2
for value in range(1,5):
print(value)

指定步长

上面可以通过 list( ) 将 range( ) 的结果直接转换为列表。

range( ) 的第一个值代表起始值,第二个值代表终值,第三个值代表步长。

1
2
n = list(range(2,11,2))
print(n) # [2, 4, 6, 8, 10]

列表解析

1
2
squares = [value**2 for value in range(1,11)]
print(squares) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

1.2.3 while 循环

基本语法

Python 编程中 while 语句用于循环执行程序,即在某条件下,循环执行某段序,以处理需要重复处理的相同任务。

其基本形式为:

1
2
while 判断条件(condition):
执行语句(statements)......

while 循环不需要知道循环的次数,即无限循环,直到条件不满满足为止。

break 与 continue 语句

两者的作用:

  • break 语句是立即退出 while 循环,不再运行循环中余下的代确码,也不管条件判断的结果是否为真。
  • continue 语句是结束本次循环,返回到 while 语句开始的位置,接着条件判断。如果为真,程序接着执行,否则退出。

两者的区别:

  • continue 语句跳出本次循环,只跳过本次循环 continue 后的语句。
  • break 语句立即跳出整个循环体,循环体中未执行的循环将不会执行。

2. 函数

2.1 函数基础

2.1.1 函数定义

函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,不需要编写大量重复的代码。函数还可以接收参数,并根据参数的不同做出不同的操作,最后再把处理结果返回给我们。

基本语法如下:

  • 函数代码块以 def 关键词开头,后接函数标识符名称和圆括号 ()。
  • 圆括号之间可以用于定义参数。
  • 函数的第一行语句可以选择性地使用文档字符串来说明函数用途。
  • 函数内容以冒号起始,并且缩进。
  • return [表达式] 结束函数,选择性地返回一个值给调用方,不带表达式的 return 相当于返回 None。
1
2
3
4
def functionname(parameters):
"函数_文档字符串"
functions_suite
return [expression]

函数调用的基本语法格式:[返回值] = 函数名([形参值])

2.1.2 参数传递

实参和形参

在使用函数时,经常会用到形式参数(简称 “ 形参 “)和实际参数(简称 “ 实参 “),二者之间的区别是:

  • 形式参数:在定义函数时,函数名后面括号中的参数就是形式参数。
  • 实际参数:在调用函数时,函数名后面括号中的参数称为实际参数,也就是函数的调用者给函数的参数。

举个简单的例子,其中下面的 username 是一个形参,’jesse’ 是一个实参。

1
2
3
4
def greet_user(username):
print(f"Hello, {username.title()}!")

greet_user('jesse') # Hello, Jesse!

其实,函数的本质就是一段有特定功能、可以重复使用的代码这段代码已经被提前编写好了,并且为其起一个 “ 好听 “ 的名字。在后续编写程序过程中,如果需要同样的功能,直接通过起好的名字就可以调用这段代码。

可变类型与不可变类型

Python 函数的参数传递:

  • 不可变类型:类似 C++ 的值传递,如整数、字符串、元组。如 fun(a),传递的只是 a 的值,没有影响 a 对象本身。比如在 fun(a) 内部修改 a 的值,只是修改另一个复制的对象,并不会影响 a 本身。
  • 可变类型:类似 C++ 的引用传递,如列表,字典。如 fun(a),则是将 a 真正的传过去,修改后 fun 外部的 a 也会受影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# python传不可变对象实例
def ChangeInt(b):
b = 10
print("函数体内:{}".format(b)) # 函数体内:10

b = 2
ChangeInt(b)
print("函数体外:{}".format(b)) # 函数体外:2


# python传可变对象实例
def Changeme(mylist):
mylist.append(40)
print("函数内取值", mylist) # 函数内取值 [10, 20, 30, 40]

mylist = [10, 20, 30]
Changeme(mylist)
print("函数外取值", mylist) # 函数外取值 [10, 20, 30, 40]

参数使用方法

调用函数时常用的参数使用类型:

  1. 默认参数(可选参数)

调用函数时,默认参数的值如果没有传入,则被认为是默认值。

下面代码一为正常的函数定义,代码二则给形参使用了默认值。

1
2
3
4
5
6
7
# 代码一
def describe_pet(animal_type, pet_name):
pass

# 代码二
def describe_pet(pet_name, animal_type='dog'):
pass

但是要注意的是,如果使用默认值且调用时只有一个实参时,默认值顺序需往后放。

例如,上面代码二我调用这个函数时只传递一个参数,那么这个实参会按照参数定义的顺序从左到右依次进行匹配,所以该实参会优先被赋值给 pet_name,而 animal_type 会使用默认值 dog。

  1. 位置参数(必选参数)

函数调用时,实参默认按位置顺序进行传递,并且要求个数和形参完全匹配。下面代码传递的实参是位置实参,即按照参数出现的顺序来接收传递给函数的实参。

1
2
3
4
def describe_pet(animal_type, pet_name):
pass

describe_pet('hamster', 'harry')
  1. 命名参数(关键字参数)

使用关键字参数允许函数调用时参数的顺序与声明时不一致,通过参数名进行匹配。

下面代码三和代码四是对上述函数的调用,而不是定义。下面代码的实参是命名参数,即不用按照函数定义的参数顺序来的参数。

1
2
3
4
def describe_pet(animal_type, pet_name):
pass

describe_prt(animal_type='hamster', pet_name='harry')
  1. 可变参数

你可能需要一个函数能处理比当初声明时更多的参数,这些参数叫做不定长参数或可变参数。

  • 元组类型可变参数

下面形参名 *toppings 中的 星号(*)是创建了一个名为 toppings 的空元组 ,并将收到的所有值都分装到这个元组中。我们经常会看到通用形参名 *args,它用于收集任意数量的位置形参。

1
2
3
4
def make_pizza(*toppings):
print(toppings)

make_pizza('mushrooms','green peppers','extra cheese') # ('mushrooms','green peppers','extra cheese')
  • 字典类型可变参数

下面形参 **user_info 中的两个星号(**)创建了一个名为 user_info 的空字典 ,并将收到的所有名称值对都放到这个字典中,其中(location='princeton',field='physics')表示关键字实参。

1
2
3
4
5
6
7
def build_profile(first, last, **user_info):
user_info['first_name'] = first
user_info['last_name'] = last
return user_info

user_profile = build_profile('albert','einstein', location='princeton', field='physics')
print(user_profile) # {'location': 'princeton', 'field': 'physics', 'first_name': 'albert', 'last_name': 'einstein'}

当然,我们也会经常看到形参名 **kwargs,它用于收集任意数量的关键字实参。

  • 万能参数

万能参数其实就是结合上面两个可变参数,这样我就可以传入任意数量或类型的参数了。

1
2
3
4
5
6
7
def func(*args, **kwargs):
print(args)
print(kwargs)

func(10, 20) # arg: (10, 20) kwagrs: {}
func(10, 20, c=20, d=40) # arg: (10, 20) kwagrs: {'c': 20, 'd': 40}
func(10, 20, 30, c=40, d=20) # arg: (10, 20, 30) kwagrs: {'c': 40, 'd': 20}

2.1.3 函数返回值

Python 中用 def 语句创建函数时可以用 return 语句指定应该返回的值该返回值可以是任意类型。需要注意的是 return 语句在同一函数中可以出现多次但只要有一个得到执行就会直接结束函数的执行。

函数中使用 return 语句的语法格式如下:

1
return [返回值]

其中返回值参数可以指定也可以省略不写(将返回空值 None)。

1
2
3
4
5
6
def name(first_name, last_name):
full_name = f"{first_name} {last_name}"
return full_name.title()

musician = name('jimi', 'hendrix')
print(musician) # Jimi Hendrix

Tips:
Python 中的函数,返回值的数量是没有限制的,即可以返回多个值。

2.1.4 变量作用域

所谓作用域(Scope),就是变量的有效范围,即变量可以在哪个落范围以内使用。有些变量可以在整段代码的任意位置使用,有些变量只能在函数内部使用,有些变量只能在 for 循环内部使用。

变量的作用域由变量的定义位置决定,在不同位置定义的变量,它的作用域是不一样的。

局部变量

在函数内部定义的变量,它的作用域也仅限于函数内部,出了函数就不能使用了,我们将这样的变量称为局部变量(Local Variable)。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 函数内定义的局部变量
def demo():
test_str = "hello world"
print("函数内部 test_str = ", test_str) # 函数内部 test_str = hello world

demo()

# 函数的参数也属于局部变量,只能在函数内部使用
def demo(name, test_str):
print("函数内部 name = ", name) # 函数内部 name = 张三
print("函数内部 add = ", test_str) # 函数内部 add = hello world

demo("张三", "hello world")

全局变量

除了在函数内部定义变量,Python 还允许在所有函数的外部定义变量,这样的变量称为全局变量(Global Variable)。

和局部变量不同,全局变量的默认作用域是整个程序,即全局变量既可以在各个函数的外部使用,也可以在各函数内部使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 方式一:在函数体外定义的变量,一定是全局变量
test_str = "hello world"
def text():
print("函数体内访问:", test_str) # 函数体内访问: hello world

text()
print("函数体外访问:", test_str) # 函数体外访问: hello world

# 方式二:在函数体内定义全局变量,使用global关键字对变量进行修饰后,该变量就会变为全局变量
def text():
global test_str
test_str = "hello world"
print("函数体内访问:", test_str) # 函数体内访问: hello world

text()
print("函数体外访问:", test_str) # 函数体外访问: hello world

2.2 匿名函数

在定义函数的时候,不想给函数起一个名字,这个时候就可以用 lambda 来定义一个匿名函数,lambda 表达式,又称匿名函数。

语法:

  • 参数:可选,通常以逗号分隔的变量表达式形式,也就是位置参数
  • 表达式:不能包含循环、return,可以包含 if…else…
1
变量名 = lambda 参数: 表达式

可以理解 lambda 表达式,就是简单函数(函数体仅是单行的表达式)的简写版本。相比函数,lamba 表达式具有以下 2 个优势:

  • 对于单行函数,使用 lambda 表达式可以省去定义函数的过程,让代码更加简洁。
  • 对于不需要多次复用的函数,使用 lambda 表达可以在用完之后立即释放,提高程序执行的性能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 示例 1:使用函数,不带参数
def func():
return 1 == 2

result = func()
print(result) # False

# 使用匿名函数
result = lambda: 1 == 2
print(result) # <function <lambda> at 0x10444cea0>
print(result()) # False


# 实例 2:使用函数,带有参数
def func_a(x, y, z):
return x + y + z

result = func_a(1, 2, 3)
print(result) # 6

# 使用匿名函数
a = lambda x, y, z: x + y + z
print(a(1, 2, 3)) # 6


# 示例 3:使用函数,含有if...else语法
def func(x, y):
if x > y:
return x
else:
return y

result = func(2, 6)
print(result) # 6

# 使用匿名函数
func = lambda x, y: x if x > y else y
print(func(2, 6)) # 6

2.3 高阶函数

函数式编程

函数式编程将一个问题分解成一系列函数,通过把大段代码拆成函数,通过一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。

函数式编程还具有一个特点:允许把函数本身作为参数传入另一个函数,还允许返回一个函数。

高阶函数

在 Python 中,高阶函数是指可以接受一个或多个函数作为参数,或者能够返回一个函数的函数。比如 map( )、filter( ) 和 reduce( ) 这些内置函数就是常见的高阶函数。

2.3.1 内置高阶函数

map( ) 函数

map( ) 函数接受一个函数和一个可迭代对象作为参数,然后将这个函数应用到可迭代对象的每个元素上,并返回一个新的可迭代对象。

下面这个例子就可以看出方法二中使用了 map( ) 函数之后,操作就方便简洁了许多,不用再自己去手动遍历列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 对传入参数的值进行翻倍
def double_num(x):
return x * 2

numbers = [1, 2, 3, 4, 5]
number_new = []

# 方法一
for i in numbers:
number_new.append(double_num(i))
print(number_new) # [2, 4, 6, 8, 10]

# 方法二
print(list(map(double_num, numbers))) # [2, 4, 6, 8, 10]

filter( ) 函数

filter( ) 函数的基本语法格式:

1
filter(function, iterable)

funcition 参数表示要传入一个函数,iterable 表示一个可选代对象。

filter( ) 函数的功能是对 iterable 中的每个元素,都使用 function 函数判断,并返回 True 或者 False,最后将返回 True 的元素组成一个新的可遍历的集合。

例如,我想过滤一个字符串列表,筛选出以 “A” 开头的字符串,则可以先自定义一个判断规则,然后传入 filter 函数并过滤指定的列表。

1
2
3
4
5
6
7
8
9
# 打印列表中以"A"开头的名字
def first_name(x):
if x.startswith("A"):
return True
else:
return False
name_list = ["Alex", "Hana", "Anny", "Sunny"]
f = filter(first_name, name_list)
print(list(f)) # ['Alex', 'Anny']

2.3.2 Functools 模块

各个函数应用场景:

  • 偏函数:传入的参数存在大量重复的值
  • cmp_to_key:需要自定义排序规则的场景
  • singledispatch:判断分支比较多的场景
  • wraps:需要用到装饰器的场景
  • lru_cache:涉及到大量计算的场景

偏函数

在 Python 中,偏函数(Partial Function)是通过 functools 模块的 partial 函数创建。偏函数允许固定一个函数的某些参数,从而创建一个新的函数,这个新函数只需要接收剩余未固定的参数。

偏函数可以使代码更简洁、更具可读性,并且在某些情况下可以避免重复编写相似的函数调用代码。

格式:int1 = functools.partial(参数1, 参数2)

  • 参数 1:需要转变的函数名
  • 参数 2:需要设置的默认参数

举个例子,我在某个场景中会频繁的调用下面的 add 函数,并且传入的参数中总是有一个为 10,那么就可以创建一个偏函数 add_new1 将 10 这个参数固定下来。之后每次调用 add_new1 就只用传递两个参数,10 这个参数会自动帮我们传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
import functools

def add(a, b, c):
print(a, b, c)
return a + b + c

add_new1 = functools.partial(add, 10)
result = add_new1(2, 3) # 10 2 3
print(result) # 15

add_new2 = functools.partial(add, 10, 2)
result = add_new2(4) # 10 2 4
print(result) # 16

cmp_to_key

在 python 中我们可以使用 sorted 方法对指定列表进行排序,如果传入的是字符串列表,则会按照 ASCII 码值进行排序,但如果我有其它需求例如按照字符数量大小进行排序,则这种方法就行不通了。

1
2
3
4
5
numbers = [1, 200, 33, 4, 50, 10]
print(sorted(numbers))

str_list = ['apple', 'banana', 'test', 'python']
print(sorted(str_list)) # ['apple', 'banana', 'python', 'test']

而 functools 中的 cmp_to_key 函数就可以做到这点,我们只需先定义好一个排序规则。然后通过下面这种传参的方式传入 sorted 函数中即可,这相当于指定了自定义的排序规则。

1
2
3
4
5
6
7
8
9
10
11
import functools

# 按照字符数量进行排序
def compare_length(a, b):
if len(a) < len(b):
return -1
elif len(a) > len(b):
return 1
else:
return 0
print(sorted(str_list, key=functools.cmp_to_key(compare_length))) # ['test', 'apple', 'banana', 'python']

singledispatch

singledispatch 是 Python 3.4 引入的标准库,它提供了一种将函数转换为多态函数的方法,可以根据函数的第一个参数类型的不同来调用不同的实现。

具体来说,使用 @singledispatch 装饰器可以定义一个函数,并针对第一个参数的类型定义不同的实现。当函数被调用时,Python 会自动查找对应类型的实现来执行,如果找不到对应类型的实现,则会调用默认实现。

举个例子,下面定义了一个名为 func 的函数,并使用 @singledispatch 装饰器将其转换为多态函数。然后,使用 @func.register 装饰器针对不同的参数类型分别定义了不同的实现。当调用 func 函数时,Python 会自动根据传入参数的类型来选择对应的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from functools import singledispatch

@singledispatch
def func(data):
print("default implementation")

@func.register(int)
def func_int(data):
print(f"arg: {data} is an integer")

@func.register(str)
def func_string(data):
print(f"arg: {data} is a string")

func(1) # arg: 1 is an integer
func("hello") # arg: hello is a string
func([1, 2]) # default implementation

我们通过这种方式可以使得我们的代码更清晰更具有结构化,而不是通过写一堆的 if else 语句来进行分支的判断。

wraps

wraps 用于在装饰器中保留被装饰函数的元数据(如函数名和文档字符串),以避免装饰器对原始函数的影响。

我们来看看不加 wraps 的结果是什么,可以发现原始函数名称被改变了。

1
2
3
4
5
6
7
8
9
10
11
def add_exclamation(func):
# @wraps(func)
def wrapper(name):
return func(name) + '!'
return wrapper

@add_exclamation
def greet(name):
return 'hello ' + name

print(greet.__name__) # 输出:wrapper

下面我们在装饰器中使用 @wraps(func),它就会用 func 的元数据覆盖了 wrapper 函数的元数据,这样就可以保留原始函数的元数据,不会因为装饰器的存在而改变原始函数的名字和文档字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
from functools import wraps

def add_exclamation(func):
@wraps(func)
def wrapper(name):
return func(name) + '!'
return wrapper

@add_exclamation
def greet(name):
return 'hello ' + name

print(greet.__name__) # 输出:greet

lru_cache

lru_cache 用于实现缓存功能,即 “Least Recently Used”(最近最少使用)缓存策略。

当使用 lru_cache 装饰一个函数时,它会自动缓存该函数的调用结果。对于相同的参数组合,如果函数已经被调用过,那么下次调用时会直接返回缓存的结果,而无需重新计算,从而提高函数的执行效率。

举个例子,下面 fibonacci 函数计算斐波那契数列,如果没有 lru_cache 装饰器,对于每个 n 的值都会重新计算。有了 lru_cache 装饰器后,已经计算过的结果会被缓存起来,下次相同参数调用时直接返回缓存结果。

1
2
3
4
5
6
7
8
9
10
from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))

lru_cache 函数接受一个可选的参数 maxsize ,用于指定缓存的最大大小。如果 maxsize 设为 None ,则缓存大小没有限制,默认值通常是 128。当缓存达到最大容量时,最近最少使用的缓存结果会被删除,为新的结果腾出空间。

2.3.3 Collections 模块

统计对象个数

在需要统计对象个数的场景下,我们不用自己通过很多代码去实现统计方法,而是可以直接调用 collections 模块中的 Counter 函数,直接通过一行代码就能获取对象的个数。

1
2
3
4
5
6
7
8
9
10
11
12
from collections import Counter

list = [1, 2, 3, 2, 1, 3, 5, 2]
str1 = "hello world"

# 统计每个元素出现的次数
print(Counter(list)) # Counter({2: 3, 1: 2, 3: 2, 5: 1})
print(Counter(str1)) # Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})

# 统计出现次数最多的前x个元素
print(Counter(list).most_common(3)) # [(2, 3), (1, 2), (3, 2)]
print(Counter(str1).most_common(2)) # [('l', 3), ('o', 2)]

合并多个字典

在需要处理多个字典的场景下,可以通过 collections 中的 ChainMap 函数先将多个字典合并成一个字典,然后就能很方便的进行后续处理操作。

1
2
3
4
5
6
7
from collections import ChainMap

dict1 = {'python': 100}
dict2 = {'java': 90}
dict3 = ChainMap(dict1, dict2) # 合并dict1和dcit2为一个字典
for key, value in dict3.items():
print(key, value)