天池Python训练营Day3 - 对象、类、函数

1、对象(Object)

1.1 什么是对象?

  • 平时,代码存在硬盘里,cpu只用于运行代码。在运行代码时,使用的数据会临时存储在内存里。
  • cpu具有非常有限的存储空间。为提高运算效率,在cpu和硬盘之间增加内存,用于临时存储马上需要使用的数据。
  • 简单理解,对象即容器,是内存中存储指定数据的区域。整数、小数、布尔值、字符串、空值等都算作对象。

面向对象编程vs面向过程编程

面向对象的编程,是通过对象实现某项功能;面向过程的编程,是将实现该功能分解为一个个步骤,再通过对每个步骤进行抽象进行编程,通过逐一实现每个步骤,最终实现目标功能。

通过生活中的例子来解释:目标:倒1杯白开水,倒1杯果汁。

  • 面向对象:1. 定义对象:杯子*2,白开水,果汁;2. 定义动作(函数):倒水;3. 实现功能:将对象与动作相结合,即给杯子1号倒白开水,给杯子2号倒果汁
  • 面向过程:1. 取出杯子1号;2. 拿起白开水;3. 将白开水倒入杯子1号;4. 取出杯子2号;5. 拿起果汁; 6. 将果汁导入杯子2号。
  • 如果想要改变目标,比如倒2杯果汁,对于面向对象编程,只需要将果汁这个对象替换成白开水,其余不变;但对于面向过程编程,因为步骤5和6变了,导致需要将整个代码重写。
  • 由此可见,面向过程编程的可复用性比较低,难于维护,一个功能的代码只适用于该功能,如果要实现其他功能,即使是很相近的功能,也需重新编写全部代码。
  • 面向对象也有过程步骤(比如倒水),但是关注于对象,将这个对象涉及的过程都存在对象里(比如,将取出杯子这个动作,存在杯子这个对象里)。倒水其实也存在一个对象里,这样可以实现给不同的容器里倒各种饮料。

面向对象的编程思想是将各种功能保存到对应的对象里(例如,杯子是一个对象,倒水是一个对象,果汁也是一个对象),需要实现某个目标的时候,找到并调用需要的对象即可,如果没有现成的对象,则先创造对象再调用对象。面向对象编程更容易维护,也容易复用。

面向对象的三大特性:封装、继承和多态。

  • 封装:隐藏对象中一些不希望被外部访问到的属性或方法。
  • 继承:
  • 多态:

1.2 对象的结构

每个对象都保存三种数据:

  • id:用于标识对象的唯一性,类似于身份证号.

1)id由解析器生成的
2)在CPython中,id是对象的内存地址
3)用id(a)查看对象a的id

k = True
print (id('123') #140443389493616
print (id(k)) #4551017680
print (id(print)) #140443323327584
print (id(id)) #140443323326544
print (id(None)) #4550715496
  • 类型(type): 用于标识对象所属的类型(int, str, float, bool等)。类型决定了对象具有的功能。
  • 值(value): 用于存储对象的值/数据。

注意:

  1. 对象一旦创建,在被删除前,对象的id和type不可以改变
  2. 有些对象的值(value)可以改变,有些对象的value不可以被改变。
  3. 可以改变value的对象称为可变对象,不可以改变value的对象称为不可变对象。整型、字符串、浮点数、布尔值均为不可变对象,即一旦创建了对象"123"、12、123.2、True,则不可以对值本身进行修改,例如不可以将整数12中的1直接改成2。

1.2 变量与对象

  • 变量中存储的不是对象的值,而是对象的id(内存地址)。当使用变量时,实际上是通过对象id在查找对象。
  • 变量中保存的对象,只有在为变量重新赋值时才会改变。
  • 变量和变量之间是相互独立的,修改一个变量不会影响另一个变量。
  • 对象的类型转换:将原对象的值转换成指定的类型,并返回一个新的对象。变量与对象的关系

1.2.1 改对象与改变量的区别

a = [1,2,3] —> 这个操作是将列表对象的id赋值给变量a

a[0] = 4 ----> 这种操作是通过变量a修改列表对象的值,不修改变量a内所存储的对象id

a = [4,2,3] ----> 这种操作是修改变量a内所存储的对象id,即此时变量a已经指向了新的列表对象[4,2,3]。

原列表对象[1,2,3]仍在内存中存在,但再没有变量指向它了。这个没有变量能找到原列表[1,2,3],就像在太空中孤单游荡的失联宇宙飞船。。。

#【修改对象的值和修改变量的区别】
a = [1,2,3]  #将列表对象[1,2,3]的id赋值给变量a
print (a, 'id=', id(a))
# 运行结果
# [1, 2, 3] id=2604572200832

a[0] = 4  
#上述操作为通过变量a, 修改列表对象的值,使列表对象的值变为[4,2,3]。注意此时变量a仍指向原列表对象,即id(a)未发生变化,只是原列表对象的值改变
print(a, 'id=', id(a))
# 运行结果
# [4, 2, 3] id=2604572200832

a = [4,2,3]  
#上述操作创建了一个新的列表对象[4,2,3],并让变量a指向了新的列表对象,注意id(a)发生了变化。
print(a, 'id=', id(a))
# 运行结果
# [4, 2, 3] id=2604572426880
深拷贝 vs 浅拷贝

当修改对象的时候,如果有其他的变量也指向了同一个对象,则其他指向该对象的变量也会发生变化。

#【修改对象的影响】
a = [1,2,3]   #变量a指向对象[1,2,3]
b = a         #变量a赋值给变量b,即深拷贝,所以变量b也指向对象[1,2,3]
print ('a=', a, ', a_id=', id(a))
print ('b=', b, ', b_id=', id(b))
# 运行结果  a,b的id一样,说明指向同一个对象
#a=[1, 2, 3] , a_id=2604571277760
#b=[1, 2, 3] ,  b_id=2604571277760

b[0] = 10
print ('a=', a, ', a_id=', id(a))
print ('b=', b, ', b_id=', id(b))
# 运行结果   a,b的值都发生了变化,但是id都没有发生变化,因为所指向的对象的值发生了改变
#a=[10, 2, 3] , a_id=2604571277760
#b=[10, 2, 3] , b_id=2604571277760

当修改变量的时候,如果有其他的变量也指向了同一个对象,则其他指向该对象的变量不会发生变化。

#【修改对象的影响】
a = [1,2,3]   #变量a指向对象[1,2,3]
b = a         #变量a赋值给变量b,所以变量b也指向对象[1,2,3]

b = [10,2,3]  #将变量b指向了另一个对象[10,2,3]
print ('a=', a, ', a_id=', id(a))
print ('b=', b, ', b_id=', id(b))
#运行结果  变量b的值和id均发生了变化,而变量a的值和id不受影响
#a= [1, 2, 3] , a_id= 2604572370496
#b= [10, 2, 3] , b_id= 2604572370624
  • 拓展:浅拷贝的应用
#【修改对象的影响--浅拷贝】
a = [1,2,3]   #变量a指向对象[1,2,3]
b = a[:]      #变量a的值赋值给变量b,即浅拷贝,变量b指向新的对象[1,2,3]
print ('a=', a, ', a_id=', id(a))
print ('b=', b, ', b_id=', id(b))
# 运行结果   a,b的值相同,但是id不同
# a= [1, 2, 3] , a_id= 2604572251072
# b= [1, 2, 3] , b_id= 2604572235328
# 这种情况下,修改变量b就不会对对象[1,2,3]产生影响,也不会对变量a产生影响

2. 类(class)

2.1 什么是类

类(class):简单理解为创建对象的图纸,根据类来创建对象,对象是类的实例(instance), 一个类可以创建多个对象。如果多个对象都是通过一个类创建的,则称这些对象为一类对象。

python有内置类,比如int(), str()等,也允许自定义类。可以用type()函数查看对象所属的类。

如何自定义类:用class 关键字创建。语法:class 类名([父类]): 代码块

  • 如果自定义类,类名的开头需要用大写字母。

【示例-创建一个最简单的类】

#【示例-创建一个最简单的类】
class My_Class():
    pass   #此处暂不写任何功能,但空着会认为程序未完结,会报错,所以用pass跳过。
print (My_Class)
# 运行结果
# <class '__main__.My_Class'>    
  • 下图实例中,定义了一个名称为圆形的类,圆形类里有两个特征:半径和颜色(其中颜色是为了更便于区分不同的圆)。圆形类里还定义了两个方法,增加半径长度以及画圆。具体代码如下:
# Create a class Circle

class Circle(object):
    
    # Constructor
    def __init__(self, radius=3, color='blue'):
        self.radius = radius
        self.color = color 
    
    # Method
    def add_radius(self, r):
        self.radius = self.radius + r
        return(self.radius)
    
    # Method
    def drawCircle(self):
        plt.gca().add_patch(plt.Circle((0, 0), radius=self.radius, fc=self.color))
        plt.axis('scaled')
        plt.show()  

2.2 类的属性和方法

在类中可以定义变量和函数。在类中定义的变量,称为属性,将成为这个类的所有实例的公共属性,所有实例都可以访问;在类中定义的函数,称为方法,该类的方法可以通过实例进行访问。

  • 类的属性,类似于日常生活照事物的信息数据(例如人的姓名、身高、年龄等);类的方法,类似于日常生活中事物的行为(例如,人可以说话、走路等)。
  • 类中定义的属性和方法都是公共的,任何该类的实例(instance)都可以访问。
  • 注意,类里定义的属性及该属性对应的值(value)、类里定义的方法都是这个类的值(value), 而不是所创建的对象实例的值(value)。
  • 如果直接打印创建的实例的值,可以发现该实例是没有值的。
#【示例-在类里定义变量和函数】
class Person():
    name = 'Eva'  #定义Person类的属性name为Eva
    birth_year = 1991  #定义Person类的属性birth_year为1991
    age = lambda birth_year, year: year-birth_year
    def say_hello():
        print('hello')
    def say_bye(a): #此处随便定义一个形参,占位,防止报错
        print('bye')
    
p1 = Person()    #创建一个名字为p1的Person类型
print(p1)        #访问p1的值(因为p1没有值,所以返回的其实是type和id)
# 运行结果
# <__main__.Person object at 0x7ff8e5bb1b50>  

调用属性和调用方法的区别:

  • 调用属性:对象.属性,注意不带括号!!
  • 调用方法:对象.函数名(), 注意带括号!!

调用方法(即类里面定义的函数)和调用一般函数的区别:

  • 调用一般函数:调用时有几个函数就会传递几个实参
  • 调用方法:调用时解析器自动传递一个参数。所以方法中至少需要定义一个形参。(该参数到底是什么详见2.2.2 类方法的self)
#【调用属性】
print (p1.name)  #访问这个p1对象的name属性值(注意,name属性值不是p1对象赋予的,不存在p1对象的值里,而是类定义的,存在类的值里)
# 运行结果
# Eva

#【修改p1的name属性】
# 将p1的name属性值修改
# 注意,本次修改实际上是在p1这个对象的值里新增了一个name属性,这个name属性值为'Bob', 类似于对p1对象定义了专属名字。
p1.name = 'Bob' 
print(p1.name)
# 运行结果
# Bob

# 上述修改的是p1对象的name属性,没有修改p1对象所属类的name属性
# 所以,创建一个新的对象p2后,由于p2本身没有定义name的值,所以调用p2的name属性值时仍调用的是类Person里定义的属性值。
p2 = Person()
print(p2.name)
# 运行结果
# Eva

#【调用方法】
p1.say_hello()
# 运行结果,报错,因为类里的函数没有定义形参,但是在调用时会默认传递一个参数
# TypeError: say_hello() takes 0 positional arguments but 1 was given

p1.say_bye() #调用时默认传递一个参数
# 运行结果
# bye

2.2.1 调用对象的属性和方法

属性和方法的查找流程:

  • 当调用一个对象的属性/方法时,解析器会先在当前对象的值(value)里寻找是否含有该属性/方法。
  • 如有,则返回当前对象的属性值/方法;如没有,则去当前对象的类对象里寻找属性值/方法,如有则返回类对象的属性值/方法,如还是没有则返回错误。
  • 类里定义的属性/方法存在类的值里,通过对象定义或者修改后的属性/方法的值存储在对象里
  • 可以理解为,类对象里定义存储的属性值是对该类所有实例均可以调用的普适的属性值,例如将人这个类的年龄属性定义为0,代表所有人出生时年龄为0。对象里定义存储的属性值为当前对象特有的属性值,例如创建一个人后,这个人的默认年龄为人类出生时的年龄,即0岁。如果将这个人的年龄修改为18,表示这个人成长到18岁了。此时,人类的年龄属性仍为0(人类的出生年龄仍是0岁),即后续新创建的没有修改过年龄的人的年龄属性仍为0。

如果某个属性/方法是这个类里所有实例对象共有的,则应在类里定义这个属性/方法;如果某个属性/方法是某个或某几个实例对象特有的,则应该在对象里定义这个属性/方法。

  • 一般情况下,属性保存到实例对象中,方法保存到类中。
  • 因为,一般不同对象的属性信息不一样,但是会有同样的行为(方法)。比如人类都会说话、走路,但是每个人的名字、身高等属性会因人而异。

在类的方法(函数)中,不能直接调用类中定义的属性。

【实例-类的方法不能直接调用类中定义的属性】

#【说明类的方法不能直接调用类中定义的属性】
class Person():
    name = 'Bob'  #在类中定义name属性
    def say_hello(a):  #调用函数时默认传入一个参数
        print ('Hi, my name is %s' %name) #将name属性作为参数变量在函数中调用
p1 = Person()
p1.say_hello()
# 运行结果
# NameError: name 'name' is not defined
# 报错,name没有被定义。

# 将调用的参数变成某个对象的name属性,虽然实际中不会这么使用。
class NewPerson():
    name = 'Bob'
    def say_hello(a):
        print('Hi, my name is %s' %p2.name)
p2 = NewPerson()
p3 = NewPerson()
p2.say_hello()
# 运行结果
# Hi, my name is Bob

#修改p2名字后再尝试调用say_hello函数
p2.name = 'Test'
p2.say_hello()
p3.say_hello()
# 运行结果
# Hi, my name is Test
# Hi, my name is Test

2.2.2 类方法中的self形参解析

针对上文代码中,类中定义的方法可以调用某一个实例对象的属性,其实是因为类方法中,系统默认传递给该形参(即第一个形参)的实参是调用该方法的实例对象。

该默认形参的名称虽然可以随意取名,但是按照习惯通常称这个形参为"self“。

【类的方法中的self形参】

#【self形参的解析】
# 对上文代码中定义的Person类进行修改
class Person():
    age = 0
    def say_hello(a):
        print (a)  #看看参数a的id地址
        print ('Hi, my name is %s' %p1.name)
    def my_age(self):
        print('the info of self is', self)  #看一下参数self的id地址
        print('Hi, I am %s years old.' %self.age)
p1 = Person()
print ('the id of p1 is', id(p1)) #打印一下p1的id信息 
# 运行结果
# the id of p1 is 140376857803088

p1.name = 'Eva'
p1.say_hello() #调用类中定义的say_hello函数
# 运行结果
# the id of a is 140376857803088   
# Hi, my name is Eva
#发现传入实参后a的地址p1的地址一样,说明传入的实参就是p1.


p1.my_age()  #调用类中定义的my_age函数
# 运行结果
# the id of self is 140376857803088
# Hi, I am 0 years old.
# 发现传入实参后self的地址p1的地址一样,说明传入的实参就是p1,而且形参名字的改变不影响传入实参。

2.2.3 类的初始化-特殊方法init

为什么需要类的初始化,即__init__方法?

  • 假如类中有必须设置的属性(比如上述Person类say_hello方法中使用的name属性),但不同对象的该属性的值又不一样(比如每个人的名字不一样)。
  • 为避免创建对象时忘记设置该属性的值导致后续程序报错,需要在对象创建时自动设置该属性;
  • 但同时又不希望在类中直接定义该属性的值,而是希望在创建对象的时候对该属性赋值,从而保持该属性值的灵活性。

__init__是一种特殊方法(也称为魔术方法)。

  • 特殊方法都是以双下划线__开头,以双下划线__结尾。特殊方法会在特殊的时刻自动调用,无需用户手动调用。
  • init方法会在对象创建时马上调用。

【优化Person类,添加特殊方法init】

#【优化Person类,添加特殊方法init】
class Person():
    name = '这是存储在类中的姓名'  #这个name属性存储在Person类对象的value里
    def __init__(self):  #init属于类方法,所以也需要传递形参self
        print ('init方法调用啦')   #测试init方法的调用
        self.name = '这是存储在对象中的姓名'  #这个name属性存储在创建的实例对象的value里
    def my_age(self):
        print('Hi, I am %s years old.' %self.age)
    def say_hello(self):
        print ('Hi, my name is %s' %self.name)
        
p1 = Person()
p1.__init__()     #仅用于说明init可以手动调用,但实际编码中不要这么写,因为init会在特殊时刻自动调用
# 运行结果,发现__init__被调用了两次
# init方法调用啦
# init方法调用啦

print (p1.name)
# 运行结果
# '这是存储在对象中的姓名‘ 

上述代码中,p1 = Person()的实际运行流程

  1. 创建一个名字为p1的变量;
  2. 在内存中创建一个新的对象;
  3. 执行__init__(self)方法,给新的对象初始化属性;
  4. 将对象的id赋值给变量p1。

【继续优化Person类,使用特殊方法init初始化对象属性】

#【优化Person类,使用特殊方法init初始化对象属性】
class Person():
    name = '这是存储在类中的姓名'  #这个name属性存储在Person类对象的value里
    def __init__(self, name):  #init方法里传递name参数
        self.name = name  #这个name是外部传递的参数
    def my_age(self):
        print('Hi, I am %s years old.' %self.age)
    def say_hello(self):
        print ('Hi, my name is %s' %self.name)

p1 = Person()
# 运行结果 (提示传递的参数缺失)
# TypeError: __init__() missing 1 required positional argument: 'name'

p1 = Person('Eva')
p1.say_hello()
# 运行结果
# Hi, my name is Eva

2.2.4 类的基本结构

常用的类的基本结构

class 类名([父类]):

    公共的属性(需对属性赋值)

    #对象的初始化方法
    def __init__(self, [其他形参]):  ([]表示非必须参数)
        self.属性名 = 形参名

    #类的其他方法
    def method_1(self, [其他形参]):
        ......
    def 方法名(self, [其他形参]):
        .......

2.3 类与对象

创建一个对象,即是创建一个类的具体实例。以现实生活类比,人是一个类class, 每一个实体人是一个对象实例。

  • 以python中的内置类为例,int(), float(),str(), list()等都是类。比如, int()是整数类,a=int(10)创建一个int类的实例,等价于 a = 10。

结合2.1中的圆形类实例,以下创建了两个对象,一个是半径10的红色的圆,一个是半径为100的蓝色的圆(注意,因为圆形类里默认对象颜色是蓝色,所以在创建的时候没有传递颜色的参数)

# Create an object RedCircle
RedCircle = Circle(10, 'red')
# Create a blue circle with a given radius
BlueCircle = Circle(radius=100)
  • 获取该对象自带的方法(在圆形类里定义的)
# Find out the methods can be used on the object RedCircle
dir(RedCircle)
  • 获取该对象变量-半径和颜色-的信息
# Print the object attribute radius
RedCircle.radius
RedCircle.color
  • 改变该对象半径变量的值
# Set the object attribute radius
RedCircle.radius = 1
RedCircle.radius
  • 调用该对象自带的方法-画圆
# Call the method drawCircle
RedCircle.drawCircle()
  • 调用该对象自带的方法-增加半径
# Use method to change the object attribute radius
RedCircle.add_radius(2)
print('Radius of object of after applying the method add_radius(2):',RedCircle.radius)
RedCircle.add_radius(5)
print('Radius of object of after applying the method add_radius(5):',RedCircle.radius)

2.4 封装

封装:在定义类的时候,通过一些方式,隐藏对象中一些不希望被外部访问到的属性或方法。

注意,没有方式可以完全隐藏属性或者方法使得外部无法访问,但可以通过一些方式使得这些希望隐藏的属性或方法很难被外部访问到。

隐藏属性的方法:

  • 将对象的属性名改成外部不知道名字,比如在定义类的时候,将属性名改为外部不知道的名字。

【隐藏属性】

#【示例-隐藏属性方法1】
class Dog:
    def __init__(self, name):  #注意init两边各有两个下划线
        self.hidden_name = name   #在类里,属性名是hidden_name
    def say_hello(self):
        print(f'hello, I am {self.hidden_name}.')
   
d = Dog('旺财') #从外部传入狗的名字
d.say_hello()
# 运行结果
# hello, I am 旺财.

d.name = '小黑'  #除非外部查看类的定义,否则一般很难猜到属性名是hidden_name,所以一旦创建了对象d,则很难从外部直接修改狗的名字
d.say_hello()
# 运行结果
# hello, I am 旺财.

d.hidden_name = '小白'  #但如果真找出来了,通过属性名还是可以从外部修改对象的名字的。 
d.say_hello()
# 运行结果
# hello, I am 小白.

在隐藏了属性后,有时候外部还是会需要获取属性,则通过专门预留的通道进行访问,即使用getter、setter方法。

  • getter方法泛指用于获取属性值的方法,这种方法通常采用”get_属性名“命名;
  • setter法泛指用于修改属性值的方法,这种方法通常采用”set_属性名“命名。
#【在类中设置getter和setter方法】
class Person:
    def __init__(self, name, age):
        self.hidden_name = name
        self.hidden_age = age
    def get_name(self):  #返回对象的name属性值
        return self.hidden_name
    def get_age(self):   #返回对象的age属性
        return self.hidden_age
    def set_name(self, name): #修改对象的name属性
        self.hidden_name = name
        print(f'reset {self.hidden_name}\'s age to {age}.')
    def set_age(self, age): #修改对象的age属性,并添加校验规则
        if int(age) >= 0:
            self.hidden_age = age
        else:
            print('age属性值需为不小于0的整数')
p1 = Person('Eva', 18)
print(p1.get_name())
print(p1.get_age())
# 运行结果
# Eva
# 18
p1.set_age(10)
print(p1.get_age())
# 运行结果
# reset Eva's age to 10.
# 10

p1.set_age(-10)
print(p1.get_age())
# 运行结果
# age属性值需为不小于0的整数
# 18

使用了封装后,增加了类的定义的复杂程度,但是增强了数据的安全性。一是,隐藏了属性,使得调用者无法从外部随意修改对象的属性;二是,增设专门用于获取和修改属性的专用通道,可以控制属性的可读性和值的范围。

  • 如果是只读的属性,可以只保留getter方法,而不设置setter方法
  • 如果是可修改的属性,可以使用getter和setter方法。

2.4. 类型转换

类型转换四个函数:int(), float(), str(), bool()

  1. int(): 将其它类型的对象转换成整型,返回转换后的对象。对于不能转换为整型的对象(例如None),系统抛出ValueError,提示值错误。
a = '123'
b = int(a)
print (a, type(a)) #123 <class 'str'> 
#变量a的值和类型都没有改变
print (b, type(b)) #123 <class 'int'>
 #转换成整型后的对象返回给了变量b

m = 123.6
n = int(m)
print (n, type(n))  #123 <class 'int'>
 #浮点转整数的时候,舍去小数位,只取整数位。

x = True
y = int(True)
print (y)  #1  
#布尔值转整型,True -> 1, False -> 0
  1. float(): 将其它类型的对象转换成浮点型,返回转换后的对象,与转换整型的规则基本一样。对于不能转换为浮点型的对象(例如None),系统抛出ValueError,提示值错误。
a = 123
b = float(a)
print(b)  #123.0
  1. str(): 可以将对象转换成字符串
  2. bool(): 对于所有表示为空的对象(比如空列表, 空字符串, None, 0等)转换成False;其他转换为True。

3、函数(Function)

3.1 函数简介

  1. 函数是一种对象
  • 对象是内存中存储指定数据的区域。所以,可以理解,函数是专门用于储存可执行的代码的对象,且可以在需要的时候,通过调用函数,运行这些代码。
  • 有了函数,通过调用函数可以重复使用某些代码,而不需要每次复制粘贴这批代码,也便于后期维护。(函数与代码块的关系,类似于变量与数值的关系)
  1. 创建函数(定义函数)和调用函数
#定义函数
def 函数名([形参1,形参2......]):
    代码块
#调用函数    
函数名(输入变量)

#【示例】
def add(a):
    '''
    给输入变量加1
    '''
    b = a +1 
    print (a, '如果你加1', b)
    return(b)
    
add(1)

函数的运行过程

函数名代表函数对象,函数名()代表调用函数
print (函数名)会返回函数的地址

print (test) #<function test at 0x7fa2ed3ede60>
print (type(test))  #<class 'function'>
test  #程序没有任何反应
test()  #my first function

3.2 函数的参数

在定义函数时,可以在函数名后的()里定义多个形参,形参之间用逗号隔开。

3.2.1 形参vs实参

  • 形参:形式参数,定义形参相当于在函数内部声明了一个变量,但并没有对该变量赋值
  • 实参:实际参数,如果在创建函数时定义了形参,调用函数时必须传递实参,实参将会给对应的形参赋值。定义函数时有几个形参,调用时就要有几个实参。
#【在创建带形参的函数,并传递实参】
#定义带形参的函数
def sum(a, b): #a, b是函数sum的形参
    print (a+b)  #sum函数的功能是将两个参数a,b相加然后打印出来
#调用函数时传递实参
sum(10, 20) #10, 20函数sum的实参,分别给形参a,b赋值10,20
#调用函数时传递部分实参
sum(10,#系统报错,缺少必须的实参b,TypeError: sum() missing 1 required positional argument: 'b'

定义形参时,可以为形参指定默认值。如果在创建函数的时候指定了形参的默认值,在调用函数的时候,如果用户没有传递实参,则默认值生效,反之默认值不生效。

指定默认值时,需从后向前指定,即含有默认值的参数需要放在不含默认值的参数的后面,否则系统报错 “SyntaxError: non-default argument follows default argument”。

#【定义函数时指定形参默认值】
def display(a, b=0):
    print ('a=',a)
    print ('b=',b)
display(1,2) #调用函数时对两个形参都赋值,则默认值不生效
#a=1 
#b=2
display(1,)  #调用函数时对一个形参不赋值,则默认值生效
#a=1
#b=0

#【定义函数时,指定默认值的形参放在未指定默认值的形参之前】
def display(a = 1, b):
    print ('a=',a)
    print ('b=',b)
display(1,2)
#【运行结果】
#  File "<ipython-input-18-15f09282e2f5>", line 2
#    def displayer(a =1, b):
#                  ^
# SyntaxError: non-default argument follows default argument

3.2.2 参数的传递

实参的传递方式:位置参数,关键字参数

  • 位置参数:将对应位置的实参赋值给对应位置的形参。
  • 关键字参数:直接根据参数名传递参数,而无需根据位置传递参数。
  • 位置参数和关键字参数可以混合使用,但是位置参数必须写在关键字参数前面。
#练习实参的传递方式:位置参数、关键字参数
def intro(name, age, country):
    print (f'Hi, my name is {name}, I\'m {age}, and I\'m from {country}.')
    
#采用位置参数,按位置传递实参
intro ('Anna', 17, 'China')
intro (21, 'Max', 'Hebei')
#【运行结果】
#Hi, my name is Anna, I'm 17, and I'm from China.
#Hi, my name is 21, I'm Max, and I'm from Hebei.

#采用关键字参数,按关键字传递实参
intro(age = 30, name = 'Maggie', country='Thailand')
#【运行结果】
#Hi, my name is Maggie, I'm 30, and I'm from Thailand.
  • 实参可以传递任意类型的对象,甚至函数本身也可以做实参。在调用函数时,解析器不会检查实参的类型
#【任意类型实参都可以-示例】
def test(a):
    print ('a =', a)
test(1)  #整型
test(0.666) #浮点型
test('im a test')  #字符串
test(True)  #布尔值
test(['hello', 'readers'])  #列表
test(('have', 'a', 'nice', 'day'))   #元组
test(dict(name = 'Tom', type = 'cat', hobby = 'catch Jerry')) #字典

# 运行结果
# a = 1
# a = 0.666
# a = im a test
# a = True
# a = ['hello', 'readers']
# a = ('have', 'a', 'nice', 'day')
# a = {'name': 'Tom', 'type': 'cat', 'hobby': 'catch Jerry'}
#【函数本身也可以做实参】
def welcome(a):
   print ('hello,',a)
   
def test(b):
   print('b =', b)

test(welcome)  #此处是将函数welcome本身作为实参,而不是调用welcome函数后的结果
# 运行结果   
# b = <function welcome at 0x00000298BE5E80D0>  
  • 不限制实参,有时候会造成问题。比如,当实参传递的对象类型不满足函数里计算的要求,系统会报错
#【实参类型不符合函数要求时,会导致报错】
def test(a,b):
   print('result =', a+b)
test(10,11)
# 运行结果  因为两个整型可以相加,所以函数正常运行
# 21
test(10, '11')
# 运行结果  因为整型和字符串不能相加,所以函数运行失败,系统报错
# TypeError: unsupported operand type(s) for +: 'int' and 'str'

函数里对形参的操作可能会影响实参传递的对象本身,继而影响所有指向该对象的变量

  • 在函数中,对形参进行重新赋值(改变量),不会影响其他变量
  • 但如果形参指向了一个可变对象,并通过形参修改了对象的值,则所有指向该对象的 变量都会受到影响
# 【修改函数中的形参对实参的影响】
def change(a): 
    a[0] = 1
    print('a的id:', id(a))
    print ('a =', a)
c = ['hello', 'you']
print ('c的id:', id(c))
change(c)   #向函数change传入实参c
print (c)
# 运行结果
# c的id: 2855052165248   #变量c所指向的对象的内存地址
# a的id: 2855052165248   #形参a和实参c的id一样,说明两者指向了同一个对象,说明实参c传递的是对象的地址。
# a = [1, 'you'] #函数通过修改形参,修改了对象的值
# [1, 'you']   #因为对象的值被修改,所以指向该对象的c也受到影响
  • 如果希望避免函数中对形参的操作影响到实参指向的可变对象,则可以使用实参名.copy() 方法或者实参名.[:] ,向函数传递实参所指对象的副本(即创建一个和实参所指对象值相同的新对象),而不是传递对象本身。
#【向函数传递实参副本】
def change(a): 
    a[0] = 1
    print('a的id:', id(a))
    print ('a =', a)
c = ['hello', 'you']
print ('c的id:', id(c))
change(c.copy())   #向函数change传入实参c的副本
print (c)
# 运行结果
# c的id: 2855052216000
# a的id: 2855052218304   #形参a和实参c指向了不同的对象
# a = [1, 'you']
# ['hello', 'you']

change(c[:])   #向函数change传入实参c的副本
print (c)
# 运行结果
# a的id: 2855052216000   #形参a的id又发生了变化,和实参c指向了不同的对象
# a = [1, 'you']
# ['hello', 'you']

3.2.3 不定长参数(可变参数)

当函数不确定要使用多少个参数时(比如,求任意个数字之和),可以设置不定长参数。

不定长参数有两种类型:1. 不定长位置参数(*形参名),2. 不定长关键字参数(**形参名)

  1. 不定长位置参数(*形参名):定义函数的时候,在形参前面加一个星号(例如,*a),该形参a就会获取接收所有剩余的位置参数,类似元组的解构。 这个不定长的形参a不能接收关键字参数。
  2. 不定长关键字参数(**形参名):定义函数的时候,在形参前面加两个星号(例如,**b),该形参b就会获取接收所有剩余的关键字参数,但这个形参b不能接收位置参数。
  • 实际上,向只接受位置参数的不定长位置形参(*a)传入多个实参后,这个参数a的类型会变为元组;而向只接受关键字参数的不定长关键字形参(**b)传入多个实参后,这个参数b的类型会变为字典。
  • 不过,一个函数里两种类型的不定长参数分别最多只能有一个(不然实参传进来就不知道该如何分配了)

不定长位置参数(*a)的用法

#【设置只接受位置参数的不定长位置参数*a】
def show_info(*a):
    print ('a=',a)
    print ('a tpye is', type(a))

show_info(123,456,'mamamia')
# 运行结果
# a= (123, 456, 'mamamia')
# a tpye is <class 'tuple'>
  • 使用不定长位置参数(*a)实现对任意个数的数字求和
#【定义一个函数,可以对任意个数字求和】
def sum_num(*a):
    # 创建一个变量用于记录结果
    result = 0
    # 遍历元组,求和
    for num in a:
        result += num
    return (result) 
# 调用函数,并对(3,4,5,6,7)求和
sum_num(3,4,5,6,7)
# 运行结果
# Out: 25

# 使用不定长位置参数的时候,不传入参数也不会因参数个数不匹配而报错
sum_num()
# 运行结果
# Out: 0
  • 不定长位置参数(*a)只接收位置参数,不接收关键字参数。
# 【不定长位置参数接收关键字参数,系统报错】
def sum_num (*nums):
    result = 0
    for num in numes:
        result += num
    return (result)
sum_num (a=1, b=2, c=3)
# 运行结果  因为对不定长位置参数传递了关键字参数,所以系统报错
# TypeError: sum_num() got an unexpected keyword argument 'a'

不定长关键字参数(**b)的用法

#【设置不定长关键字参数**b】
def show_info (**b):
    print ('b=', b)
    print ('b tpye is', type(b))
show_info (a=5,b=6,c=7)
# 运行结果   参数b变成了字典
# b= {'a': 5, 'b': 6, 'c': 7}
# b tpye is <class 'dict'>
  • 不定长关键字参数(**b)只接收关键字参数,不接收位置参数。
#【不定长关键字参数,不接收位置参数】
def show_info (**b):
    print ('b=', b)
    print ('b tpye is', type(b))
show_info (1,2,'hello', a=5,b=6,c=7)
# 运行结果   系统报错提示,**b不接收位置参数,但是传递的实参里有三个位置参数
# TypeError: show_info() takes 0 positional arguments but 3 were given
  • 【拓展 - *的另一种用法】如果在定义函数的时候,在所有形参的位置最前面加星号* (示例:*,a,b,c),代表该函数的所有形参都必须是关键字参数。
#【在所有形参的位置最前面加*】
def print_name(*, names, tag):
    for name in names:
        print(tag,':', name) 
#这里的names是一个元组,是实参。 
names = ('Eva', 'Bob', 'Jane')
# 所有的参数都必须是关键字参数
print_name(tag='speaker name', names = names)
# 运行结果
# speaker name : Eva
# speaker name : Bob
# speaker name : Jane

# 如果有参数不是关键字参数,系统报错
print_name('speaker name', names = names)
# 运行结果  系统提示该函数没有位置参数,但却传递了一个位置参数。
# TypeError: print_name() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given

3.2.4 不定长参数与定长参数混合使用

  • 不定长位置参数(*a)可以与定长的位置参数和关键字参数混合使用。
#【不定长位置参数可以与定长的参数混合使用】
# 位置参数与不定长位置参数
def print_name(tag, *names):
    for name in names:
        print(tag,':', name)
print_name('speaker name','Eva', 'Bob', 'Jane')
# 运行结果
# speaker name : Eva
# speaker name : Bob
# speaker name : Jane
  • 但是单个的位置参数必须在不定长位置参数之前。换句话说,不定长位置参数后面或者没有任何参数,或者只能是关键字参数。
  • (分析一下:按照不定长位置形参的设计原理,即获取所有剩余的位置参数,如果不定长位置参数*a写在位置参数m前,按照python从左到右的执行逻辑,会先给不定长位置参数a赋值,即把所有传递的实参都优先传给了a.等到开始给排在后面的位置参数m赋值的时候,已经没有可以传递的实参了。)
#【将不定长位置参数*a写在定长参数之前】-- 错误写法
def print_name(*names, tag):
    for name in names:
        print(tag,':', name)    
print_name('speaker name','Eva', 'Bob', 'Jane')
# 运行结果  因为不定长位置形参*names抢走了所有的实参,导致没有值赋给形参tag,所以系统报错
# TypeError: print_name() missing 1 required keyword-only argument: 'tag'
  • 关键字参数与不定长位置参数(*a)的位置可前可后,但是传递实参的时候必须按函数定义时形参类型的顺序。如果定义的时候,关键字参数在前,不定长参数在后,则传递实参的时候,必须先传递关键字参数,再传递不定长参数。反之,亦然。
#【不定长位置参数与关键字参数】
#关键字参数的位置可以在不定长位置参数的位置之后
def print_name(*names, tag):
    for name in names:
        print(tag,':', name)        
#传递参数的时候,需要按函数定义时形参的顺序传递,最后传递关键字参数
print_name('Eva', 'Bob', 'Jane',tag='speaker name',)
# 运行结果  传递进来的实参依次传给不定长位置参数,直到关键字参数,由于不定长位置参数收不了,故而留了下来。
# speaker name : Eva
# speaker name : Bob
# speaker name : Jane

#如果传递参数的时候,没有按函数定义时的顺序传递
print_name(tag='speaker name','Eva', 'Bob', 'Jane')
# 运行结果   因为先传递了关键字参数,与函数定义时的顺序不符,出现系统报错,提示位置参数在关键字参数后面。 
# SyntaxError: positional argument follows keyword argument
  • 不定长关键字参数(**b)与不定长位置参数(*a)的不定长形参类似,可以与位置参数、关键字参数混合使用。
  • 定长的位置参数和关键字参数都必须在不定长关键字参数(**b)之前。换句话说,不定长关键字参数(**b)后面不能有任何参数。
#【不定长关键字参数与位置参数混合使用】
def person_info(name, **info):
    print ('name is', name)
    print (name, 'info', info)
person_info ('Anna', gender = 'Female', age = '18', height = '170')
# 运行结果
# name is Anna
# Anna info {'gender': 'Female', 'age': '18', 'height': '170'}

#【不定长关键字参数与定长的关键字参数混合使用】
# 传递实参的时候,单个的关键字参数可以放在任意位置,无需与函数定义时的位置一致。

# 放在最后面
person_info (gender = 'Female', age = '18', height = '170',name = 'Anna')
# 运行结果
# name is Anna
# Anna info {'gender': 'Female', 'age': '18', 'height': '170'}

#放在最前面
person_info (name = 'Anna',gender = 'Female', age = '18', height = '170')
# 运行结果
# name is Anna
# Anna info {'gender': 'Female', 'age': '18', 'height': '170'}

#放在中间
person_info (gender = 'Female', age = '18', name = 'Anna',height = '170')
# 运行结果
# name is Anna
# Anna info {'gender': 'Female', 'age': '18', 'height': '170'}
  • 如果在函数定义时,不定长关键字参数后面有其他形参,则系统报错
#【不定长关键字参数后面存在其他参数】
def person_info(**info,name):
    print ('name is', name)
    print (name, 'info', info)
    
# 如果属于位置参数    
person_info (gender = 'Female', age = '18', height = '170','Anna')
# 运行结果  系统报错
# SyntaxError: invalid syntax

# 如果是按关键字传参   
person_info (gender = 'Female', age = '18', height = '170',name = 'Anna')
# 运行结果  系统报错
# SyntaxError: invalid syntax

3.2.5 不定长参数的混合使用

  • 不定长位置参数和不定长关键字参数可以同时出现在一个函数里,但是一个函数里这两种类型的不定长参数分别最多只能有一个(不然实参传进来就不知道该如何分配了)。
#【不定长位置参数和不定长关键字参数混合使用】
def test_print (*a, **b):
    print ('a=', a)
    print ('b=', b)
test_print (1,2,3,4, x=5,y=6,z=7)
# 运行结果   a接收了所有的位置参数,b接收了所有的关键字参数
# a= (1, 2, 3, 4)
# b= {'x': 5, 'y': 6, 'z': 7}
  • 不定长参数、定长参数可以混合使用。形参类型顺序为位置参数、不定长位置参数、关键字参数、不定长关键字参数
#【多种不定长参数和定长参数混合使用】
def test_print (p, *a, x, **b):
    print ('p=', p)
    print ('a=', a)
    print ('x=', x)
    print ('b=', b)
    
test_print (1,2,3,4, x=5,y=6,z=7) 
# 运行结果  p为位置参数,a为不定长位置参数,x为关键字参数,b为不定长关键字参数
# p= 1
# a= (2, 3, 4)
# x= 5
# b= {'y': 6, 'z': 7}
  • 函数定义时,形参类型的位置放错的话,运行函数时会出现报错。
#【多种不定长参数和定长参数混合使用】-- 顺序错误
def test_print (p, x, *a, **b):
    print ('p=', p)
    print ('a=', a)
    print ('x=', x)
    print ('b=', b)
    
test_print (1,2,3,4, x=5,y=6,z=7)  
# 运行结果  系统报错,因为x被当成位置参数赋值2,然后又被当做关键字参数再次赋值,导致x有多个值。
# TypeError: test_print() got multiple values for argument 'x'

#如果在传递实参时,调整一下关键字参数的位置,也是不行的
test_print (1,x=5,2,3,4, y=6,z=7)  
# 运行结果  系统报错,因为关键字参数必须在位置参数之后
# SyntaxError: positional argument follows keyword argument

3.2.6 参数的解包

参数的解包:将传递进来的一串元素,逐一赋值给函数的形参。即实参是一包元素,传递实参的时候,将这包中的元素一个一个传递给函数的形参,而不是将这包元素作为一个实参传递给函数。

  1. 对序列类型的参数解包: 在传递序列类型的实参t时,在该实参t前面添加一个星号(*),则系统会自动将序列中的元素依次作为参数传递给函数。序列中的元素个数必须和函数中的形参个数一致,或者函数中有不定长位置形参。
#【序列类型的参数解包】
def show_info (a, b, c):
    print ('a =', a)
    print ('b =', b)
    print ('c =', c)

#未解包传递
test = (1,2,3)
show_info(test)
# 运行结果,此时test被当做一个参数传递函数,由于传递的实参个数小于形参个数,系统报错
# TypeError: show_info() missing 2 required positional arguments: 'b' and 'c'

#解包传递
test = (1,2,3)
show_info(*test)
# 运行结果
# a = 1
# b = 2
# c = 3
  • 当序列包里的元素个数不确定时,可以在函数中设置不定长位置形参。
#【序列类型的参数解包-使用不定长位置形参】
def show_info (a, b, *c):
    print ('a =', a)
    print ('b =', b)
    print ('c =', c)
    
#解包传递
test = (1,2,3,4,5,6,'End')
show_info(*test)
# 运行结果
# a= 1
# b= 2
# c= (3, 4, 5, 6, 'End')
  1. 对字典类型的参数解包: 在传递字典类型的实参t时,在该实参t前面添加两个星号(**),则系统会自动将序列中的元素依次作为参数传递给函数。字典中的元素个数必须和函数中的形参个数一致,且字典中元素的键名必须与形参名字一致。
#【字典类型的参数解包】
def show_info (name, age, gender):
    print ('a =', a)
    print ('b =', b)
    print ('c =', c)
 
#解包传递,字典实参里的键名必须与函数的形参名一致
test = {'name':'Eva', 'age':18, 'gender':'f'}
show_info(**test)
# 运行结果
# name = Eva
# age = 18
# gender = f

#解包传递,当字典实参里的键名与函数的形参名不一致时,系统报错
test = {'name':'Eva', 'age':18, 'sex':'f'}
show_info(**test)
# 运行结果
# TypeError: show_info() got an unexpected keyword argument 'sex'
  • 当字典包里的元素个数不确定时,可以在函数中设置不定长关键字形参。
#【字典类型的解包传递--使用不定长关键字形参】
def show_info (name, age, **rest):
    print ('name =', name)
    print ('age =', age)
    print ('rest =', rest)
    
# 解包传递
test = {'name':'Eva', 'age':18, 'gender':'f', 'country': 'China'}
show_info(**test)
# 运行结果
# name = Eva
# age = 18
# rest = {'gender': 'f', 'country': 'China'}

3.2.7 列表可以直接作为函数的参数

列表可以直接作为函数的参数。不过要注意,作为参数的列表可能在函数调用时直接改变原列表,所以在定义函数的时候要小心。

#【列表作为函数的参数】
def AddItems(list):
    list.append('a')
    list.append('b')
list = ['1','2']
AddItems(list)
print(list)

#结果
#['1','2','a','b']

3.3 函数的返回值

返回值:函数执行后返回的结果,通过return指定函数的返回值。return后面可以跟任意对象,甚至是函数对象。可以直接使用返回值,也可以用一个变量接收这个返回值。

#【有和没有返回值的区别】
# 1. 创建一个将两个变量相加的函数,没有返回值,不打印结果
def sum_num (a, b):
    c = a+b   

sum_num (2,3)
print (sum_num (4,3))
# 运行结果   计算结果并没有传递到函数外
# None


# 2. 创建一个将两个变量相加的函数,没有返回值,打印结果
def sum_num_p (a, b):
    c = a+b
    #将函数执行的结果c打印,不返回
    print(c)
sum_num_p (2,3)
# 运行结果
# 5
print (sum_num_p (4,3))
# 运行结果   
# 7   sum_num函数执行时打印的计算结果c
# None  但计算结果c并没有传递到函数外,所以打印的结果为None

# 3. 创建一个将两个变量相加的函数,有返回值
def sum_num_r (a, b):
    c = a+b
    #将函数执行的结果返回。结果c将传递到函数外
    return c
sum_num_r (2,3)
# 运行结果
# (空)

print (sum_num_r (4,3))
# 运行结果
# 7
  • 调用函数后,可以直接使用返回值,也可以用一个变量接收这个返回值。
#【可以直接使用返回值,也可以用变量接收返回值】
def sum_num_r (a, b):
    c = a+b
    #将函数执行的结果返回。结果c将传递到函数外
    return c

print (sum_num_r (4,3)+1)
# 运行结果
# 8

result = sum_num_r (4,3)
print(result+2)
# 运行结果
# 9
  • return后面可以跟函数对象,即返回值可以是函数
#【返回值是函数】
def return_fx(a,b):
    result = a+b
    def fx(a,b):   #这个fx是函数return_fx内部的函数
        print (a+b)
    return fx

r = return_fx(10,5)
print (r)
# 运行结果   由于r是一个函数对象,所以打印这个函数对象的信息
# <function return_fx.<locals>.fx at 0x7fc33bcef170>

r()
# 运行结果  调用了r接收的函数,即调用了函数fx()
# 15
  • 如果没有return或者return后面为空,则相当于return None
#以下三个函数的返回值一样
def sum_num_1(a,b):
    c = a+b
    
def sum_num (a, b):
    c = a+b
    return 
    
def sum_num_2 (a, b):
    c = a+b
    return (None)
         
print (sum_num_1 (4,3))
print (sum_num_2 (4,3))
print (sum_num (4,3))
# 运行结果
# None
  • 在函数中,一旦遇到return,函数结束。return后面的语句都不会执行。
def sum_num (a, b):
    print (a)
    return 
    print (b)
sum_num (5,6)
# 运行结果  
# 5 
# return后面的print语句不执行

def sum_num (a, b):
    i = 0
    while i < a:
        b += 2
        i += 1
        return (f'结果b={b}, 执行次数i={i}')
result = sum_num (3, 5)
print (result)  
# 运行结果
# 结果b=7, 执行次数i=1
# 由于return在循环中,所以在执行第一次循环时会遇到return,导致之后的循环均不再执行

print不带括号的函数名( 例如:print(fn) )和print带括号的函数名( 例如:print(fn()) ),是有区别的:

  1. print(fn):由于fn是函数对象,所以打印的是函数对象的信息
  2. print(fn()):由于fn()是调用函数fn,所以打印的是函数执行后的返回值。
#【示例,打印函数时带括号和不带括号的区别】
def fn (a, b):
    c = a+b
    return c
print ('1.不带括号:', fn)
print ('2.带括号:', fn(3,2)) 
# 运行结果
# 1.不带括号: <function fn at 0x7fc33bd5eb00>
# 2.带括号: 5

3.4 函数的说明

文档字符串doc str: 在定义函数的时候,可以在函数内部编写函数的说明文档,有助于他人或者之后使用的时候快速了解函数功能及用法。这个说明文档即文档字符串。

  • 在函数的第一行写一个字符串,就是文档字符串
  • 当编写了文档字符串以后,可以通过python内置的help()函数,查看所需函数的文档字符串。
  • 注意:使用help函数的时候,语法为help(函数名) ,函数名后面不要加括号。
#【文档字符串示例】
def sum_num (a, b):
    '''
    这是一个文档字符串示例
    本函数的作用是:求两个数值之和
    本函数的参数包括:
       a: 作用,类型, 默认值, blahblahblah
       b: 作用,类型, 默认值, blahblahblah
    '''
    c = a+b
    return c

help(sum_num)
# 运行结果
# Help on function sum_num in module __main__:
# sum_num(a, b)
#     这是一个文档字符串示例
#     本函数的作用是:求两个数值之和
#     本函数的参数包括:
#        a: 作用,类型, 默认值, blahblahblah
#        b: 作用,类型, 默认值, blahblahblah
  • 在定义函数时,可以在形参后面添加对类型、默认值的描述,也可以添加对函数返回值类型的描述等,使用语法为: 函数(形参名:类型)-> 函数返回值类型
  • 上述的描述也属于文档字符串,旨在帮助使用者理解和使用函数。但需注意,这些仅属于描述,对可传入的实参类型、函数返回值类型没有限制。
#【在定义函数时添加描述的示例】

def sum_num (a: int, b: int) -> int:
    c = a+b
    return c

print (sum_num(1,2))
help(sum_num)
# 运行结果
# 3
# Help on function sum_num in module __main__:
#  sum_num(a: int, b: int) -> int


# 对函数的描述不限制可传入的实参类型,不限制返回值类型
print (sum_num('a','b'))
# 运行结果
# ab
  • 如果函数的形参有默认值,则默认值放在类型描述之后,使用语法为: 函数(形参名:类型 = 默认值)-> 函数返回值类型
#【在函数上添加描述和默认值的示例】

def sum_num (a: int, b: int = 0) -> int:
    c = a+b
    return c

print (sum_num(1))
# 运行结果
# 1

3.5 作用域

作用域:变量生效的区域。
python里有两种作用域:全局作用域,函数作用域(也称为局部作用域)

3.5.1 全局作用域和函数作用域

全局作用域:

  • 在程序执行时被创建,在程序结束时被销毁;
  • 在一个程序中,函数之外的区域都是全局作用域;
  • 在全局作用域中定义的变量为全局变量,可以在程序中的任意位置(包括函数内)被访问。

函数作用域:

  • 也叫做局部作用域
  • 在函数调用时创建,在函数调用结束后销毁;定义函数的时候不会创建作用域;
  • 所以函数每调用一次,会创建一个新的函数作用域;
  • 在函数作用域中定义的变量为局部变量,局部变量只能在函数内部访问。
#【全局变量vs局部变量】

b = '你好' #b属于全局变量,可以在程序中任意位置访问
def fx():
    a = 'hello'  #a属于局部变量,只能在函数内部访问
    print ('这里是函数内部,a =', a)
    print ('这里是函数内部,b =', b)

fx()
# 运行结果
# 这里是函数内部,a = hello
# 这里是函数内部,b = 你好

print ('这里是函数外部,b =', b)
print ('这里是函数外部,a =', a)
# 运行结果  在函数外部调用a时,系统报错
# 这里是函数外部,b = 你好
# NameError: name 'a' is not defined

3.5.2 变量调用与作用域

调用变量的时候,会优先在当前作用域查找变量。如果找到,则使用;如果没找到,则向上一级作用域寻找。

  • 如果到全局作用域都没找的,则系统会抛出异常NameError。
  • 例如:如果在函数fx内部定义另一个子函数fn,子函数fn可以访问函数fx定义的变量。(因为子函数fn属于函数fx的内部,所以fn可以访问fx定义的变量)
#【局部变量的调用】

def fx():
    a = 'hello'  #定义局部变量
    def fn():
        print ('这里是fn内部, a =', a)  #在函数fn内部访问变量a
    fn()  #调用函数fn 

fx()  #调用函数fx
# 运行结果
# 这里是fn内部, a = hello
#【优先调用当前作用域的变量】

def fx():
    a = 'hello'  #定义一个局部变量a
    def fn():
        a = '你好' #在fn内部再定义一个局部变量a
        print ('这里是fn内部, a =', a)  #在函数fn内部访问变量a
    fn()  #调用函数fn 

fx()  #调用函数fx
# 运行结果
# 这里是fn内部, a = 你好

函数内部对变量的赋值,默认为对局部变量的赋值,不影响函数外部的变量值。

如果想在函数内部对全局变量的值进行修改,需要使用global关键字进行声明,将函数内部的局部变量变为全局变量。

#【global的使用-局部变量变为全局变量】

# 1. 不使用global
a = 'hello'   #全局变量a
def fx():
    a = '你好'  #局部变量a
    print ('函数内部的a =', a)
    
fx()
print ('函数外部的a =', a)
# 运行结果   函数内部对a的修改没有影响全局变量a的值
# 函数内部的a = 你好
# 函数外部的a = hello


# 2. 使用global
a = 'hello'   #全局变量a
def fx():
    global a   #声明函数内部的a为全局变量a
    a = '你好'  #对全局变量a重新赋值
    print ('函数内部的a =', a)
    
fx()
print ('函数外部的a =', a)
# 运行结果   在函数内部对之前定义的全局变量a的值进行了修改
# 函数内部的a = 你好
# 函数外部的a = 你好
  • 如果当函数内部修改全局变量值的时候,在函数外部还没有定义该全局变量,则会直接创建该全局变量。
#【全局变量出现顺序】

def fx():
    global a   #声明函数内部的a为全局变量a
    a = '你好'  #给全局变量a赋值
    print ('函数内部的a =', a)
    
fx()
a = 'hello'   #给全局变量a重新赋值
print ('函数外部的a =', a)
# 运行结果   先在函数内部创建全局变量a,然后函数外部重新赋值
# 函数内部的a = 你好
# 函数外部的a = hello

3.5.3 闭包作用域与nonlocal关键字

闭包:函数式编程的一种语法结构,一种特殊的内嵌函数。

  • 如果在一个某函数内部的函数里的外层函数的非全局作用域的变量进行引用,那么内部函数就被认为是闭包。
  • 通过闭包可以访问外层非全局作用域的变量,这个作用域称为闭包作用域。
  • 闭包的返回值通常是函数。
#【闭包作用域和变量引用】
def out_f(x):
    def inner_f(y):
        return x*y
    return inner_f
    
# 调用外部函数,将内部函数作为返回值赋给i    
i = out_f(6)
print (type(i))

# 调用内部函数
result = i(5)
print (result)

# 运行结果
# <class 'function'>
# 30

如果要修改闭包作用域中的变量,需使用 nonlocal 关键字,声明该变量为外层函数的非全局变量

#【nonlocal关键字使用】
# 使用了nonlocal关键字
def outer():
    num = 'hello'
    print('outer num is', num,'and its id is', id(num))
    def inner():
        nonlocal num  #声明nonlocal关键字
        num = '你好'
        print ('inner num is', num,'and its id is', id(num))
    inner()  #在外部函数调用内部函数
    print ('outer num after inner is', num,'and its id is', id(num))

outer()

# 运行结果(内外部函数的num变量均指向了同一个对象)
# outer num is hello and its id is 2541908695664
# inner num is 你好 and its id is 2541909346224
# outer num after inner is 你好 and its id is 2541909346224
#【nonlocal关键字使用】
# 不使用nonlocal关键字

def outer():
    num = 'hello'
    print('outer num is', num,'and its id is', id(num))
    def inner():
        num = '你好'
        print ('inner num is', num,'and its id is', id(num))
    inner() #在外部函数调用内部函数
    print ('outer num after inner is', num,'and its id is', id(num))
    
outer()

# 运行结果  (内外部函数的num变量指向了不同的对象)
# num in outer is hello and its id is 2541908695664
# num in inner is 你好 and its id is 2541909346032
# num in outer is hello and its id is 2541908695664

3.6 命名空间

命名空间namespace():变量存储的位置。

  • 每个变量都需要存储到指定的命名空间中
  • 每个作用域都有属于自己的命名空间,
  • 全局命名空间是该程序的命名空间,用于储存全局变量;函数作用域是函数的命名空间,用于储存函数的变量(局部变量)
  • 命名空间的本质是一个专门用于存储变量的字典
  • 访问命名空间的函数有:locals(), globals()

locals():用于获取当前作用域的命名空间。在全局作用域调用,获取全局命名空间;在函数作用域调用,获取函数的命名空间

#【在不同作用与调用用locals】
a = 'hello' 
print('当前作用域的命名空间: ', locals())
print(type(locals()))
# 运行结果
# 当前作用域的命名空间: {'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', '__package__': None, '__loader__': None, '__spec__': None, '__builtin__': <module 'builtins' (built-in)>, ......此处省略一堆其他内置变量......, 'a': 'Strangers'}
# <class 'dict'>

def fx():
    a = '你好'
    print ('在fx函数获取当前作用域的命名空间', locals())

def fx_1():
    a = 'hello'
    print ('在fx_1函数获取当前作用域的命名空间', locals())
fx()
fx_1()
# 运行结果
# 在fx函数获取当前作用域的命名空间 {'a': '你好'}
# 在fx_1函数获取当前作用域的命名空间 {'a': 'hello'}

globals():用于在任意位置获取全局作用域的命名空间。用法与locals()类似。

3.7 递归函数

递归:简单来说,即自己引用自己,就像在一个前后都是镜子的房间,可以无限看到自己。

递归函数:在函数内部调用自身函数,例如在函数fx()中调用fx。利用递归函数,将需要解决的大问题一层层分解为小问题,直到小问题不能再分解为更小的问题。

递归函数的两个要件:

  1. 基线条件:能分解到的最小的问题,当满足基线条件时,递归停止,类似于循环中的停止条件。缺少基线条件,函数会无限递归。
  2. 递归条件:将问题继续分解的条件。
#【创建递归函数】
# 创建一个函数,实现任意数的阶乘
# 将问题分解:
# 10!= 10*9!
# 9! = 9*8!
# 8! = 8*7!
# ......
# 2! = 2*1!
# 1! = 1   不能再分解,所以这就是基线条件。

def factorial(n: int):
    #说明基线条件
    if n == 1:
        return 1
    # 递归条件,不断对阶乘问题进行拆分
    return n*factorial(n-1)
factorial(15)
# 运行结果
# 1307674368000
  • 利用递归函数,计算斐波那契数列
#【利用递归函数计算斐波那契数列】

def recur_fibo(n):
    if n <= 1:
        return n
    return recur_fibo(n - 1) + recur_fibo(n - 2)

# 获得第n位斐波那契数    
recur_fibo(10)
# Out: 55

#打印整个数列
lst = []
for k in range(11):
    lst.appen(recur_fibo(k))
print (lst)
# 运行结果
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

3.8 高阶函数

高阶函数:接收至少一个函数作为参数,或者返回值是函数。

当使用函数作为参数时,其实是将指定的代码传递进目标函数。例如,将函数x作为函数y的参数,其实是将函数x的代码传递进了函数y。

  • 【示例】接收函数作为参数的函数
  • 注意将函数作为参数传递时,千万不要在函数参数后面加括号()
#【示例】接收函数作为参数的函数

#定义一个函数sub_list,该函数从传入的列表参数lst中选取符合函数参数func要求的元素并保存到新列表中,返回新列表
def sub_list(func, lst):
    new_list = []
    for n in lst:
        if func(n):
            new_list.append(n)
    return new_list

#定义一个函数fn2,该函数用于检查一个任意的数字是否为偶数
def fn2(num):
    if num % 2 == 0:
        return True
    return False

lst = [1,2,3,4,5,6,7,8,9]
#将fn2作为参数传递进sub_list,创建了从指定列表中获取偶数的函数
print (sub_list(fn2, lst))

#运行结果
#[2,4,6,8]

3.8.1 用函数作参数:filter()

上述示例中的功能可以用**python内置的函数filter()**实现。

  • filter(function, iterable): 第一个参数是函数,第二个参数是一个可迭代的数据结构(比如序列)
  • 该函数功能:从可迭代的数据结构(比如序列)中过滤出符合function条件要求的元素,保存到一个新的可迭代的数据结构(比如序列)。
  • 该函数的返回值为一个可迭代的数据结构(比如序列)
#【filter()函数使用】
#定义一个函数fn2,该函数用于检查一个任意的数字是否为偶数
def fn2(num):
    if num % 2 == 0:
        return True
    return False
lst = [1,2,3,4,5,6,7,8,9]

print (filter(fn2,lst))
# 运行结果   因为返回值是一个可迭代的数据结构,直接打印不出里面的元素,打印的是这个结构所在的位置。
# <filter object at 0x0000028A359465B0>

print (list(filter(fn2, lst)))
# 运行结果  将返回的结果转换成list,可以打印出元素
# [2,4,6,8]

注意!!将函数作为参数时,不要在函数名后面带括号。函数名后面加括号,代表调用函数,得到的是函数返回值。

3.8.2 用函数作参数:sort()和sorted()

sort()方法:可以使用key关键字参数,该关键字参数需要一个函数作为参数,通过比较函数参数的返回值,进行多种排序,例如按字符串长度排序等。

  • 【sort()与函数参数结合示例】
#【sort()+key关键字参数-示例】
# 将以下列表中的字符串按照字符长度从短到长排序
strs = ['aaa', 'b','ccccccc','dddd']
strs.sort(key=len)
print(strs)

sorted()函数:可以对任意序列进行排序,排序原理与sort()基本一致,但是sorted()函数将排序后的结果作为一个新的对象返回,不改变被排序的原序列,此点与sort()不同。

  • 以函数为参数,使用sorted()示例
#【sorted()使用示例】
lst = ['aaa', 'b','ccccccc','dddd']
print('排序前:', list(lst))

# 对lst按照字符串长度进行排序
new_lst = sorted(lst, key = len)
print('排序后:', list(lst))
print(new_lst)

# 运行结果
# 排序前: ['aaa', 'b', 'ccccccc', 'dddd']
# 排序后: ['aaa', 'b', 'ccccccc', 'dddd']
# ['b', 'aaa', 'dddd', 'ccccccc']

3.8.3 闭包

将函数作为返回值的这种高阶函数,也称为闭包。

  • 因为函数内部可以访问函数外部的参数,但是函数外部(即在全局作用域)不能访问函数内部的参数,通过闭包可以创建并访问只有当前函数才能访问的变量。
  • 因为返回值是一个在函数内部定义的函数,所以通过返回值函数可以访问函数内部的函数。

【闭包示例】

#【闭包示例】
def fn():
    a = "I'm from inside"
    def inner():
        print ('test:', a)
    return inner
r = fn()
# r是调用fn后返回的函数inner。
# 由于inner这个函数是在函数内部定义的,不是全局函数,
# 因此通过inner函数总可以访问到fn函数内部定义的变量
r()
# 运行结果
# test: I'm from inside
  • 实际应用中,可以将不希望全局访问的(比如只在特定函数内使用的)且不希望别人修改的变量放在闭包内。

【创建一个求平均值的函数】

#【创建一个求平均值的函数】
def make_averager():  #该函数为求平均函数的外套函数,用于将全局变量num变成函数内部的变量,以防程序其他部位意外访问该变量导致计算错误
    nums = []  #创建一个空列表变量nums,该列表变量在函数内定义,可以有效防止函数外部意外访问或者覆盖该变量
    def averager(n):  #该函数为求平均的函数
        nums.append(n)
        return sum(nums)/len(nums)
    return averager
averager = make_averager()  #将内部函数averager赋值给变量averager.
print(averager(10))
# 运行结果
# 10.0
print(averager(20))
# 运行结果
# 15.0
print(averager(30))
# 运行结果
# 20.0

print (nums) #尝试调用函数内部的nums,返回错误“无法找到定义的nums变量”,说明外部是无法调用修改函数内部的变量
# 运行结果
# NameError: name 'nums' is not defined

nums = []  #外部重新定义nums,并不影响函数内部的nums。
# 但是如果去掉外层函数make_averager(),则此处定义nums会覆盖之前定义的nums)

print(averager(30))
# 运行结果
# 22.5

3.9 匿名函数 (lambda函数表达式)

lambda表达式:函数创建的一种简化方法,主要用于创建一些简单的函数。

  • 语法:lambda 参数列表:返回值
  • 一般作为函数的参数使用,简单便捷

【lambda函数表达式示例】

#【lambd函数表达式与def创建函数功能一样,示例】
def sumsum(a,b):
    return a+b

lambda a,b : a+b   
# 运行结果
# <function __main__.<lambda>(a, b)>

print (sumsum(1,3))
# 运行结果
# 4

x = lambda a,b :a+b
print (x(1,3))
# 运行结果
# 4

【lambda表达式的调用】

a = 1
b = 3

# 将lambda表达式赋值给一个变量(一般不这么用)
x = lambda a,b :a+b

print (x)
# 运行结果 
# <function <lambda> at 0x0000028A35917EE0>
# 因为x为函数, 打印时没有调用函数(函数x后面没有括号,没有传递参数),所以打印的是函数本身信息。
# 可以看到,打印出的函数信息里,该函数没有函数名,函数名的地方显示的是<lambda>, 所以也称为匿名函数

print (x(a,b))
# 运行结果
# 4

# 如果想直接调用lambd表达式,可以在表达式后面加括号并传入参数(一般也不这么用)
# 注意,需要先将整个lambda表达式用括号包围起来,然后再在末尾加括号和参数,不能直接加括号,否则末尾的括号会直接作用在最后一个元素b上。
lambda a,b :a+b(2,10)
# 运行结果
# <function __main__.<lambda>(a, b)>
(lambda a,b :a+b)(2,10)
# 运行结果
# 12

lambda函数式可用于作为一次性使用的函数参数,实现函数的定制化,但lambda只能用于写简单的函数,写不了for循环、while循环等

#【用lambda函数式作为函数的参数】
lst = [1,2,3,4,5,6,7,8,9]
r = filter(lambda i: i%2==0, lst)
#上式等同于之前高阶函数中的filter()函数示例,即创建了一个可从指定列表lst中获取偶数的函数。

print(list(r))
# 运行结果
# [2,4,6,8]

lambda函数式还可以和map()函数组合使用

map()函数:可对可迭代对象中的所有元素做指定的操作,然后将这些元素添加到新的对象中。

  • 例如:将一个序列中的所有元素都加1。

【map()和lambda表达式示例】

#【map函数与lambda表达式的结合】
#现想将特定列表lst中的所有元素都乘以2
lst = [1,2,3,4,5]
m = map(lambda i : i*2, lst)
print(list(m))
# 运行结果
# [2,4,6,8,10]

3.10 装饰器

装饰器,用于在不修改原函数的基础上,对原函数功能进行扩展。通过这种方式扩展函数功能,可用于避免以下问题:

  • 如果想要扩展功能统一,但需要修改的函数过多,逐一修改函数会比较麻烦。
  • 不方便后期维护
  • 违反开闭原则(OCP):程序开发中的开闭原则,要求开放对函数的扩展,关闭对函数的修改

在定义函数时,通过@装饰器,来使用指定的装饰器,装饰当前函数(扩展当前函数功能)。

一个函数可以指定多个装饰器,按照从内向外的顺序被装饰

【装饰器使用实例】

#【装饰器使用实例】
# 第一个装饰器
# 将需要扩展的函数作为一个参数传入装饰器函数,返回一个扩展后的新函数
def begin_end(old):
    '''
    这是一个用于扩展函数功能的装饰器。该装饰器可以在执行其他函数前先打印“开始执行”,在函数执行结束后戴莹“结束执行”。
    参数:old 要扩展的函数
    '''
    #创建一个新函数。
    #由于要扩展的函数可能需要传递参数,但传递参数的个数和类型未知,因此在创建函数的时候需要对参数进行打包,使用不定长位置参数和不定长关键词参数囊括所有可能出现的参数。
    def new_function(*args, **kwargs): 
        print("开始执行...")
        #调用被扩展的函数
        result = old(*args, **kwargs)  #对传递进来的参数进行解包
        print("结束执行...")
        #返回函数的执行结果
        return result
    #返回扩展后的新函数    
    return new_function

#第二个装饰器
def fn3(old):
   def new_funtion(*args, *kwargs):
       print('fn3装饰器开始执行')
       result = old(*args, **kwargs)     
       print('fn3装饰器结束执行')
       return result
   return new_function

x = lambda a,b: a+b
f = begin_end(x)  #f是原函数x扩展后得到的新函数
r = f(1,3)
print(r)
# 运行结果
# 开始执行...
# 结束执行...
# 4

@begin_end  #调用装饰器,装饰say_hello函数
def say_hello(): 
    print('hello')
say_hello()  #使用装饰器后,原函数say_hello本体直接被扩展。
# 运行结果
# 开始执行...
# hello
# 结束执行...

@begin_end
@fn3
def say_bye():
    print('byebe')
say_bye()
# 运行结果
# 开始执行...
# fn3装饰器开始执行
# byebye
# fn3装饰器结束执行
# 结束执行...
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值