Python面向对象
Python面向对象
概念
1 | class Box(): |
面向对象的思想主要是以对象为主,将一个问题抽象出具体的对象,并且将抽象出来的对象和对象的属性和方法封装成一个类。
类与对象
定义
类是多个类似事物组成的群体的统称
对象,又称为实例,由类创造的一个具体可操作结构
在python
中,一切都是对象,所以之前已经接触过非常多的对象了,比如各种数据类型,使用type()
函数,可以输出对象的类(数据类型)
1 | print(type(1220)) # 输出<class 'int'> 说明1220这个对象,是int这个类的对象,由int类创造 |
创建类和对象
1 | class Candle(object) : |
创建一个类的语法相对简单,一般类中有多个函数,把类中的函数称为 对象的方法
实例名=类名()
将类赋值一个变量,这个变量就是类的对象(实例)
类的属性
1 | class Person (object): |
直接写在类里面的变量被称为:类的属性,在代码中,每个通过Person
类生成的对象,都有love
的值,为'money'
类的属性可以被直接修改,比如Person.love='light'
,之后创建的类的love属性就变成了light
对象属性
1 | class Person (object): |
__init__(self, ...)
是一个特殊方法,在创建对象时进行初始化操作,它会自动执行,他的第一个参数永远都是self
,代表实例本身。
__init__
其中的name,gender
规定了对象的属性,即创建的对象必须传入这两个属性,比如candle=Person("candle","man")
,我们就创建了一个name
为candle
,gender
为man
的对象
而之所以需要这个self
参数,是因为必须先有实例,才能对实例的属性进行修改,也就是说,必须有self
指代类中的对象,才能给通过self.name=name
给对象赋予它传入的名字!
self
在类中表示的是对象本身。在类的内部,通过这个参数访问自己内部的属性和方法
类中方法
上文提到过,类中的函数称为方法,而类中的方法主要分为以下3种
实例方法
是最普遍的方法
1 | class Person (object): |
info
方法就是普通的实例方法,self
指代实例,对象在调用该方法的时候,会自动把对象本身传入进去,这种方式称为 隐式传输
candle.info()
会输出my name is candle
,括号里并不需要传入什么参数
静态方法
使用关键字@staticmethod
创建
1 | class Person (object): |
可以发现,静态方法本质上,就是一个只能由对象使用的函数罢了!
而静态方法和函数的唯一区别就是,静态方法的作用空间在类中,而函数的作用空间在整个程序中
所以,在编程中,基本不会使用到静态方法
类方法
在python
中,一切皆是对象,虽然对象是由类创建的,但是类本身也是一个对象
原则上,类方法是将类本身作为对象进行操作的方法,使用@classmethod
这一关键字
而类方法的作用是,让类本身可以记录信息,换言之,类方法可以修改类属性
比如我想知道类被调用了几次,而类本身作为一个模版,是不会记录数据的,所以此时需要类方法
1 | class Man: |
类方法id_number
的作用是让类属性id+1
,然后返回类属性,赋予给对象属性,从而每次创建对象时,都实现了类属性id+1
动态绑定属性
在已经创建对象之后,可以给对象添加类中不存在的属性,以及使用类中不存在的方法
1 | class Person (object): |
需要注意的是,动态绑定的属性or函数,仅绑定的对象能用,类创建的其他对象是无法使用的
特性
封装
封装:把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口。
实际上,通过类这一结构,我们可以通过创建对象就实现类中的方法,而并不需要关注类中的具体细节
私有变量
私有变量是一种在Python
中实现封装的方式,它可以保护类中的数据不被外部访问或修改。私有变量的命名规则是以两个下划线开头,但不以两个下划线结尾,例如__name
1 | class Person (object): |
私有变量的用途不是为了让外部访问,而是为了让类的内部使用。私有变量可以存储类的一些重要的信息,比如状态、属性、配置等,这些信息只有类自己知道,不需要让外部知道。
继承
继承:子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。
继承的作用是实现代码的可复用性,提供工作效率
实现形式是:class name (father1,father2):
,在创建类的时候,在后面括号中添加父类的名字,python
是支持多继承的
如果一个子类没有继承任何类,会默认继承object
类
1 | class Animal(object): # 定义一个动物类,继承自object类 |
在定义子类的属性的时候,必须在构造函数中调用父类的构造函数(__init__
)
注意这里使用了super()
方法来创建了一个父类的对象,以调用父类的构造函数
super
本身其实就是一个类,super()
其实就是这个类的实例化对象,它需要接收两个参数 super(class, obj)
,它返回的是obj
的继承顺序中class
类的父类(比如C继承了A,B,则C的继承顺序就是C—>A—>B,super(A,c)
指的就是B)
实际上在简单的继承关系中,可以不写super()
的内置参数,默认指的是当前对象的父类
object类
object
类是所有类的父类,因此所有类都有object
类的属性和方法
object
类有一个__str__()
方法,用于返回一个对于“对象的描述”,默认是输出对象的内存地址,但是我们可以重写__str__()
方法,使之返回我们想要的信息
1 | class Person (object): |
多态
Python
中的多态是一种面向对象编程的特性,它指的是不同类型的对象可以对同一个方法或函数产生不同的响应。多态的好处是可以提高代码的灵活性和可扩展性,也可以实现不同对象的自定义行为。
Python
中的多态有两种形式:
- 鸭子类型(duck typing):这种形式不依赖于继承关系,而是依赖于对象的属性和方法。只要对象具有相同的属性或方法,就可以被视为同一类型,而不管它们实际上属于什么类。
- 继承和重写(inheritance and overriding):这种形式是基于继承关系的,子类可以继承父类的属性和方法,并且可以重写父类的方法,以实现自己的功能。这样,当调用父类的方法时,会根据对象的实际类型,执行相应的子类的方法。这种形式也叫做多态性
1 | # 定义一个动物类,有一个叫的方法 |
尽管cat
和dog
完全不是一个类型,但是函数func()
却可以执行他们的call()
方法,这就是动态语言的优势
特殊
特殊属性
__dict__
属性可以获得类对象或实例对象所绑定的所有属性和方法的字典
__class__
属性可以返回当前对象是哪个类的实例
1 | class Person(): |
特殊方法
__new__(cls, *args, **kwargs)
之前在上文讲过,__init__(self, ...)
是一个特殊方法,在创建对象时进行初始化操作
那么,随之而来有一个问题,既然__init__
是方法,会自动执行,那么是什么对象在创建新对象的时候调用了__init__
方法呢?此时就需要用到__new__(cls, *args, **kwargs)
这个特殊方法,
只要是面向对象的编程语言,类的实例化都一定包含两个步骤:
- 在内存中创建对象,即开辟一块内存空间来存放类的实例 ( new )
- 初始化对象,即给实例的属性赋予初始值,例如全部填 0 ( init )
先分析一下new
函数的内部
1 | class Person(object): |
这里的super(Person, cls)
的意思就是调用person
类的父类,以调用new
方法,在上文解释过super
方法的用法,在简单的继承关系可以不写
可以看到,__new__
方法的参数是cls, *args, **kwargs
,这表示它可以接受任意数量和类型的参数,但是它并不会使用这些参数,而是直接传递给父类的__new__
方法。这样可以保证Person
类的__new__
方法和object
类的__new__
方法的兼容性,也可以让Person
类的__init__
方法自由地定义和使用参数
在 python
中,第一步由 new 函数负责,第二步由 init 函数负责。
1 | class Person(object): |
通过运行这段代码,我们可以看到,new
方法的调用是发生在init
之前的。其实当 你实例化一个类的时候,具体的执行逻辑是这样的:
p = Person(name, age)
- 首先执行使用
name
和age
参数来执行Person
类的new
方法,这个new
方法会 返回Person
类的一个实例(通常情况下是使用super(Persion, cls).__new__(cls)
这样的方式), - 然后利用这个实例来调用类的
__init__
方法,上一步里面__new__
产生的实例也就是__init__
里面的的self
所以,__init__
和 __new__
最主要的区别在于:
__init__
通常用于初始化一个新实例,控制这个初始化的过程,比如添加一些属性, 做一些额外的操作,发生在类实例被创建完以后。它是实例级别的方法。__new__
通常用于控制生成一个新实例的过程。它是类级别的方法。
拷贝
浅拷贝
可以使用copy
模块的copy
函数来创建一个新的对象,该对象是原始对象的浅层副本。下面是一个浅拷贝的例子:
1 | import copy |
运行上述代码,输出如下:
1 | 原始列表: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] |
copy.copy(x):
返回对象x
的浅拷贝。这个函数创建一个新的对象,该对象是原始对象的浅层副本。新对象的一些属性会直接引用原始对象的属性,而不是创建新的属性。
因此,如果原始对象的属性是可变的,则新对象的属性也将是可变的,并且对新对象的更改将反映在原始对象中。如果原始对象的属性是不可变的,则新对象的属性也将是不可变的,因此对新对象的更改不会影响原始对象
深拷贝
深拷贝是一种对象复制的方式,它会创建一个新的对象,该对象是原始对象的递归副本。深拷贝复制了原始对象的所有属性,包括所有嵌套对象和子对象,而不是仅仅复制引用。这意味着,如果我们修改新对象的属性,原始对象的属性不会受到影响
1 | import copy |
运行上述代码,输出如下:
1 | 原始列表: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] |
可以看到,只改变了新列表的属性,原来列表的属性没有被修改
模块
在Python
中,模块就是一个Python
文件,以.py
结尾,包含了Python
对象定义和Python
语句。
导入
可以使用import module_name
的形式在模块A中导入模块B,从而在模块A中使用模块B里面的函数,类以及方法等
具体的方法是:
在定义好模块之后,我们可以使用import
语句来引入模块。例如,要引用名为math
的模块,可以在文件的顶部使用import math
来引入。在调用math
模块中的函数时,需要使用模块名.函数名的语法。
如果一个模块在当前的搜索路径中,当解释器遇到import
语句时,该模块将被导入。搜索路径是解释器在搜索模块时首先查找的目录列表。如果要导入模块math.py
,需要将命令放在脚本的顶部
1 | import math |
主程序
在Python
中,如果一个模块不是被导入到其他程序中执行,那么它可能在解释器的顶级模块中执行,这种情况下,该模块被视为主程序。
在每个模块的定义中都包括一个记录模块名称的变量__name__
,程序可以检查该变量,以确定它们在哪个模块中执行。如果一个模块是主程序,那么它的__name__
变量的值为__main__
。因此,我们可以使用以下代码来检查一个模块是否是主程序:
1 | if __name__ == "__main__": |
这个代码块只有在该模块作为主程序运行时才会执行。如果该模块被导入到其他程序中执行,那么这个代码块将不会执行
常用模块
Python
中有一些常用的内置模块,方便导入使用
以下是一些常用的Python
标准库模块:
- os模块:提供了与操作系统交互的函数,例如创建、移动和删除文件和目录,以及访问环境变量等
- sys模块:提供了与Python解释器和系统相关的功能,例如解释器的版本和路径,以及与stdin、stdout和stderr相关的信息
- time模块:提供了处理时间的函数,例如获取当前时间、格式化日期和时间、计时等
- random模块:提供了生成随机数的函数,例如生成随机整数、浮点数、序列等
- math模块:提供了数学函数,例如三角函数、对数函数、指数函数、常数等
还有一些很好用的第三方模块,可以使用 pip install....
的方式安装到本地上
包
包是一组Python
模块的集合,它们被组织在一个目录层次结构中。一个包是一个包含一个或多个模块的目录,其中还包含一个名为__init__.py
的文件,用于区分包和仅包含一堆Python
脚本的目录。
包可以嵌套到任意深度,只要相应的目录包含自己的__init__.py
文件即可。模块和包的区别似乎仅在文件系统级别上存在。当您导入一个模块或包时,Python
创建的相应对象始终是模块类型的对象。导入一个包时,只有__init__.py
文件中的变量/函数/类是直接可见的,而不是子包或模块。
在其他Python
文件中使用这个包时,您可以使用import
语句来导入包中的模块。例如,要使用mypackage
包中的module1.py
模块中的函数,可以使用以下语法:
1 | from mypackage import module1 |