1 函数
1.1 定义函数
上图中的参数即为自变量,结果为因变量,也就是说输入因变量,返回自变量。
写程序的终极原则:高内聚低耦合---> high cohesion loe coupling
设计函数最为重要的原则:单一职责原则(一个函数只要干好一件事)--->高度内聚
1.2 函数的参数
1.2.1 默认参数
def add(a=0, b=0, c=0): """三个数相加求和""" return a + b + c # 调用add函数,没有传入参数,那么a、b、c都使用默认值0 print(add()) # 0 # 调用add函数,传入三个参数,分别赋值给a、b、c三个变量 print(add(1, 2, 3)) # 6
若函数中没有return
语句,那么函数默认返回代表空值的None
。 需要注意的是带默认值的参数必须放在不带默认值的参数之后,否则将产生SyntaxError
错误。
1.2.2 可变参数
# 用星号表达式来表示args可以接收0个或任意多个参数 def add(*args): total = 0 # 可变参数可以放在for循环中取出每个参数的值 for val in args: total += val return total # 在调用add函数时可以传入0个或任意多个参数 print(add()) # 0 print(add(1)) # 1 print(add(1, 3, 5, 7 )) # 16
1.3 全局变量和局部变量
Python程序中搜索一个变量是按照 LEGB 顺序进行搜索的
Local(局部作用域) ---> Embeded(嵌套作用域) ---> Global(全局作用域) ---> Built-in(内置作用域) ---> 如果内置作用域都没有发现会报错:NameError: name ... not defined
global ---> 声明使用全局变量或者定义一个局部变量将其放到全局作用域
nonlocal ---> 声明使用嵌套作用域的变量(不使用局部变量)
x = 100 def foo(): # 如果我不想在函数foo中定义局部变量x,想直接使用全局变量x # global x x = 200 def bar(): # 如果我不想在函数bar中定义局部变量x,想直接使用嵌套作用域中的x # nonlocal x x = 300 print(x) bar() print(x) foo() print(x)
1.4 用模块管理函数 ( 解决命名冲突 )
做工程化项目开发时,如果项目中的代码文件非常多,我们可以使用"包"(package)来管理"模块"(module), 再通过模块来管理函数,包其实就是一个文件夹,而模块就是一个Python文件,通过这种方式就可以很好的解决 大型项目团队开发中经常遇到的命名冲突的问题。 Python中的from、import、as关键字就是专门用来处理包和模块导入的操作的。
方法一:完全限定名(qualified name),通过 import 导入模块,然后再通过 " 模块名.函数名 " 的方式调用函数;
方法二:直接从模块中导入函数 ---> "from 模块 import 函数" ---> 直接通过函数名调用函数
import 导入函数、模块时,可以使用as关键字(alias)进行别名
# module1.py def foo(): print('hello, world!') # module2.py def bar(): print('goodbye, world!')
方法一:
# test.py import module1 import module2 # 用“模块名.函数名”的方式(完全限定名)调用函数, module1.foo() # hello, world! module2.bar() # goodbye, world!
方法二:
# test.py import module1 as f1 import module2 as f2 f1.foo() # hello, world! f2.bar() # goodbye, world!
1.5 关键字参数
在没有特殊处理的情况下,函数的参数都是位置参数 。
# 定义三角形 def is_triangle(a, b, c): print(f'a = {a}, b = {b}, c = {c}') return a + b > c and b + c > a and a + c > b # 按位置对号入座 print(is_triangle(1, 2, 3)) # 调用函数也可按“参数名=参数值”的形式,顺序不要求 print(is_triangle(a=1, b=2, c=3)) print(is_triangle(c=3, a=1, b=2))
定义函数时,写在 * 前面的参数称为位置参数,调用函数传递参数时,只需要对号入座 ;写在 * 后面的参数称为命名关键字参数,调用函数传递参数时,必须要写成"参数名=参数值"的形式。关键字参数一定是在位置参数的后面!!!
在设计函数的时候,函数的参数个数是暂时无法确定时:
*args ---> 可变参数--->可以接收零个或任意多个位置参数 ---> 将所有位置参数打包成一个元组
**kwargs ---> 可以接收零个或任意多个关键字参数 ---> 将所有关键字参数打包为字典
def add(*args, **kwargs): total = 0 for arg in args: if type(arg) in (int, float): total += arg for value in kwargs.values(): if type(value) in (int, float): total += value return total print(add()) # 0 print(add(1, )) # 1 print(add(1, 2, c=3, b=2.5, a=1, d='hello')) # 9.5 print(add(1, '2', 3)) # 4 print(add(1, 2, 'hello', 4)) # 7
1.6 高阶函数
Python中的函数是一等函数(一等公民):
1. 函数可以作为函数的参数
2. 函数可以作为函数的返回值
3. 函数可以赋值给变量
如果把函数作为函数的参数或者返回值,这种玩法通常称之为高阶函数。 通常使用高阶函数可以实现对原有函数的解耦合操作。
def calc(*args, op, init_value=0, **kwargs): # op 定义的运算 total = init_value for arg in args: if type(arg) in (int, float): total = op(total, arg) for value in kwargs.values(): if type(value) in (int, float): total = op(total, value) return total def add(x, y): return x + y def mul(x, y): return x * y print(calc(11, 22, 33, 44, op=add)) # 110 print(calc(11, 22, 33, 44, init_value=1, op=mul)) # 351384
Lambda函数 ---> 没有名字而且一句话就能写完的函数,唯一的表达式就是函数的返回值。下面由Lambda改写后的代码:
def calc(*args, op, init_value=0, **kwargs): total = init_value for arg in args: if type(arg) in (int, float): total = op(total, arg) for value in kwargs.values(): if type(value) in (int, float): total = op(total, value) return total print(calc(11, 22, 33, 44, op=lambda x, y: x + y)) # 110 print(calc(11, 22, 33, 44, init_value=1, op=lambda x, y: x * y)) # 351384
1.7 递归调用
函数如果直接或间接的调用了自身,这种调用称为递归调用。 不管函数是调用别的函数,还是调用自身,一定要做到快速收敛。 在比较有限的调用次数内能够结束,而不是无限制的调用函数。 如果一个通常指递归调用的函数不能够快速收敛,那么就很有可能产生下面的错误 :RecursionError: maximum recursion depth exceeded ----> 最终导致程序的崩溃。
递归函数的两个要点:
1. 递归公式(第n次跟第n-1次的关系)
2. 收敛条件(什么时候停止递归调用)
实例:利用递归求 n!
问题分析:根据阶乘的定义 n! = n * (n - 1) * (n - 2) * ... * 2 * 1 ----> n! = n * (n - 1)!
def fac(num: int) -> int: """递归求阶乘""" if num == 0: return 1 return num * fac(num - 1) if __name__ == '__main__': # 计算思路: # return 5 * fac(4) # return 4 * fac(3) # return 3 * fac(2) # return 2 * fac(1) # return 1 * fac(0) # return 1 print(fac(5))
2 面向对象编程
2.1 相关基本概念
所谓编程范式即程序设计的方法学 ,分为面向对象编程 、函数式编程、指令式编程等。
面向对象编程:把一组数据和处理数据的方法组成对象,把行为相同的对象归纳为类,通过封装隐藏对象的内部细节,通过继承实现类的特化和泛化,通过多态实现基于对象类型的动态分派。
对象:数据 + 函数(方法)---> 对象将数据和操作数据的函数从逻辑上变成了一个整体。
~ 一切皆为对象
~ 对象都有属性和行为
~ 每个对象都是独一无二的
~ 对象一定属于某个类
类:将有共同特征(静态特征和动态特征)的对象的共同特征抽取出来之后得到的一个抽象概念。
面向对象编程的四大支柱:
~ 抽象(abstraction):提取共性(定义类就是一个抽象过程,需要做数据抽象和行为抽象)。
~ 封装(encapsulation):把数据和操作数据的函数从逻辑上组装成一个整体(对象)。
---> 隐藏实现细节,暴露简单的调用接口。
~ 继承(inheritance):扩展已有的类创建新类,实现对已有类的代码复用。
~ 多态(polymorphism):给不同的对象发出同样的消息,不同的对象执行了不同的行为。
---> 方法重写:子类对父类已有的方法,重新给出自己的实现版本
在面向对象编程的世界中,一切皆为对象,对象都有属性和行为,每个对象都是独一无二的,而且对象一定属于某个类。 类是一个抽象的概念,对象是一个具体的概念。比如我们经常说的人类,这是一个抽象概念,而我们每个人就是人类的这个抽象概念下的具体的实实在在的存在,也就是一个对象。简单的说,类是对象的蓝图(模板),有了类才能够创建出这种类型的对象。
2.2 面向对象编程的步骤
1.定义类 ---> 驼峰命名法(每个单词首字母大写)
~ 数据抽象:找到和对象相关的静态特征(属性)--->找名词
~ 行为抽象:找到和对象相关的动态特征(方法)--->找动词
2.创建对象
3.给对象发消息
# 第一步:定义类
class Student: # class为关键字,后跟类名,需驼峰命名法
"""学生"""
# 数据抽象(属性)
# 通过初始化方法即 '__init__' 方法指定属性,同时完成对属性赋初始值的操作
def __init__(self,name,age): # self 它代表了接收这个消息的对象本身
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_av(self):
"""看视频"""
if self.age<18:
print(f'{self.name}未满18岁,只能看《熊出没》')
else:
print(f'{self.name}正在看电影')
# 第二步 创建对象 ---> 使用构造器语法 ---> 格式:类名(...,...)
stu1=Student('王大锤',15)
stu2=Student('骆',41)
# 第三步:给对象发出消息(调用对象的方法),有两种方式
# 方式一:“类.方法”调用,第一个参数是接收消息的对象即stu1,第二个参数是学习的课程名称
# Student.study(stu1,'Python程序设计') # 王大锤正在学习Python程序设计
# 方式二:“对象.方法”调用,点前面的对象就是接收消息的对象,只需要传入第二个参数
stu1.study('Python程序设计') # 王大锤正在学习Python程序设计
stu1.eat() # 王大锤正在吃饭
stu2.play('斗地主') # 骆正在玩斗地主
stu2.watch_av() # 骆正在看电影
2.3 魔术方法
在Python中,以两个下划线 '__' 开头和结尾的方法通常都是有特殊用途和意义的方法,一般称之为魔术方法或魔法方法。
2.3.1 __init__ ---> 初始化方法
__init__ ---> 初始化方法,在调用构造器语法创建对象的时候会被自动调用,如上述例子在定义学生对象属性时所用。
2.3.2 __str__与__repr__
魔术方法
~ __str__ ---> 获得对象的字符串表示,在调用print函数输出对象时会被自动调用
~ __repr__ ---> 获得对象的字符串表示,把对象放到容器中调用print输出时会自动调用
---> representation
若在前述例子,第三步给对象发消息步骤时,直接打印:
print ( stu1 ) # < ex01.Student object at 0x0000029A05524AF0 > 返回的是地址,ex01为文件名
在前述定义类的行为抽象(方法)后面增加如下内容:
def __str__(self): return f'{self.name}:{self.age}'
print ( stu1 ) # 王大锤:15
若是容器类型则把 str 改为 repr :
def __repr__(self): return f'{self.name}:{self.age}'
students = [ stu1,stu2 ]
print ( students ) # [ 王大锤: 16, 骆: 41]
2.3.3 __slots__魔法与动态属性
在Python中,我们可以动态为对象添加属性 。如果要限制一个类的对象只能拥有某些属性,可以在类中使用__slots__魔法属性
class Student: # __slots__ = ('name', 'age') # 注释掉此行代码可增加grade属性,否则只能拥有name,age def __init__(self, name, age): self.name = name self.age = age stu = Student('王大锤', 20) # 为Student对象动态添加grade属性 stu.grade = '五年级'
此外还有 __lt__魔法, ---> 在使用 < 运算符比较两个对象大小时会自动调用。
2.4 静态方法和类方法
我们在类里面写的函数,通常称之为方法,它们基本上都是发给对象的消息。 但是有的时候,我们的消息并不想发给对象,而是希望发给这个类(类本身也是一个对象), 这个时候,我们可以使用静态方法或类方法。
静态方法 - 发给类的消息 ---> @staticmethod ---> 装饰器
类方法 - 发给类的消息 ---> @classmethod ---> 装饰器 ---> 第一个参数(cls)是接收消息的类
实例:定义描述三角形的类,提供计算周长和面积的方法
class Triangle: # 数据抽象(属性): def __init__(self, a, b, c): if not Triangle.is_valid(a, b, c): raise ValueError('无效的边长,无法构成三角形') self.a = a self.b = b self.c = c # 类方法(与下面类静态方法效果等价) # @classmethod # def is_valid(cls,a,b,c): # return a + b > c and b + c > a and a + c > b # 静态方法 @staticmethod def is_valid(a, b, c): return a + b > c and b + c > a and a + c > b def perimeter(self): return self.a + self.b + self.c def area(self): half = self.perimeter() / 2 return (half * (half - self.a) * (half - self.c) * (half - self.b)) ** 0.5 if __name__ == '__main__': try: t = Triangle(3, 4, 5) print(t.perimeter()) print(t.area()) except ValueError as err: print(err)
2.5 继承和多态
继承:对已有的类进行扩展创建出新的类,这个过程就叫继承。 提供继承信息的类叫做父类(超类、基类),得到继承信息的类称为子类(派生类),继承是实现代码复用的一种手段。
两个类之间的关系:
~ is - a关系:继承 ---> 从一个类派生出另一个类
a student is a person. a teacher is a person.
~ has - a关系:关联 ---> 把一个类的对象作为另外一个类的对象的属性
a person has an identity card.
a car has an engine.
~(普通)关联
~ 强关联:整体和部分的关联,聚合和合成 ---> 如:扑克与牌
~ use-a关系:依赖 ---> 一个类的对象作为另外一个类的方法的参数或返回值
a person use a vehicle.
多态:
子类对父类已有的方法,重新给出自己的实现版本,这个过程叫做方法重写(override);在重写方法的过程中,不同的子类可以对父类的同一个方法给出不同的实现版本,那么该方法在运行时就会表现出多态行为。
例如,定义一个学生类和一个老师类,会发现他们有大量的重复代码,而这些重复代码都是老师和学生作为人的公共属性和行为,所以在这种情况下,我们应该先定义人类,再通过继承,从人类派生出老师类和学生类。