Python基础知识(三)

一、函数

函数是带名字的代码块,用于完成具体的工作。要执行函数定义的特定任务,可调用该函数。需要在程序中多次执行同一项任务时,无须反复编写完成该任务的代码,只需要调用执行该任务的函数,让Python运行其中的代码即可。

1、定义函数

1
2
3
4
def greet_user(): 
"""显示简单的问候语。"""
print("Hello!")
greet_user()

关键字def 来告诉Python,你要定义一个函数,这是函数定义 ,向Python指出了函数名,还可能在圆括号内指出函数为完成任务需要什么样的信息。

文档字符串用三引号 括起,Python使用它们来生成有关程序中函数的文档。

向函数传递信息

1
2
3
4
def greet_user(username): 
"""显示简单的问候语。"""
print(f"Hello, {username.title()}!")
greet_user('jesse')

实参和形参

在函数greet_user() 的定义中,变量username 是一个 形参(parameter),即函数完成工作所需的信息。在代码greet_user(‘jesse’) 中,值’jesse’ 是 一个实参(argument),即调用函数时传递给函数的信息。调用函数时,将要让函数使用的信息放在圆括号内。在greet_user(‘jesse’) 中,将实参’jesse’ 传递给了函数greet_user(),这个值被赋给了形参username 。

注意 大家有时候会形参、实参不分,因此如果你看到有人将函数定义中的变 量称为实参或将函数调用中的变量称为形参,不要大惊小怪。

2、传递实参

位置实参

调用函数时,Python必须将函数调用中的每个实参都关联到函数定义中的一个形参。为此,最简单的关联方式是基于实参的顺序。这种关联方式称为位置实参。

1
2
3
4
5
def describe_pet(animal_type, pet_name): 
"""显示宠物的信息。"""
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet('hamster', 'harry')

在函数中,可根据需要使用任意数量的位置实参,Python将按顺序将函数调用中的实参关联到函数定义中相应的形参。

关键字实参

关键字实参是传递给函数的名称值对。因为直接在实参中将名称和值关联起来,所以向函数传递实参时不会混淆(不会得到名为Hamster的harry这样的结果)。关键字实参让你无须考虑函数调用中的实参顺序,还清楚地指出了函数调用中各个值的用途。

1
2
3
4
5
def describe_pet(animal_type, pet_name): 
"""显示宠物的信息。"""
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(animal_type='hamster', pet_name='harry')

关键字实参的顺序无关紧要,因为Python知道各个值该赋给哪个形参。

注意 使用关键字实参时,务必准确指定函数定义中的形参名。

默认值

编写函数时,可给每个形参指定默认值 。在调用函数中给形参提供了实参时, Python将使用指定的实参值;否则,将使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的实参。使用默认值可简化函数调用,还可清楚地指出函数的典型用法。

1
2
3
4
5
def describe_pet(pet_name, animal_type='dog'): 
"""显示宠物的信息。"""
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(pet_name='willie')

注意 使用默认值时,必须先在形参列表中列出没有默认值的形参,再列出有默认值的实参。这让Python依然能够正确地解读位置实参。

等效的函数调用

1
2
3
4
5
6
7
8
9
def describe_pet(pet_name, animal_type='dog'):

# 一条名为Willie的小狗。
describe_pet('willie')
describe_pet(pet_name='willie')
# 一只名为Harry的仓鼠。
describe_pet('harry', 'hamster')
describe_pet(pet_name='harry', animal_type='hamster')
describe_pet(animal_type='hamster', pet_name='harry')

注意 使用哪种调用方式无关紧要,只要函数调用能生成你期望的输出就行。 使用对你来说最容易理解的调用方式即可。

3、返回值

返回简单值

1
2
3
4
5
6
def get_formatted_name(first_name, last_name): 
"""返回整洁的姓名。"""
full_name = f"{first_name} {last_name}"
return full_name.title()
musician = get_formatted_name('jimi', 'hendrix')
print(musician)

让实参变成可选的

1
2
3
4
5
6
7
8
9
10
11
def get_formatted_name(first_name, last_name, middle_name=''): 
"""返回整洁的姓名。"""
if middle_name:
full_name = f"{first_name} {middle_name} {last_name}"
else:
full_name = f"{first_name} {last_name}"
return full_name.title()
musician = get_formatted_name('jimi', 'hendrix')
print(musician)
musician = get_formatted_name('john', 'hooker', 'lee')
print(musician)

为了让中间名变成可选的,可给形参middle_name 指定一个空的默认值, 并在用户没有提供中间名时不使用这个形参。为让get_formatted_name() 在没 有提供中间名时依然可行,可将形参middle_name 的默认值设置为空字符串,并将其移到形参列表的末尾。

在函数体中,检查是否提供了中间名。Python将非空字符串解读为True ,因此如果函数调用中提供了中间名,if middle_name 将为True。

返回字典

函数可返回任何类型的值,包括列表和字典等较复杂的数据结构。

1
2
3
4
5
6
def build_person(first_name, last_name): 
"""返回一个字典,其中包含有关一个人的信息。"""
person = {'first': first_name, 'last': last_name}
return person
musician = build_person('jimi', 'hendrix')
print(musician)
1
2
3
4
5
6
7
8
9
def build_person(first_name, last_name, age=None): 
"""返回一个字典,其中包含有关一个人的信息。"""
person = {'first': first_name, 'last': last_name}
if age:
person['age'] = age
return person

musician = build_person('jimi', 'hendrix', age=27)
print(musician)

在函数定义中,新增了一个可选形参age ,并将其默认值设置为特殊值None (表 示变量没有值)。可将None视为占位值。在条件测试中,None 相当于False 。 如果函数调用中包含形参age 的值,这个值将被存储到字典中。在任何情况下,这个函数都会存储人的姓名,但可进行修改,使其同时存储有关人的其他信息。

4、传递列表

1
2
3
4
5
6
7
8
def greet_users(names): 
"""向列表中的每位用户发出简单的问候。"""
for name in names:
msg = f"Hello, {name.title()}!"
print(msg)

usernames = ['hannah', 'ty', 'margot']
greet_users(usernames)

我们将greet_users() 定义为接受一个名字列表,并将其赋给形参names 。这个函数遍历收到的列表,并对其中的每位用户打印一条问候语。

在函数中修改列表

将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量数据。

禁止函数修改列表

可向函数传递列表的副本而非原件。这样,函数所做的任何修改都只影响副本,而原件丝毫不受影响。

要将列表的副本传递给函数,可以像下面这样做:

1
2
# 切片表示法[:] 创建列表的副本。
function_name(list_name_[:])

虽然向函数传递列表的副本可保留原始列表的内容,但除非有充分的理由,否则还是应该将原始列表传递给函数。这是因为让函数使用现成的列表可避免花时间和内存创建副本,从而提高效率,在处理大型列表时尤其如此。

5、传递任意数量的实参

1
2
3
4
5
6
 def make_pizza(*toppings): 
"""打印顾客点的所有配料。"""
print(toppings)

make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')

形参名*toppings 中的星号让Python创建一个名为toppings 的空元组,并将收到的所有值都封装到这个元组中。函数体内的函数调用print() 通过生成输出,证明Python能够处理使用一个值来调用函数的情形,也能处理使用三个值来调用函数的情形。它以类似的方式处理不同的调用。注意,Python将实参封装到一个元组中,即便函数只收到一个值:

(‘pepperoni’,)

(‘mushrooms’, ‘green peppers’, ‘extra cheese’)

结合使用位置实参和任意数量实参

如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参 放在最后。Python先匹配位置实参和关键字实参,再将余下的实参都收集到最后一 个形参中。

1
2
3
4
5
6
7
def make_pizza(size, *toppings): 
"""概述要制作的比萨。"""
print(f"\nMaking a {size}-inch pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

基于上述函数定义,Python将收到的第一个值赋给形参size ,并将其他所有值都 存储在元组toppings 中。在函数调用中,首先指定表示比萨尺寸的实参,再根据 需要指定任意数量的配料。

注意 你经常会看到通用形参名*args ,它也收集任意数量的位置实参。

使用任意数量的关键字实参

有时候,需要接受任意数量的实参,但预先不知道传递给函数的会是什么样的信 息。在这种情况下,可将函数编写成能够接受任意数量的键值对——调用语句提供 了多少就接受多少。

1
2
3
4
5
6
7
8
9
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)

编写函数时,能以各种方式混合使用位置实参、关键字实参和任意数量的实参。知道这些实参类型大有裨益,因为阅读别人编写的代码时经常会见到它们。要正确地使用这些类型的实参并知道其使用时机,需要经过一定的练习。

注意 你经常会看到形参名**kwargs ,它用于收集任意数量的关键字实参。

6、将函数存储在模版中

使用函数的优点之一是可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。你还可以更进一步,将函数存储在称为模块 的独立文件 中,再将模块导入到主程序中。import 语句允许在当前运行的程序文件中使用模块中的代码。

通过将函数存储在独立的文件中,可隐藏程序代码的细节,将重点放在程序的高层逻辑上。这还能让你在众多不同的程序中重用函数。将函数存储在独立文件中后, 可与其他程序员共享这些文件而不是整个程序。知道如何导入函数还能让你使用其他程序员编写的函数库。

导入整个模块

要让函数是可导入的,得先创建模块。 模块是扩展名为.py的文件,包含要导入到程序中的代码。

只需编写一条import语句并在其中指定模块名,就可在程序中使用该模块中的所有函数。如果使用这种import 语句导入了名为 module_name.py的整个模块,就可使用下面的语法来使用其中任何一个函数:

1
module_name.function_name()

导入特定的函数

还可以导入模块中的特定函数,这种导入方法的语法如下:

1
2
from module_name import function_name
from module_name import function_0, function_1, function_2

使用 as 给函数指定别名

如果要导入函数的名称可能与程序中现有的名称冲突,或者函数的名称太长,可指定简短而独一无二的别名 :函数的另一个名称,类似于外号。要给函数取这种特殊外号,需要在导入它时指定。

1
from module_name import function_name as fn

使用 as 给模块指定别名

还可以给模块指定别名。通过给模块指定简短的别名,让你能够更轻松地调用模块中的函数。

1
import module_name as mn

导入模块中的所有函数

1
from module_name import *

import 语句中的星号让Python将模块pizza 中的每个函数都复制到这个程序文件 中。由于导入了每个函数,可通过名称来调用每个函数,而无须使用句点表示法。 然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法。这是因为如 果模块中有函数的名称与当前项目中使用的名称相同,可能导致意想不到的结果: Python可能遇到多个名称相同的函数或变量,进而覆盖函数,而不是分别导入所有 的函数。

最佳的做法是,要么只导入需要使用的函数,要么导入整个模块并使用句点表示 法。这让代码更清晰,更容易阅读和理解。

7、函数编写指南

  • 应给函数指定描述性名称,且只在其中使用小写 字母和下划线。

  • 每个函数都应包含简要地阐述其功能的注释。该注释应紧跟在函数定义后面,并采 用文档字符串格式。

  • 给形参指定默认值时,等号两边不要有空格:

    1
    2
    def function_name(parameter_0, parameter_1='default value')
    function_name(value_0, parameter_1='value')
  • 如果形参很多,导致函数定义的长度超过了79字符,可在函数定义中输入左 括号后按回车键,并在下一行按两次Tab键,从而将形参列表和只缩进一层的函数体 区分开来。

    1
    2
    3
    4
    def function_name( 
    parameter_0, parameter_1, parameter_2,
    parameter_3, parameter_4, parameter_5):
    function body...
  • 如果程序或模块包含多个函数,可使用两个空行将相邻的函数分开,这样将更容易知道前一个函数在什么地方结束,下一个函数从什么地方开始。

  • 所有import 语句都应放在文件开头。唯一例外的情形是,在文件开头使用了注释来描述整个程序。

二、类

面向对象编程是最有效的软件编写方法之一。在面向对象编程中,你编写表示现实世界中的事物和情景的类,并基于这些类来创建对象。编写类时,你定义一大类对象都有的通用行为。基于类创建对象 时,每个对象都自动具备这种通用行为,然后可根据需要赋予每个对象独特的个性。

1、创建和使用类

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog: 
"""一次模拟小狗的简单尝试。"""

def __init__(self, name, age):
"""初始化属性name和age。"""
self.name = name
self.age = age
def sit(self):
"""模拟小狗收到命令时蹲下。"""
print(f"{self.name} is now sitting.")
def roll_over(self):
"""模拟小狗收到命令时打滚。"""
print(f"{self.name} rolled over!")

根据约定,在Python中,首字母大写的名称指的是类。这个类定义中没有圆括号,因为要从空白创建这个类。编写了一 个文档字符串,对这个类的功能做了描述。

方法 __init()__

类中的函数称为方法 ,有关函数的一切都适用于方法

__init()__是一个特殊方 法,每当你根据Dog 类创建新实例时,Python都会自动运行它。在这个方法的名称 中,开头和末尾各有两个下划线,这是一种约定,旨在避免Python默认方法与普通 方法发生名称冲突。务必确保__init__()的两边都有两个下划线,否则当你使用类来创建实例时,将不会自动调用这个方法,进而引发难以发现的错误。

我们将方法__init__()定义成包含三个形参:self 、name 和age 。在这个方法的定义中,形参self 必不可少,而且必须位于其他形参的前面。为何必须在方法定义中包含形参self 呢?因为Python调用这个方法来创建Dog 实例时,将自动传入实参self 。每个与实例相关联的方法调用都自动传递实参self ,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。创建Dog 实例时,Python 将调用Dog 类的方法__init__()。我们将通过实参向Dog() 传递名字和年龄, self会自动传递,因此不需要传递它。每当根据Dog 类创建实例时,都只需给最后两个形参(name和age)提供值。

以self 为前缀的变量可供类中的所有方法使用,可以通过类的任何实例来访问。self.name = name获取与形参name相关联的值,并将其赋给变量name,然后该变量被关联到当前创建的实例。self.age = age的作用与此类似。像这样可通过实例访问的变量称为属性 。

Dog 类还定义了另外两个方法:sit()和roll_over()。这些方法执行时不需要额外的信息,因此它们只有一个形参self。我们随后将创建的实例能够访问这些方法,换句话说,它们都会蹲下和打滚。当前,sit()和roll_over()所做的有限,只是打印一条消息,指出小狗正在蹲下或打滚。

使用类创建实例

1
2
3
4
5
class Dog: 
--snip--
my_dog = Dog('Willie', 6)
print(f"My dog's name is {my_dog.name}.")
print(f"My dog is {my_dog.age} years old.")

通常认为首字母大写的名称(如Dog )指的是类,而小写的名称(如 my_dog )指的是根据类创建的实例。

可按需求根据一个类创建任意数量的实例,条件是将每个实例都存 储在不同的变量中,或者占用列表或字典的不同位置。

2、使用类和实例

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
39
40
class Car: 

def __init__(self, make, model, year):
"""初始化描述汽车的属性。"""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0 # 给属性指定的默认值

def get_descriptive_name(self):
--snip--

def read_odometer(self):
"""打印一条指出汽车里程的消息。"""
print(f"This car has {self.odometer_reading} miles on it.")

def update_odometer(self, mileage):
"""将里程表读数设置为指定的值。"""
self.odometer_reading = mileage

def increment_odometer(self, miles):
"""将里程表读数增加指定的量。"""
self.odometer_reading += miles

my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()

# 修改属性的值
# a.直接修改属性的值
my_new_car.odometer_reading = 23
my_new_car.read_odometer()

# b.通过方法修改属性的值
my_new_car.update_odometer(23)
my_new_car.read_odometer()

# c.通过方法对属性的值进行递增
my_used_car.increment_odometer(100)
my_used_car.read_odometer()

注意 你可以使用类似于上面的方法来控制用户修改属性值(如里程表读 数)的方式,但能够访问程序的人都可以通过直接访问属性来将里程表修改为任何值。要确保安全,除了进行类似于前面的基本检查外,还需特别注意细节。

3、继承

编写类时,并非总是要从空白开始。如果要编写的类是另一个现成类的特殊版本, 可使用继承 。一个类继承 另一个类时,将自动获得另一个类的所有属性和方法。 原有的类称为父类 ,而新类称为子类 。子类继承了父类的所有属性和方法,同时还可以定义自己的属性和方法。

子类的方法__init__()

在既有类的基础上编写新类时,通常要调用父类的方法__init__() 。这将初始化 在父类__init__() 方法中定义的所有属性,从而让子类包含这些属性。

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
39
class Car: 
"""一次模拟汽车的简单尝试。"""

def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0

def get_descriptive_name(self):
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()

def read_odometer(self):
print(f"This car has {self.odometer_reading} miles on it.")

def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")

def increment_odometer(self, miles):
self.odometer_reading += miles

class ElectricCar(Car):
"""电动汽车的独特之处。"""

def __init__(self, make, model, year):
"""初始化父类的属性。"""
super().__init__(make, model, year)
self.battery_size = 75

def describe_battery(self):
"""打印一条描述电瓶容量的消息。"""
print(f"This car has a {self.battery_size}-kWh battery.")

❺ my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())

首先是Car 类的代码(见❶)。创建子类时,父类必须包含在当前文件中,且位于子类前面。在❷处,定义了子类ElectricCar 。定义子类时,必须在圆括号内指 定父类的名称。方法__init__() 接受创建Car 实例所需的信息(见❸)。

❹处的super()是一个特殊函数,让你能够调用父类的方法。这行代码让Python调 用Car 类的方法__init__(),让ElectricCar 实例包含这个方法中定义的所有属性。父类也称为 超类 (superclass),名称super由此而来。

给子类定义属性和方法

让一个类继承另一个类后,就可以添加区分子类和父类所需的新属性和新方法了。

重写父类的方法

对于父类的方法,只要它不符合子类模拟的实物的行为,都可以进行重写。为此, 可在子类中定义一个与要重写的父类方法同名的方法。这样,Python将不会考虑这个父类方法,而只关注你在子类中定义的相应方法。

将实例用作属性

使用代码模拟实物时,你可能会发现自己给类添加的细节越来越多:属性和方法清单以及文件都越来越长。在这种情况下,可能需要将类的一部分提取出来,作为一 个独立的类。

4、导入类

导入单个类

car.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""一个可用于表示汽车的类。""" 

class Car:
"""一次模拟汽车的简单尝试。"""

def __init__(self, make, model, year):
"""初始化描述汽车的属性。"""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0

def get_descriptive_name(self):
"""返回整洁的描述性名称。"""
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()

my_car.py

1
2
3
4
5
6
7
from car import Car 

my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())

my_new_car.odometer_reading = 23
my_new_car.read_odometer()

在一个模块中存储多个类

虽然同一个模块中的类之间应存在某种相关性,但可根据需要在一个模块中存储任意数量的类。

从一个模块中导入多个类

可根据需要在程序文件中导入任意数量的类。

1
from car import Car, ElectricCar

导入整个模块

还可以导入整个模块,再使用句点表示法访问需要的类。这种导入方式很简单,代码也易于阅读。因为创建类实例的代码都包含模块名,所以不会与当前文件使用的任何名称发生冲突。

1
import car

导入模块中的所有类

要导入模块中的每个类,可使用下面的语法:

1
from module_name import *

不推荐使用这种导入方式,原因有二。第一,如果只看文件开头的import 语句, 就能清楚地知道程序使用了哪些类,将大有裨益。然而这种导入方式没有明确地指出使用了模块中的哪些类。第二,这种方式还可能引发名称方面的迷惑。如果不小 心导入了一个与程序文件中其他东西同名的类,将引发难以诊断的错误。这里之所以介绍这种导入方式,是因为虽然不推荐使用,但你可能在别人编写的代码中见到 它。

需要从一个模块中导入很多类时,最好导入整个模块,并使用 module_name.ClassName 语法来访问类。这样做时,虽然文件开头并没有列出用到的所有类,但你清楚地知道在程序的哪些地方使用了导入的模块。这也避免了 导入模块中的每个类可能引发的名称冲突。

在一个模块中导入另外一个模块

有时候,需要将类分散到多个模块中,以免模块太大或在同一个模块中存储不相关的类。将类存储在多个模块中时,你可能会发现一个模块中的类依赖于另一个模块中的类。在这种情况下,可在前一个模块中导入必要的类。

使用别名

导入类时,也可为其指定别名。

1
from electric_car import ElectricCar as EC

5、Python标准库

Python标准库是一组模块,我们安装的Python都包含它。

可以使用标准库中的任何函数和类,只需在程序开头包含一条简单的import 语句即可。

比如模块random

1
2
3
4
5
6
7
8
>>> from random import randint 
>>> randint(1, 6)
3
>>> from random import choice
>>> players = ['charles', 'martina', 'michael', 'florence', 'eli']
>>> first_up = choice(players)
>>> first_up
'florence'

一个有趣的函数是randint() 。它将两个整数作为参数,并随机 返回一个位于这两个整数之间(含)的整数

在模块random中,另一个有用的函数是choice() 。它将一个列表或元组作为参 数,并随机返回其中的一个元素

6、类编码风格

类名应采用 驼峰命名法 ,即将类名中的每个单词的首字母都大写,而不使用下划 线。实例名和模块名都采用小写格式,并在单词之间加上下划线。

对于每个类,都应紧跟在类定义后面包含一个文档字符串。这种文档字符串简要地描述类的功能,并遵循编写函数的文档字符串时采用的格式约定。每个模块也都应包含一个文档字符串,对其中的类可用于做什么进行描述。

可使用空行来组织代码,但不要滥用。在类中,可使用一个空行来分隔方法;而在 模块中,可使用两个空行来分隔类。

需要同时导入标准库中的模块和你编写的模块时,先编写导入标准库模块的import语句,再添加一个空行,然后编写导入你自己编写的模块的import语句。在包含多条import语句的程序中,这种做法让人更容易明白程序使用的各个模块都来自何处。