Python学习第三周总结:奇妙的函数与烧脑的面向对象编程
先言
在学习函数之前,我们可以先引出函数为什么会出现,出现的意义的什么,能给程序员带来什么便利。
那么,先来一个题目:输入M,N计算C(M, N)。这是高中就学过的阶乘知识,下面先给出公式:
C
M
N
=
M
!
N
!
(
M
?
N
)
!
C_M^N = \frac {M!} {N!(M-N)!}
CMN?=N!(M?N)!M!? 例,一个5的阶乘:5! = 5 * 4 * 3 * 2 * 1
因此,要求M,N的阶乘就需要利用循环做累成得到阶乘数
"""
play01 - 输入M,N计算C(M, N)
Author:悾格
Date: 2021/8/7
"""
m = int(input('m = '))
n = int(input('n = '))
fm = 1
for i in range(1, m + 1):
fm *= i
fn = 1
for i in range(1, n + 1):
fn *= i
fm_n = 1
for i in range(1, m - n + 1):
fm_n *= i
print(fm / (fn * fm_n))
这就是我们以我们现有的知识得到的Python代码
现在就让我们来看一下代码中的小问题:
3个for 循环都只是修改了一个变量,其余的步骤都没有多大的变化,所以就造成了不必要的代码重复,增加代码的空间。世界级的编程大师Martin Fowler先生曾经说过:“代码有很多种坏味道,重复是最坏的一种!”。
函数
所以,在这里就出现了函数,能够让代码减少重复次数,当我们需要的时候,能够直接使用。
-
函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段。 -
函数能提高应用的模块性,和代码的重复利用率。 -
在这之前,我们已经遇到过函数了,比如print(),这种属于Python内置函数,是自带的。但是你也可以自己创建自己需要的函数,这种属于用户自定义函数。 -
对于Python来说,它有一个非常好的地方,那就是拥有一个非常强大的社区和丰富的第三方库。 -
就像是,你遇到一个比较大的困难,到社区里看看就可以找到问题,就目前而言,你遇到的所有问题,社区里的人都遇到过并给出了解决方法
函数的定义
Python中的函数与数学中的函数相似,将一个代码块存放在地方,给这个地方一个函数名,调用函数返回的结果称为因变量,自变量可以有,也可以没有,也称为参数。
- Python用
def 关键字定义函数 - 函数名,命名规则与变量的命名规则是一致的
- 函数名后接
( ) ,在括号中,可以传入参数,用于定义参数 return [表达式] 结束函数,选择性返回一个值给调用方,不带表达式的return 返回None 值。
def add(a, b):
return a + b
def 函数名(参数列表):
? 函数体
那么现在我们通过函数对C(M, N)代码进行重构。所谓重构,是在不影响代码执行结果的前提下对代码的结构进行调整。
"""
play01 - 用函数实现:输入M,N计算C(M, N)
Author:悾格
Date: 2021/8/7
"""
def fac(num):
"""求阶乘"""
result = 1
for i in range(1, num + 1):
result *= i
return result
m = int(input('m = '))
n = int(input('n = '))
print(fac(m) // fac(n) // fac(m - n))
函数的基本结构完成以后,你可以通过另一个函数调用执行,也可以直接从 Python 命令提示符执行。
函数的参数
默认参数
在定义函数时,函数可以没有参数(自变量),但是圆括号() 是必须有的。当函数中没有参数值时,返回的值是根据函数中某个值的变化而变化的,调用时无法改变其变量。函数也可以拥有默认值,调用不加参数时,会输出默认值,也可以加入参数。
"""
play02 - 默认参数
Author:悾格
Date: 2021/8/7
"""
def add(a=0, b=0):
"""两数相加"""
return a + b
print(add())
print(add(1))
print(add(1, 2))
print(add(b=5, a=10))
注意:带默认值的参数必须放在不带默认值的参数之后,否则将产生SyntaxError 错误,错误消息是:non-default argument follows default argument ,翻译成中文的意思是“没有默认值的参数放在了带默认值的参数后面”。
可变参数
可变参数,在调用函数时,可以向函数传入0个或任意多个参数。
用星号表达式表示该参数可以接收0个或任意多个参数
"""
play03 - 可变参数
Author:悾格
Date: 2021/8/7
"""
def add(*args):
total = 0
for val in args:
total += val
return total
print(add())
print(add(1))
print(add(1, 2))
print(add(1, 2, 3))
print(add(1, 3, 5, 7))
标准库中的模块和函数
python标准库中提供了大量的模块和函数来简化我们的开发工作:
random 模块为我们提供了生成随机数和进行随机抽样的函数;time 模块提供了时间操作的相关函数math 模块中也包含了大部分数学运算操作的相关函数,例如上面的求阶乘
但是如果要使用上面的标准库函数的话,需要使用import 导入到模块才能使用,但是在Python标准库中还有一类被称为内置函数的函数可以不用通过import 直接使用。
"""
play04 - 内置函数
Author:悾格
Date: 2021/8/7
"""
print(abs(-2.5))
print(chr(8364))
print(ord(''))
print(bin(123))
print(oct(123))
print(hex(123))
x=input('请输入:x=')
print(len([1, 2, 3]))
print(max(12, 95, 37))
print(min(12, 95, 37))
print(pow(2, 8))
print(x)
print(range(1, 100))
print(round(1.23456, 2))
print(sum(range(1, 101)))
print(type(10))
print(type('a'))
关键字参数
下面是判断传入的三边能否构成三角形的函数,在调用函数传入参数时,我们可以指定参数名,也可以不指定参数名。
def is_triangle(a, b, c):
return a + b > c and a + c > b and b + c > a
print(is_triangle(1, 2, 3))
print(is_triangle(a=1, b=2, c=3))
print(is_triangle(c=3, a=1, b=2))
在没有特殊处理的情况下,函数的参数都是位置参数,对号入座即可
命名关键字参数:调用函数是必须以参数名=参数值 传入参数,在函数的参数列表中,写在* 之后的参数。
def is_triangle(a, *, b, c):
return a + b > c and a + c > b and b + c > a
print(is_triangle(a=1, b=2, c=3))
注意:上面的is_triangle 函数,参数列表中的* 是一个分隔符,* 前面的参数都是位置参数,而* 后面的参数就是命名关键字参数。
提示:不带参数名的参数(位置参数)必须出现在带参数名的参数(关键字参数)之前,
高阶函数
高阶函数:函数的参数和返回值可以是任意类型的对象,这就意味着函数本身也可以作为函数的参数或返回值。
Python中的函数是一等函数(一等公民):
- 函数可以作为函数的参数
- 函数可以作为函数的返回值
- 函数可以赋值给变量
如果把函数作为函数的参数或者返回值,这种玩法通常惩治为高阶函数。 通常使用高阶函数可以实现对原有函数的解耦合操作
"""
play06 - 高阶函数
Author:悾格
Date: 2021/8/7
"""
def calc(*args, init_value, op, **kwargs):
result = init_value
for arg in args:
result = op(result, arg)
for value in kwargs.values():
result = op(result, value)
return result
def add(x, y):
return x + y
def mul(x, y):
return x * y
print(calc(1, 2, 3, init_value=0, op=add, x=4, y=5))
print(calc(1, 2, x=3, y=4, z=5, init_value=1, op=mul))
通过对高阶函数的运用,calc 函数不再和加法运算耦合,所以灵活性和通用性会变强。
需要注意的是,将函数作为参数和调用函数是有显著的区别的,调用函数需要在函数名后面跟上圆括号,而把函数作为参数时只需要函数名即可。
Lambda函数(匿名函数)
在使用高阶函数时,我们也可以不需要重新再定义一个新的函数,而是可以使用Lambda 函数直接使用。
numbers1 = [35, 12, 8, 99, 60, 52]
numbers2 = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, numbers1)))
print(numbers2)
那么将Lambda 函数加入calc 函数,在函数中也可以给op 设为默认函数,在传入函数时,也可以通过Lamdba 函数更改为所需要的函数
def calc(*args, init_value=0, op=lambda x, y: x + y, **kwargs):
result = init_value
for arg in args:
result = op(result, arg)
for value in kwargs.values():
result = op(result, value)
return result
print(calc(1, 2, 3, x=4, y=5))
print(calc(1, 2, 3, x=4, y=5, init_value=1, op=lambda x, y: x * y))
装饰器
装饰器是Python中用一个函数装饰另外一个函数或类并为其提供额外功能的语法现象。
装饰器本身是一个函数,它的参数是被装饰的函数或类,它的返回值是一个带有装饰功能的函数。
装饰器函数本身也可以参数化,简单的说就是通过我们的装饰器也是可以通过调用者传入的参数来定制的。
递归调用
函数自己调用自己称为递归调用。
使用递归调用的方式来写一个求阶乘的函数:
def fac(num):
if num in (0, 1):
return 1
return num * fac(num - 1)
上面的代码中,fac 函数中又调用了fac 函数,这就是所谓的递归调用。
注意,函数调用会通过内存中称为“栈”(stack)的数据结构来保存当前代码的执行现场,函数调用结束后会通过这个栈结构恢复之前的执行现场。
栈(FILO )是一种先进后出的数据结构,这也就意味着最早入栈的函数最后才会返回,而最后入栈的函数会最先返回。
每进入一个函数调用,栈就会增加一层栈帧(stack frame),栈帧就是我们刚才提到的保存当前代码执行现场的结构;每当函数调用结束后,栈就会减少一层栈帧。
通常,内存中的栈空间很小,因此递归调用的次数如果太多,会导致栈溢出(stack overflow),所以递归调用一定要确保能够快速收敛。
面向对象编程
面向对象编程是一种非常流行的编程范式(programming paradigm),所谓编程范式就是程序设计的方法学,也就是程序员对程序的认知和理解。
把相对独立且经常重复使用的代码放置到函数中。
在面向对象编程的世界里,程序中的数据和操作数据的函数是一个逻辑上的整体,我们称之为对象,对象可以接收消息,解决问题的方法就是创建对象并向对象发出各种各样的消息;通过消息传递,程序中的多个对象可以协同工作,这样就能构造出复杂的系统并解决现实中的问题。
说明: 今天我们使用的很多高级程序设计语言都支持面向对象编程,但是面向对象编程也不是解决软件开发中所有问题的“银弹”,或者说在软件开发这个行业目前还找不到这种所谓的“银弹”。
类和对象
如果要用一句话来概括面向对象编程,我认为下面的说法是相当精准的。
面向对象编程:把一组数据和处理数据的方法组成对象,把行为相同的对象归纳为类,通过封装隐藏对象的内部细节,通过继承实现类的特化和泛化,通过多态实现基于对象类型的动态分派。
圈出几个关键词:
- 对象(object)
- 类(class)
- 封装(encapsulation)
- 继承(inheritance)
- 多态(polymorphism)
在面向对象编程中,类是一个抽象的概念,对象是一个具体的概念。我们把同一类对象的共同特征抽取出来就是一个类,比如我们经常说的人类,这是一个抽象概念,而我们每个人就是人类的这个抽象概念下的具体的实实在在的存在,也就是一个对象。简而言之,类是对象的蓝图和模板,对象是类的实例。
在面向对象编程的世界中,一切皆为对象,对象都有属性和行为,每个对象都是独一无二的,而且对象一定属于某个类。对象的属性是对象的静态特征,对象的行为是对象的动态特征。按照上面的说法,如果我们把拥有共同特征的对象的属性和行为都抽取出来,就可以定义出一个类。
指令式编程 —> 面向过程(函数)编程 —> 程序比较简单的时候没有任何毛病
编程范式(程序设计的方法论):面向对象编程 / 函数式编程
对象:对象是可以接收消息的实体。面向对象编程就是通过给对象发消息达到解决问题的目标
对象 = 数据 + 函数(方法) —> 对象将数据和操作数据的函数从逻辑上变成了一个整体。
- 一切皆为对象
- 对象都有属性和行为
- 每个对象都是独一无二的
- 对象一定属于某个类
类:将有共同的特征(静态特征和动态特征)的对象的共同特征抽取出来后得到的一个抽线概念
简单的说,类是对象的蓝图(模板), 有了类才能够创建出这种类型的对象
面向对象编程:
- 定义类 —> 类的命名使用驼峰命名法(每个单词首字母大写)
- 数据抽象:找到和对象相关的静态特征(属性) —> 找名词
- 行为抽象:找到和对象相关的动态特征(方法) —> 找动词
- 造对象
- 发消息
类是抽象的概念,对象是具体的概念
定义类
使用class 关键字加上类名来定义类,类名通常使用驼峰命名法,通过缩进我们可以确定类的代码块,就如同定义函数那样。在类中写的函数一般称之为方法,方法是对象的行为,也就是对象可以接收的消息。
方法的第一个参数通常都是self ,它代表了接收这个消息的对象本身。
初始化方法
如果要给学生对象定义属性,我们可以修改Student 类,为其添加一个名为__init__ 的方法。
在我们调用Student 类的构造器创建对象时,首先会在内存中获得保存学生对象所需的内存空间,然后通过自动执行__init__ 方法,完成对内存的初始化操作,也就是把数据放到内存空间中。
class Student:
def __init__(self, name, age):
"""初始化方法"""
self.name = name
self.age = age
def eat(self):
print(f'{self.name}正在吃饭')
def study(self, course_name):
print(f' {self.name}正在学习{course_name}')
def play(self, game_name):
print(f'{self.name}正在玩{game_name}')
def watch_tv(self):
if self.age < 18:
print(f'{self.name}只能看《熊出没》')
else:
print(f'{self.name}正在观看动画片')
创建和使用对象
当我们创建好类过后,就可以使用构造器语法来创建对象:
stu1 = Student('倥格', '16')
stu2 = Student('悾格', '21')
现在给对象发送消息,即调用对象的方法。
stu1.study('Python')
stu1.age = 20
stu1.watch_tv()
打印对象
上面我们通过__init__ 方法在创建对象时为对象绑定了属性并赋予了初始值。在Python中,以两个下划线__ (读作dunder )开头和结尾的方法通常都是有特殊用途和意义的方法,我们一般称之为魔术方法或魔法方法。
如果我们在打印对象的时候不希望看到对象的地址而是看到我们自定义的信息,可以通过在类中放置__repr__ 魔术方法来做到,该方法返回的字符串就是用print 函数打印对象的时候会显示的内容。
魔术方法
魔术方法(魔法方法) —> 有特殊用途和意义的方法
-
__init__ —> 初始化方法,在调用构造器语法创建对象的时候会被自动调用 -
__str__ —> 获得对象的字符串表示,在调用print函数输出对象时会自动调用 -
__repr__ —> 获得对象的字符串表示,把对象放到容器中调用print输出时会自动调用 -
__lt__ —> 在使用 < 运算符比较两个对象大小会自动调用
如果一个变量的取值只有有限个选项,可以考虑使用枚举类型。 Python中没有定义枚举类型的语法,但是可以通过继承Enum 类来实现枚举类型。 结论1:枚举类型是定义符号常量的最佳选择!!! 结论2:符号常量(有意义的名字)总是优于字面常量
面向对象的支柱
面向对象的四大支柱:
- 抽象(abstraction):提取共性( 定义类就是一个抽象过程,需要做数据抽象和行为抽象)
- 封装(encapsulation):把数据和操作数据的函数从逻辑上组装成一个整体(对象)
- 继承(inheritance)
- 多态(polymorphism)
封装:隐藏一切可以隐藏的实现细节,只向外界暴露简单的调用接口。
可见性和属性装饰器
**访问可见性:**在很多面向对象编程语言中,对象的属性通常会被设置为私有(private)或受保护(protected)的成员,简单的说就是不允许直接访问这些属性;对象的方法通常都是公开的(public),因为公开的方法是对象能够接受的消息,也是对象暴露给外界的调用接口
可以用__name 表示一个私有属性,_name 表示一个受保护属性,代码如下所示。
Python并没有从语法上严格保证私有属性的私密性,它只是给私有的属性和方法换了一个名字来阻挠对它们的访问,事实上如果你知道更换名字的规则仍然可以访问到它们,我们可以对上面的代码稍作修改就可以访问到私有的。
Python中可以通过property 装饰器为“私有”属性提供读取和修改的方法,装饰器通常会放在类、函数或方法的声明之前,通过一个@ 符号表示将装饰器应用于类、函数或方法。
静态方法和类方法
我们在类里面写的函数,通常称之为方法,它们基本上都是发给对象的消息。 但是有的时候,我们的消息并不想发给对象,而是希望发给这个类(类本身也是一个对象), 这个时候,我们可以使用静态方法和类方法。
静态方法 - 发给类的消息 —> @static method —> 装饰器 类方法 - 发给类的消息 — > @class method —> 装饰器 —> 第一个参数是接收消息的类
对象方法、类方法、静态方法都可以通过类名.方法名 的方式来调用,区别在于方法的第一个参数到底是普通对象还是类对象,还是没有接受消息的对象。
继承和多态
面向对象的编程语言支持在已有类的基础上创建新类,从而减少重复代码的编写。提供继承信息的类叫做父类(超类、基类),得到继承信息的类叫做子类(派生类、衍生类)。
提供继承信息的类叫做父类(超类,基类), 得到继承信息的类称为子类(派生类)
继承:对已有的类进行扩展创建出新的类的过程就叫继承 继承是一种is-a关系
- a student is a person.
- a teacher is a person.
- a programmer is a person
子类直接从父类继承公共的属性和行为,再添加自己特有的属性和行为。 所以子类一定是比父类更强大的,任何时候都可以用子类对象去替代父类对象
- is-a关系:继承 —> 从一个类 —> class Student(Person)
- has-a关系:关联 —> 把一个类的对象作为另一个类对象的属性
- a person has an identity card.
- a car has an engine
- (普通)关联
- use-a关系:依赖 —> 一个类的对象作为另一类的方法的参数或返回值
- a person use a vehicle
- 强关联:整体和部分的关联,聚合和合成
Python中的继承允许多重继承,一个类可以有一个或多个父类。 如果不是必须使用多重继承的场景下,请尽量使用单一继承。
总结
- 函数是功能相对独立且会重复使用的代码的封装。
- 在写代码尤其是开发商业项目的时候,一定要有意识的将相对独立且重复出现的功能封装成函数
- Python中的函数可以使用可变参数
*args 和关键字参数**kwargs 来接收任意数量的参数,而且传入参数时可以带上参数名也可以没有参数名,可变参数会被处理成一个元组,而关键字参数会被处理成一个字典。 - 设计函数的时候,一定要注意函数的无副作用性(调用函数不影响调用者)
- 装饰器是Python中的特色语法,可以通过装饰器来增强现有的类或函数.
- 函数的递归调用一定要注意收敛条件和递归公式,找到递归公式才有机会使用递归调用,而收敛条件确定了递归什么时候停下来。
- 函数调用通过内存中的栈空间来保存现场和恢复现场,栈空间通常都很小,所以递归如果不能迅速收敛,很可能会引发栈溢出错误,从而导致程序的崩溃。
- 在面向对象的世界中,一切皆为对象,我们定义的类也是对象,所以类也可以接收消息,对应的方法是类方法或静态方法。
- 通过继承,我们可以从已有的类创建新类,实现对已有类代码的复用。
- 没人用或没有被引用的对象被称为是垃圾对象,python解释器的自动内存管理机制会对其进行空间回收的处理
学习感受
- 随着内容难度的逐渐增加,以我那极其糟糕的接受能力有点接收不了那么多的知识了。
- 上课的时候老师所说的我都能的听懂并当时接收,但是当课后回忆或做案例代码题时,老是想不起来。
- 老师问是否有问题时,大脑里往往觉得在某些地方有问题,就算是现在也有,但是却不知道怎么问,无法将大脑中的问题用语言表达出来。
- 不过这3周里,我还是学到了很多东西,虽然这些都是些基础知识,但是还是很有成就感。
- 希望后面的课程我能够尽量跟上,加油!!!!!!!!!!!!!
|