python基础第三课
函数
3.1 函数定义
函数(function)是Python中最主要也是最重要的代码组织和复用手段。Python内置了很多函数,我们可以直接调用,例如我们之前接触过的print和len函数:
a = [1, 2, 3, 4]
print(len(a))
如果我们要重复使用相同或非常类似的代码,就需要写一个函数。通过给函数起一个名字,还可以提高代码的可读性。函数名应该为小写,可以用下划线风格单词以增加可读性。函数使用def关键字声明:
def my_function():
print('Hello world!')
# 比较好的习惯是在函数定义的语句下面空两行
my_function()
用return关键字返回值:
def my_function():
return 'Hello world!'
print(my_function())
同时写多条return语句不会报错,但是如果函数执行了return语句,函数会立刻返回,结束调用,return之后的其它语句都不会被执行了。
def my_function():
return 'Hello world!'
return 'Nothing'
print(my_function())
此时输出为:
Hello world!
如果到达函数末尾时没有遇到任何一条return语句,则返回None。
def my_function():
print('Hello world!')
print(my_function())
输出为:
Hello world!
None
在return语句中,我们可以用逗号隔开多个返回值,Python会隐式地将它们封装成一个元组返回:
def my_function():
return 1, 'string', [1, 2, 3]
a = my_function()
print(a)
a, b, c = my_function() # 如果我们知道返回值的个数,也可以联系之前讲过的元组赋值的操作
print(a)
print(b)
print(c)
输出为:
(1, 'string', [1, 2, 3])
1
string
[1, 2, 3]
或者我们也可以把函数的返回值“打包”成一个字典:
def my_function():
a = 1
b = 2
c = 3
return {'a': a, 'b': b, 'c': c}
result = my_function()
print(result['b']) # 可以根据我们的需要使用对应的函数返回值
输出为:
2
3.2 函数参数
函数可以有一些位置参数(positional)和一些关键字参数(keyword)。关键字参数通常用于指定默认值或可选参数。例如下面的例子中,x和y是位置参数,z和w是关键字参数:
def my_function(x, y, z=0.0001, w=None):
if w is not None:
return x + y + z + w
else:
return x + y + z
函数参数可以只有位置参数,或者只有关键字参数,位置参数和关键字参数同时存在时:关键字参数必须位于位置参数之后。
下面这段代码就会报错:
def my_function(x, z=0.0001, y, w=None): # 程序会报错
if w is not None:
return x + y + z + w
else:
return x + y + z
对于有参数的函数,我们在调用的时候需要按顺序指明位置参数的值,而关键字参数可以指明也可以不指明:
def my_function(x, y, z=0.0001, w=None):
if w is not None:
return x + y + z + w
else:
return x + y + z
# 以下都是合法的调用方式
my_function(1, 2)
my_function(1, 2, z=3, w=4)
my_function(1, 2, 3) # 如果不指明关键字参数的名称,默认按照函数定义时的参数顺序进行对应,例如这里认为z=3
my_function(1, 2, w=4) # 如果想跳过z,指明w的值,那么这里一定要写明w=4
my_function(x=1, y=2) # 调用时我们也可以用关键字的形式传递位置参数
# my_function(x=1, 2) # 但是采用这种形式,需要保证关键字的这种形式在普通的位置参数后面,要不然会报错
my_function(1, y=2) # 这样就不会报错
用关键字参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。例如我们可以定义一个指数函数:
def power(a, x=2):
return a ** x
print(power(2)) # 当传入一个参数的时候,默认计算平方值
print(power(2, 3)) # 当我们想要计算立方时,再传入一个参数
输出为:
4
8
前面提到的都是参数个数固定的情况,如果位置参数和关键字参数的个数不固定呢?在Python函数中,还可以定义可变参数,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。这些可变参数在函数调用时自动组装为一个元组。例如我们要计算一组数值的平方和,一种方法是定义只有一个参数的函数,然后将这组数值组装成一个元组或数组传进来:
def my_function(numbers):
sum = 0
for n in numbers:
sum += n * n
return sum
print(my_function([1, 2, 3]))
输出为:
14
我们也可以定义可变参数:
# 注意我们仅仅是多加了一个*
def my_function(*args):
sum = 0
for n in args:
sum += n * n
return sum
print(my_function())
print(my_function(1))
print(my_function(1, 2))
print(my_function(1, 2, 3))
a = [1, 2, 3]
# 对于已有的列表或元组,Python允许你在前面加一个*号,把它的元素变成可变参数传进去
print(my_function(*a))
输出为:
0
1
5
14
14
对于关键字参数,Python也允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
def my_function(**kw):
if 'city' in kw:
print('city: %s' % kw['city'])
if 'country' in kw:
print('country: %s' % kw['country'])
print()
my_function()
my_function(city='Beijing')
my_function(country='China')
my_function(city='Beijing', country='China')
输出为:
city: Beijing
country: China
city: Beijing
country: China
这些参数也可以组合使用,但是要注意先后顺序,关键字参数必须位于位置参数之后:
def my_function(x, y, *args, z=0.0001, w=None, **kw):
if w is not None:
print(x + y + z + w)
else:
print(x + y + z)
sum = 0
for n in args:
sum += n * n
print(sum)
if 'city' in kw:
print('city: %s' % kw['city'])
if 'country' in kw:
print('country: %s' % kw['country'])
print()
my_function(1, 2, 1, 2, 3, z=3, w=4, city='Beijing', country='China')
输出为:
10
14
city: Beijing
country: China
3.3 递归函数
Python也支持递归函数。在函数内部,我们可以调用其他函数。如果一个函数在内部调用自身,这个函数就是递归函数。例如我们要计算阶乘:
def fact(n):
if n == 1:
return 1
else:
return n * fact(n - 1)
print(fact(5))
输出为:
120
3.4 函数作用域
Python用命名空间(namespace)描述变量作用域的名称。任何在函数中赋值的变量默认都是被分配到局部命名空间(local namespace)中的。与之相对的,是全局命名空间(global namespace)。局部命名空间是在函数被调用时创建的,函数参数会立即填入该命名空间。在函数执行完毕之后,局部命名空间就会被销毁。见下面的例子:
a = 1 # 变量a在全局命名空间
def my_function():
a = 2 # 变量a在局部命名空间,相当于“新建”了一个“临时”的新变量
print(a)
my_function() # 调用函数,会打印局部命名空间中的变量a
print(a) # 全局命名空间中的a的值没有改变
输出为:
2
1
虽然可以在函数中对全局变量进行赋值操作,但是那些变量必须用global关键字声明成全局的才行。不过这种做法并不是很建议:
a = 1 # 变量a在全局命名空间
def my_function():
global a # 声明我们要使用全局命名空间中的a
a = 2
print(a)
my_function() # 调用函数,会打印全局命名空间中的变量a
print(a) # 全局命名空间中的a的值发生了改变
输出为:
2
2
为什么不建议?可读性和可维护性差、有副作用。
3.5 匿名函数
Python支持一种被称为匿名的、或lambda函数。它仅由单条语句组成,该语句的结果就是返回值。它是通过lambda关键字定义的,这个关键字没有别的含义,仅仅是说“我们正在声明的是一个匿名函数”。
def my_function(a):
return a * 2
same_function = lambda a: a * 2 # 这里仅仅是演示,实际使用中不会这样赋值
a = 1
print(my_function(a))
print(same_function(a))
输出为:
2
2
lambda函数常用的实际例子:
- 排序:lambda函数可以用作排序函数的键,例如在列表排序时指定按特定条件排序。
students = [
{'name': 'Alice', 'grade': 80},
{'name': 'Bob', 'grade': 90},
{'name': 'Charlie', 'grade': 75}
]
sorted_students = sorted(students, key=lambda x: x['grade'], reverse=True)
print(sorted_students)
- 过滤:lambda函数可以用于过滤数据,例如在过滤列表时指定过滤条件。
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)
- 映射:lambda函数可以用于映射数据,例如在对列表中的每个元素执行相同操作时。
numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(lambda x: x**2, numbers))
print(squared_numbers)
- 计算:lambda函数可以用于简单的计算,例如在需要一个快速函数时。
add = lambda x, y: x + y
result = add(3, 5)
print(result)
- 回调函数:lambda函数可以用作回调函数,例如在事件处理或异步编程中。
def process_data(data, callback):
processed_data = callback(data)
return processed_data
# 使用lambda函数作为回调函数
result = process_data(5, lambda x: x * 2)
print(result)
这些示例展示了lambda函数在实际应用中的灵活性和便利性,lambda函数是一个强大的工具,可以帮助简化代码逻辑。
3.6 模块
有很多的函数并不需要我们亲自动手去实现,可以去调用前人已经写好的模块。而且在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。为了编写可维护的代码,我们可以把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Python中,一个.py文件就称之为一个模块(Module)。Python本身就内置了很多非常有用的模块,只要安装完毕,这些模块就可以立刻使用。
导入模块的格式包括以下几种:
格式 | 举例 |
---|---|
import 模块名 | 直接导入整个模块,占用内存多,例如import matplotlib |
import 模块名 as 名称缩写 | 导入模块的同时取一个“别名”,例如import matplotlib as mpl |
import 模块名.子模块名 as 名称缩写 | 导入某个模块的子模块,并给子模块取一个“别名”(可选),例如import matplotlib.pyplot as plt |
from 模块名 import 函数名 | 从模块中导入某个或某几个函数(英文逗号隔开),节约内存,例如from math import exp, log, sqrt |
from 模块名.子模块名 import 函数名 | 从子模块中导入某个或某几个函数,例如from matplotlib.pyplot import figure, plot |
我们也可以定义自己的模块,例如在my_package.py中定义:
def my_function():
print('Hello world')
要使用my_function,我们可以导入my_package这个模块:
import my_package
my_package.my_function()
或者可以直接导入特定的函数:
from my_package import my_function
my_function()
我们也可以对导入的模块或函数起一个更短的“别名”:
from my_package import my_function as my
my()
pip 包管理
在Python中,要引入第三方包(也称为库或模块),通常会使用 pip工具。 pip是 Python 的包管理工具,用于安装和管理 Python 包。
安装包:如果你还没有安装要使用的第三方包,可以通过以下命令使用 安装:
pip install package_name
pip install package_name 这里的 package_name 是你要安装的第三方包的名称。例如,要安装 numpy 包,可以运行以下命令:
pip install numpy
pip install numpy 导入包:安装完成后,在你的 Python 脚本或交互式环境中,可以使用 import 语句导入已安装的第三方包,例如: import package_name
import package_name
如果你想为导入的包指定一个别名,可以使用 as 关键字,例如:
import package_name as alias
使用包:一旦导入了第三方包,就可以使用其中定义的函数、类和其他对象。例如,如果导入了 numpy 包,你可以使用其中的函数和类进行科学计算:
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
print(np.mean(arr)) # 计算平均值
卸载包:如果需要卸载已安装的包,可以使用 uninstall命令,例如:
pip uninstall package_name
这样就可以卸载指定的第三方包。
3.7 柯里化
柯里化(Currying)是函数式编程中的一个重要概念,它指的是将接受多个参数的函数转换为一系列接受单个参数的函数的过程。通过柯里化,可以简化函数的调用方式,并提高函数的灵活性和复用性。
在 Python 中,可以使用闭包和函数嵌套来实现柯里化。
我们也可以在一个函数的基础上通过柯里化的方式建立一个新的函数。假设我们有一个执行两数相加的简单函数,通过这个函数,我们可以派生出一个新的只有一个参数的函数,add_five,它用于对其参数加5:
def add_numbers(x, y):
return x + y
add_five = lambda y: add_numbers(5, y)
add_numbers的第二个参数称为“柯里化的”(curried)。我们其实就只是定义了一个可以调用现有函数的新函数而已。
柯里化的优点包括:
参数复用:可以通过部分应用函数来重复使用已有的函数定义。
函数组合:可以更容易地将多个函数组合在一起,形成新的函数。
提高灵活性:可以更灵活地控制函数的参数传递方式。
柯里化是一种强大的技术,特别适用于函数式编程风格,可以帮助简化代码、提高可读性,并促进代码的模块化和复用。
3.8 生成器
Python能以一种一致的方式对序列进行迭代(比如列表中的对象或文件中的行)。这是通过一种叫做迭代器协议(iterator protocol,它是一种使对象可迭代的通用方式)的方式实现的。比如说,对字典进行迭代可以得到其所有的键:
d = {'a': 1, 'b': 2, 'c': 3}
for key in d:
print(key)
输出为:
a
b
c
当我们编写for key in some_dict时,Python解释器首先会尝试从字典创建一个迭代器:
d = {'a': 1, 'b': 2, 'c': 3}
print(iter(d))
输出为:
<dict_keyiterator object at 0x000002075BB69408>
迭代器是一种特殊对象,它可以在诸如for循环之类的上下文中向Python解释器输送对象。大部分能接受列表之类的对象的函数也都可以接受任何可迭代对象,比如min、max、sum等内置方法以及list、tuple等类型构造器:
d = {'a': 1, 'b': 2, 'c': 3}
print(min(iter(d))) # 计算了字典d的键中最小的一个元素
print(list(iter(d))) # 将字典d的键构造成了一个列表
输出为:
a
['a', 'b', 'c']
生成器(generator)是构造新的可迭代对象的一种简单方式。一般的函数执行之后只会返回单个值,而生成器则是以延迟的方式返回一个值序列,即每返回一个值之后暂停,直到下一个值被请求时再继续。要创建一个生成器,只需将函数中的return替换为yield即可:
def generator_function():
for i in range(3):
yield i
for item in generator_function():
print(item)
输出为:
0
1
2
生成器最佳的应用场景是:当我们不想在同一时间将所有计算出来的大量结果分配到内存中,或者是我们不想一下子往内存中存入大量的数据训练模型的时候。
除了for循环,我们也可以手动通过next函数取得下一个值,但是当yield掉所有的值之后,next会触发一个StopIteration的异常,而for循环的方便之处在于它会自动捕捉到这个异常并停止调用next:
def generator_function():
for i in range(3):
yield i
gen = generator_function()
print(next(gen))
print(next(gen))
print(next(gen))
# print(next(gen)) # 第四次调用会报错
输出为:
0
1
2
对于一些可以迭代的类型,例如字符串、元组、列表等,我们也可以通过iter函数将它们变成迭代器对象,从而可以使用next函数:
a = [1, 2, 6, 7, 3, 4]
a_iter = iter(a)
print(next(a_iter))
输出为:
1
另一种更简洁的构造生成器的方法是使用生成器表达式(generator expression)。这是一种类似于列表、字典、集合推导式的生成器。其创建方式为,把列表推导式两端的方括号改成圆括号:
gen = (x ** 2 for x in range(100))
# 跟下面这种函数定义方式是等价的
def _make_gen():
for x in range(100):
yield x ** 2
gen = _make_gen()
生成器表达式也可以取代列表推导式,作为函数参数:
print(sum(x ** 2 for x in range(100)))
print(dict((i, i ** 2) for i in range(5)))
输出为:
328350
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
标准库itertools模块中有一组用于许多常见数据算法的生成器。例如,groupby函数可以接受任何序列和一个函数,根据函数的返回值对序列中的连续元素进行分组。下面是一个例子:
import itertools
first_letter = lambda x: x[0]
names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven']
for letter, names in itertools.groupby(names, first_letter):
print(letter, list(names)) # names is a generator
输出为:
A ['Alan', 'Adam']
W ['Wes', 'Will']
A ['Albert']
S ['Steven']
combinations(iterable, k)函数会生成一个序列iterable中所有可能的k元元组组成的序列,不考虑顺序;permutations(iterable, k)类似,但考虑顺序,因此会生成更多的元素:
import itertools
lst = [1, 2, 3, 4]
print(list(itertools.combinations(lst, 2)))
print(list(itertools.permutations(lst, 2)))
输出为:
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
[(1, 2), (1, 3), (1, 4), (2, 1), (2, 3), (2, 4), (3, 1), (3, 2), (3, 4), (4, 1), (4, 2), (4, 3)]
product(*iterables,repeat=1)生成输入的多个序列iterables的笛卡尔积,结果为元组:
import itertools
a = [1, 2, 3]
b = [4, 5, 6]
print(list(itertools.product(a, b)))
输出为:
[(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6), (3, 4), (3, 5), (3, 6)]
文件操作
在Python中,为了打开一个文件以便读写,可以使用内置的open函数以及一个相对或绝对的文件路径。默认情况下,文件是以只读模式(‘r’)打开的。然后,我们就可以像处理列表那样来处理这个文件句柄f了,比如对行进行迭代。从文件中取出的行都带有完整的行结束符,因此我们也可以使用rstrip函数去掉这个行结束符。如果使用open创建文件对象,一定要用close关闭它,因为关闭文件可以返回操作系统资源。
txt文件
文件不存在,自动创建,文件夹不存在会报错。
#打开文件前确保文件夹存在
方法一:
import os
folder_path = "/data/tmp"
if not os.path.exists(folder_path):
os.makedirs(folder_path)
方法二:
import os
folder_path = "/data/tmp"
os.makedirs(folder_path, exist_ok=True)#exist_ok=True文件夹已经存在时会忽略创建过程
a.txt这个文件:
path = 'a.txt'
f = open(path)
for line in f:
print(line.rstrip())
f.close()
用with语句可以更容易地清理打开的文件。这样可以在退出代码块时,自动关闭文件。
path = 'a.txt'
with open(path) as f:
print([x.rstrip() for x in f])
除了只读模式,我们也可以写文件:
with open('tmp.txt', 'w') as f:
f.writelines(str(x) for x in range(10)) # 这里注意需要将要写入文本文件的变量转成字符串类型
常用的文件操作模式如下:
-
r: 只读模式。
-
w: 只写模式。创建新文件,如果文件已经存在,会进行覆盖。
-
a: 追加写。如果文件不存在则新建一个。
-
r+: 读写模式。
-
b: 附加说明用于二进制文件,即’rb’或’wb’。
csv文件
写文件:
import csv
# 数据
data = [
['Name', 'Age', 'City'],
['Alice', 30, 'New York'],
['Bob', 25, 'San Francisco'],
['Charlie', 35, 'Seattle']
]
# 打开 CSV 文件进行写入
with open('data.csv', mode='w', newline='') as file:
csv_writer = csv.writer(file)
# 写入数据
for row in data:
csv_writer.writerow(row)
读取文件:
import csv
# 打开 CSV 文件进行读取
with open('data.csv', mode='r') as file:
csv_reader = csv.reader(file)
# 逐行读取数据
for row in csv_reader:
print(row)
yaml文件
读写yaml文件:
import yaml
# 数据
data = {
'name': 'Alice',
'age': 30,
'city': 'New York'
}
# 写入 YAML 文件
with open('data.yaml', 'w') as file:
yaml.dump(data, file)
# 读取 YAML 文件
with open('data.yaml', 'r') as file:
data = yaml.safe_load(file)
print(data)
错误和异常处理
Python中也有错误和异常处理的机制。例如,Python的float函数可以将字符串转换成浮点数,但输入有误时,有ValueError错误:
print(float('1.2345'))
print(float('string'))
输出为:
1.2345
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-17-ab350e8097a9> in <module>
1 print(float('1.2345'))
----> 2 print(float('string'))
ValueError: could not convert string to float: 'string'
假如想优雅地处理float的错误,让它返回输入值。我们可以写一个函数,在try/except中调用float。当float(x)抛出异常时,才会执行except的部分:
def attempt_float(x):
try:
return float(x)
except:
return x
print(attempt_float('1.2345'))
print(attempt_float('string'))
我们也可以在except中指明一个异常类型,或者用元组指明多个异常:
def attempt_float(x):
try:
return float(x)
except (TypeError, ValueError):
return x
某些情况下,你可能不想抑制异常,你想无论try部分的代码是否成功,都执行一段代码。可以使用finally:
path = 'tmp.txt'
def write_to_file(f):
f.writelines(str(x) for x in range(10))
f = open(path, 'w')
try:
write_to_file(f)
finally:
f.close()
代码结构解析:
try:
# 可能会引发异常的代码
#result = 10 / 0 # 一个除零错误
#res = float('str')
print(123)
except ZeroDivisionError:
# 捕获特定类型的异常
print("除零错误发生!")
except Exception as e:
# 捕获所有其他类型的异常
print(f"发生异常:{e}")
else:
# 如果没有发生异常,则执行这里的代码
print("没有发生异常!")
finally:
# 无论是否发生异常,都会执行这里的代码
print("无论是否发生异常,使用了finally后的这句话总是会被执行!")
raise关键字:
#例如这是一段存款的代码
x = -1
if x < 0:
raise ValueError("存款不能为负数")
这会引发一个 `ValueError` 异常,你可以在适当的地方捕获并处理它。
一般情况raise只引发一个异常,如果一定要有多个:
#分别引发了 ValueError 和 TypeError 异常,并在 except 块中分别捕获和处理这两个异常
try:
x = -1
# x = "1"
if x < 0:
raise ValueError("x不能为负数")
raise TypeError("x类型错误")
except ValueError as ve:
print(f"发生 ValueError 异常:{ve}")
except TypeError as te:
print(f"发生 TypeError 异常:{te}")
对象和类
6.1 对象
万物皆对象:Python语言的一个重要特性就是它的对象模型的一致性。每个数字、字符串、数据结构、函数、类、模块等等,都是在Python解释器的自有“盒子”内,它被认为是Python对象。每个对象都有类型(例如,字符串或函数)和内部数据。在实际中,这可以让语言非常灵活,因为函数也可以被当做对象使用。之前提到过,我们在定义一个变量的时候,不需要指明它的类型,后面也可以修改:
a = 5 # 这是一个整数
print(type(a))
a = 'string' # 我们可以将其改成一个字符串
print(type(a))
输出为:
<class 'int'>
<class 'str'>
我们可以用isinstance函数检查对象是否是某种类型:
a = 5
print(isinstance(a, int))
输出为:
True
isinstance可以用类型元组,检查对象的类型是否在元组中:
a = 5
b = 4.5
print(isinstance(a, (int, float)))
print(isinstance(b, (int, float)))
输出为:
True
True
Python的对象通常都有属性(其它存储在对象内部的Python对象)和方法(对象的附属函数可以访问对象的内部数据)。可以用obj.attribute_name访问属性和方法:
a = 'string'
print(a.upper())
输出为:
STRING
在pycharm输入a.之后,会显示默认出默认的方法有哪些。
也可以用getattr函数,通过名字访问属性和方法:
a = 'string'
print(getattr(a, 'split'))
输出为:
<built-in method split of str object at 0x0000020758827928>
要判断两个变量是否指向同一个对象,可以使用is方法。is not可以判断两个对象是不同的:
a = [1, 2, 3]
b = a
c = list(a) # 因为list函数总是创建一个新的Python列表(即复制),我们可以断定c是不同于a的。
print(a is b)
print(a is c)
#列表
a=[1]
b=[1]
print(a is b)
#在Python中,每个变量都指向一个对象的引用,而不是对象本身。当你使用 a=[1] 和 b=[1] 时,Python会为每个列表创建一个新的对象,并将变量 a 和 b 分别指向这两个不同的对象。
#字典
a={'k':'v'}
b={'k':'v'}
print(a is b)
#元组
a=1,
b=1,
print(a is b)
#集合
a = set()
b = set()
print(a is b)
输出为:
True
False
False
False
True
False
使用is比较与==运算符不同,如下:
a = [1, 2, 3]
c = list(a)
print(a is c)
print(a == c)
输出为:
False
True
is和is not常用来判断一个变量是否为None,因为只有一个None的实例:
a = None
print(a is None)
print(a == None) # 不建议这样比较
输出为:
True
True
Python中的大多数对象,比如前面讲到的列表、字典,和用户定义的类型(类),都是可变的。意味着这些对象或包含的值可以被修改:
a = [1, 2, 3]
a[1] = 4 # 列表是可变对象
print(a)
输出为:
[1, 4, 3]
其它的,例如字符串和元组,是不可变的:
a = (1, 2, 3)
#b=1,2,3,
a[1] = 4 # 元组是不可变对象
输出为:
TypeError Traceback (most recent call last)
<ipython-input-27-02309702bb07> in <module>
1 a = (1, 2, 3)
----> 2 a[1] = 4 # 元组是不可变对象
TypeError: 'tuple' object does not support item assignment
可以修改一个对象并不意味就要修改它。这被称为副作用。例如,当写一个函数,任何副作用都要在文档或注释中写明。如果可能的话,我们推荐避免副作用,采用不可变的方式,即使要用到可变对象。
我们可以看一下副作用会导致的问题。使用列表的时候:
a = [1, 2, 3]
b = a # 前面看到b和a对应同一个对象
a[1] = 4 # 有可能我们只想改变a
print(b) # 但是b也被意外地改变了
输出为:
[1, 4, 3]
有三种简单的方式可以避免这种情况:
a = [1, 2, 3]
# b = list(a) # 前面提到的list函数
# b = a[:] # 用切片
b = a.copy() # 用copy函数
a[1] = 4 # 有可能我们只想改变a
print(b) # b没有被改变
输出为:
[1, 2, 3]
在使用函数参数的时候,也需要小心副作用:
def my_function(d):
d['a'] = 3 # 我们在函数内部修改了这个可变变量
print(d)
d = {'a': 1, 'b': 2}
my_function(d)
print(d) # 我们发现这种影响传达到了函数外部
输出为:
{'a': 3, 'b': 2}
{'a': 3, 'b': 2}
要避免这种情况,要么就是在函数内部避免修改这些传入的参数,要么就是用copy函数等方式复制一个副本,再进行修改:
def my_function(d):
d = d.copy() # 在内部我们创建了一个“临时”的变量
d['a'] = 3 # 我们在函数内部修改了这个可变变量
print(d)
d = {'a': 1, 'b': 2}
my_function(d)
print(d) # 我们阻止了这种影响传达到函数外部
输出为:
{'a': 3, 'b': 2}
{'a': 1, 'b': 2}
深拷贝和浅拷贝
在Python中,拷贝(copy)是指创建一个对象的副本。当涉及到可变对象(如列表、字典等)时,Python中有两种类型的拷贝:浅拷贝(shallow copy)和深拷贝(deep copy)。
浅拷贝(Shallow Copy):
浅拷贝创建一个新的对象,但是其内部的元素对象只是原始对象的引用。
对于嵌套对象(如列表中包含另一个列表),浅拷贝只会复制最外层的对象,内部的对象仍然是原始对象和拷贝对象共享的。
在Python中,可以使用 copy() 方法或者 copy 模块中的 copy() 函数来进行浅拷贝。
深拷贝(Deep Copy):
深拷贝会递归地复制所有对象,包括嵌套对象,因此原始对象和拷贝对象是完全独立的。
在Python中,可以使用 copy 模块中的 deepcopy() 函数来进行深拷贝。
浅拷贝和深拷贝的区别:
import copy
# 原始列表
original_list = [1, [2, 3], 4]
# 浅拷贝
shallow_copied_list = copy.copy(original_list)
# 深拷贝
deep_copied_list = copy.deepcopy(original_list)
# 修改原始列表的第二个元素
original_list[1][0] = 5
print(original_list) # 输出: [1, [5, 3], 4]
print(shallow_copied_list) # 输出: [1, [5, 3], 4]
print(deep_copied_list) # 输出: [1, [2, 3], 4]
修改原始列表的第二个元素后,浅拷贝的列表也受到影响,因为浅拷贝只复制了引用。而深拷贝则创建了一个完全独立的副本,因此不受原始对象的影响。
6.2 类
6.2.1 类和实例
面向对象最重要的概念是类(Class)和实例(Instance):类是抽象的模板,而实例是根据类创建出来的一个个具体的对象。
我们可以用class关键词定义一个类:
class Student(object):
pass
class后面跟着的Student表示类名,而(object)代表继承的最一般的Python自带的object类,后面我们会看到Python里面可以继承一个类然后发展出来新的类。
定义了类之后,我们就可以创建Student类的实例,例如下面的tom,实例是有对应的内存地址的。
tom = Student()
print(tom)
print(Student)
输出为:
<__main__.Student object at 0x0000018013415278>
<class_main__.Student'>
我们可以用type()来检查变量的类型:
print(type(tom))
print(type(Student))
输出为:
<class_main__.Student'>
<class 'type'>
我们也可以用isinstance()来判断一个实例是否属于某个类:
print(isinstance(tom, Student))
输出为:
True
我们可以给一个实例变量绑定属性(属性=成员变量):
tom.name = 'Tom'
tom.score = 59
print(tom.name)
print(tom.score)
输出为:
Tom
59
类中的方法是用来定义类的行为或功能,类中的方法=成员函数
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def display_info(self):
print(f"{self.year} {self.make} {self.model}")
# 创建实例
car1 = Car("Toyota", "Corolla", 2022)
car2 = Car("Honda", "Civic", 2023)
# 访问属性和调用方法
car1.display_info() # 输出:2022 Toyota Corolla
car2.display_info() # 输出:2023 Honda Civic
如果每个学生都需要有姓名和分数,而且最好在初始化的时候就设置好,那么我们可以在Student类中加一个特殊的init函数(特殊的函数前后都是两个下划线):
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
当我们在调用这个初始化函数时,self不需要传参数,因为Python解释器自己会把实例变量传进去:
tom = Student('Tom', 59)
print(tom.name)
print(tom.score)
输出为:
Tom
59
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且调用时不用传递该参数。除此之外,类的方法和普通函数没有什么区别。我们可以用dir()来查看一个类中的全部函数:
print(dir(Student))
print(dir(tom))
输出为:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name', 'score']
我们可以修改这些内置的函数,例如init。再例如修改了str之后,当我们print一个实例时,就会自动调用这个函数把这个实例转成字符串。
# 修改前
print(tom)
输出为:
<__main__.Student object at 0x0000018013415588>
重新修改Student类的定义
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def __str__(self):
return '%s gets a score of %s' % (self.name, self.score)
# 重新定义tom
tom = Student('Tom', 59)
# 重新print tom
print(tom)
这次输出为:
Tom gets a score of 59
类的隔离属性
class Login:
val = {}
print("val 原来{}".format(id(val)))
def update_val(self):
val1 = {'name':'mike'}
self.val.update(val1)
print(self.val)
print("val 修改后{}".format(id(self.val)))
def modify(self):
self.val = {'name': 'jordan'} # 修改类变量 val
print(self.val)
print("val 修改后{}".format(id(self.val)))
if __name__ == '__main__':
L = Login()
L.update_val()
#L.modify() # 调用 __modify 方法来修改类变量 val
X = Login()
print(X.val)
6.2.2 继承
当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
即便我们什么都不做,子类也具备了父类的所有功能。
子类可以重写父类的方法,会覆盖父类方法中的效果。
子类的对象既是子类的实例,也是父类的实例。
# 定义一个父类
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name} makes a sound")
# 定义一个子类,继承自Animal
class Dog(Animal):
def __init__(self, name, age):
super().__init__(name) # 调用父类的__init__方法
self.age = age
def speak(self):
super().speak() # 调用父类的speak方法
print(f"{self.name} says Woof!")
def how_old_are_u(self):
print(f"{self.name}今年{self.age}岁了")
# 创建一个Dog对象
dog = Dog("小白", 3)
dog.speak()
dog.how_old_are_u()
# 实例测试
print(isinstance(dog, Animal))
print(isinstance(dog, Dog))
输出为:
小白 makes a sound
小白 says Woof!
小白今年3岁了
True
True
super关键字:
在子类中用来调用父类中的方法,可以动态地查找父类,即使在多重继承的情况下也能正确地调用父类的方法。这种方式有助于避免硬编码父类的名称,使代码更具可维护性和灵活性。
访问限制
Python提供了以下几种访问修饰符:
公有成员:默认情况下,Python中的所有成员(属性和方法)都是公有的,可以在类的内部和外部访问。
私有成员:在成员名字前面加上双下划线__,即可将成员定义为私有成员,外部无法直接访问。
受保护成员:在成员名字前面加上单下划线_,即可将成员定义为受保护成员,外部无法直接访问,但可以在子类中访问。
除了私有成员,其他的都可以被修改。
class MyClass:
def __init__(self):
self.public_var = "I am a public variable"
self.__private_var = "I am a private variable"
self._protected_var = "I am a protected variable"
def public_method(self):
print("This is a public method")
def __private_method(self):
print("This is a private method")
def _protected_method(self):
print("This is a protected method")
# 创建一个对象
obj = MyClass()
# 访问公有成员
print(obj.public_var)
obj.public_method()
# 尝试访问私有成员和方法,会导致错误
# print(obj.__private_var)
# obj.__private_method()
# 访问受保护成员和方法,但是不建议这样做,可以在子类中访问
print(obj._protected_var)
obj._protected_method()
在子类中使用父类的方法和属性:
class Parent:
def __init__(self):
self._protected_var = "protected variable"
def _protected_method(self):
print("调用protected method")
class Child(Parent):
def __init__(self):
super().__init__()
def access_parent_protected_member(self):
print("在子类访问父类受保护的变量:", self._protected_var)
def call_parent_protected_method(self):
print("在子类调用父类受保护的方法:")
self._protected_method()
# 创建一个子类对象
child_obj = Child()
# 访问父类的受保护成员
child_obj.access_parent_protected_member()
# 调用父类的受保护方法
child_obj.call_parent_protected_method()
输出:
在子类访问父类受保护的变量: protected variable
在子类调用父类受保护的方法:
调用protected method
私有成员的修改
当使用@property装饰器定义一个属性时,访问该属性时不需要使用括号,因为属性被视为类的一个特性,而不是方法。
在访问属性时应该直接使用属性名,而不是带括号的方法调用形式。
class MyClass:
def __init__(self):
self.__private_var = 0
#get方法,使用@property修饰符
@property
def private_var(self):
return self.__private_var
#set方法,使用@private_var.setter修饰符
@private_var.setter
def private_var(self, value):
if value < 0:
print("Error: Value 不能小于0.")
else:
self.__private_var = value
# 创建一个类实例
obj = MyClass()
# 通过get函数访问私有成员
print("Current value of private_var:", obj.private_var)
# 通过set函数修改私有成员
obj.private_var = 10
print("Updated value of private_var:", obj.private_var)
# 尝试设置一个负值
obj.private_var = -5
输出:
Current value of private_var: 0
Updated value of private_var: 10
Error: Value 不能小于0.