Python类和对象基础¶
约 4308 个字 464 行代码 预计阅读时间 20 分钟
Python作用域和命名空间¶
namespace(命名空间)是从名称到对象的映射。现在,大多数命名空间都使用Python字典实现,但除非涉及到性能优化,一般不会关注这方面的事情,而且将来也可能会改变这种方式。在Python中,常见的命名空间例子有:
- 内置名称集合(包括
abs()
函数以及内置异常的名称等) - 一个模块的全局作用域
- 一个函数调用中的局部作用域
- 对象的属性集合(实例的命名空间)
与C++命名空间的作用类似,定义在不同的命名空间中的名称彼此之间互不冲突。例如,两个不同的模块都可以定义maximize()
函数,且不会造成混淆。用户使用函数时必须要在函数名前面加上模块名指定是哪一个模块中的函数
前面已经使用过很多次对象的方法,每一次调用方法都是通过对象.方法名
的方式,实际上,点号之后的名称被称为属性,例如表达式z.real
中,real
是对象z
的属性。同样,使用模块中的名称也被称为属性:表达式modname.funcname
中,modname
是模块对象,funcname
模块的属性
命名空间是在不同时刻创建的,且拥有不同的生命周期。内置名称的命名空间是在Python解释器启动时创建的,永远不会被删除。模块的全局命名空间在读取模块定义时创建,通常,模块的命名空间也会持续到解释器退出。从脚本文件读取或交互式读取的,由解释器顶层调用执行的语句是__main__
模块调用的一部分,也拥有自己的全局命名空间。内置名称实际上也在模块里,即builtins
函数的局部命名空间在函数被调用时被创建,并在函数返回或抛出未在函数内被处理的异常时被删除。对于递归调用来说,每次递归调用都有自己的局部命名空间
在Python中,作用域虽然是被静态确定的,但会被动态使用。执行期间的任何时刻,都会有3或4个“命名空间可直接访问”的嵌套作用域:
- 最内层作用域,包含局部名称,并首先在其中进行搜索(局部作用域)
- 那些外层闭包函数的作用域,包含“非局部、非全局”的名称,从最靠内层的那个作用域开始,逐层向外搜索(嵌套函数的内部作用域)
- 倒数第二层作用域,包含当前模块的全局名称(全局作用域)
- 最外层(最后搜索)的作用域,是内置名称的命名空间(内置作用域)
Note
“可直接访问”的意思是,该文本区域内的名称在被非限定引用(不需要使用.
进行访问)时,查找名称的范围,是包括该命名空间在内的
类和对象¶
创建类和实例化对象¶
在Python中,创建类的格式如下:
Python | |
---|---|
1 2 3 4 5 6 |
|
当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域,因此,所有对局部变量的赋值都是在这个新命名空间之内
例如有下面的类:
Python | |
---|---|
1 2 3 4 5 6 7 |
|
在上面的类中,为了创建类的实例对象,需要使用到魔法方法__init__
(这个方法也被称为类的构造方法),类中所有的成员方法第一个参数表示调用本方法的对象的引用(相当于C++、Java中的this
),一般情况下将其命名为self
,也可以命名为其他名称(但是不推荐)
Note
注意,因为__init__
是构造函数,所以创建对象时会自动调用,如果需要传递实参,对于self
来说不需要传递,传递的实参分别对应于第二个参数开始的后面的形参
在构造方法__init__
中,第二个参数开始都是为成员属性赋值的变量,注意,在Python中,类的成员属性不可以定义在__init__
方法外,在该方法内部,使用self.成员属性 = 形式参数
新增成员属性并使用形参进行赋值,例如上述例子中的name
和age
接着在类内定义了一个方法say_hello
,这个方法属于成员方法,需要类的实例对象去调用
在Python中,创建类的实例对象可以使用下面的方法:
Python | |
---|---|
1 |
|
例如对于前面定义的Person
类来说,创建其对象的方式如下:
Python | |
---|---|
1 |
|
注意,因为在__init__
方法中给了参数,所以在创建对象时就需要传递实参,如果想创建一个空对象,可以不写__init__
方法,或者给__init__
方法中的参数默认值创建一个由默认值实例化的对象,所以可以将类修改为如下:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
有了类对象后,就可以通过该对象去调用其中的方法,例如同样是前面的Person
类:
Python | |
---|---|
1 2 3 4 |
|
类属性和类方法¶
所谓类属性就是属于整个类的变量,其数据供所有当前类对象共享,一般定义在__init__
方法的上面(实际上可以写在类的任何位置),例如下面的代码:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
当需要访问类属性时,需要用类名进行调用,例如需求:每创建一个Person
类对象,number
就加1:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
因为是类属性,所以在类外访问时也需要通过类名来调用而不建议使用对象调用,例如下面的代码:
Python | |
---|---|
1 2 3 4 5 6 |
|
除了有类属性外,还有类方法,与对象方法不同,类方法默认第一个参数传递的是cls
,代表当前类,并且在定义类方法时,需要使用到装饰器@classmethod
,例如下面的代码:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
在上面的代码中,定义了一个get_number
方法,该方法因为使用了装饰器@classmethod
,所以是属于整个类的方法,第一个参数是cls
,因为其代表的是当前类,所以调用类属性时就只需要用该参数调用即可
在使用类方法时,需要使用类进行调用,也可以使用类对象调用,同样不推荐,例如下面的代码:
Python | |
---|---|
1 2 3 4 5 |
|
类的静态方法¶
在Python中,除了有类方法以外,还有静态方法,与类方法非常类似,但是不同的是,其使用装饰器@staticmethod
,并且默认情况下没有参数,例如下面的代码:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
调用类方法时,同样可以使用类或者该类对象调用,但是不推荐使用类对象进行调用,一般情况下静态方法且该类不创建对象时可以使该类成为一个工具类,所以大部分情况下都是使用类名调用静态方法,例如下面的代码:
Python | |
---|---|
1 2 3 4 |
|
封装性¶
面向对象三大特性:封装、继承和多态,Python作为面向对象的编程语言,同样具有该三大特性,下面介绍Python中的封装性
在Python中,虽然不像Java或C++那样严格地支持访问控制修饰符(如public
, private
, protected
),但仍然可以通过一些约定来实现封装性,一般情况下使用下面的习惯:
- 使用单下划线前缀
_
来表示该成员变量或方法是受保护的,但是这种方法依旧可以在类外访问,不会出现运行报错,但是不建议外部直接访问 - 使用双下划线前缀
__
来表示该成员变量是私有的,这种方法会产生名称改写,在类外访问类中的某私有变量就会报错,这使得属性难以从类外部直接访问到
例如下面的代码:
Python | |
---|---|
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 |
|
对于命名前有_
的保护属性来说,访问时依旧不会受到限制,但是不推荐在类外直接访问保护属性,而对于命名前有__
的私有属性来说,访问会报错AttributeError
:
Python | |
---|---|
1 2 3 4 |
|
之所以访问不到__money
是因为Python对私有属性进行了名称改写,通过实例对象的__dict__
属性可以查看到当前类对象所有的成员,如下:
Python | |
---|---|
1 2 3 4 |
|
可以看到除了保护属性以外,私有属性被改写为了_Person__money
,如果访问这个名称就可以访问到类中的私有属性(不推荐),例如下面的代码:
Python | |
---|---|
1 |
|
注意,因为类中__money
被Python进行了名称改写,如果这种情况下直接对__money
进行赋值相当于相当于向类中添加了一个变量__money
,而不是访问其中的__money
属性,这一点和JavaScript是一致的
既然对变量进行了保护或者私有,那么根据封装性,除了私有成员还需要对外提供相应的获取和修改接口,这也就是getter
和setter
,所以上面的类可以修改为:
Python | |
---|---|
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 |
|
注意,有时可以看到一些变量__变量名__
(不但前面有双下划线,后面也有双下划线),这种不是私有变量,而是特殊变量
继承性¶
在Python中,要表示一个类继承自另外一个类,可以在类名后使用(父类)
表示当前类继承自括号中的父类,例如下面的代码:
Python | |
---|---|
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 41 42 |
|
在上面的代码中,创建了一个Teacher对象并继承自Person类
在Python中,默认情况下所有类继承自object
,只是在继承自object
类时可以省略不写,既然说Teacher
类是Person
类的子类,那么肯定有办法判断Teacher对象就是Person
类的子类对象,这时就需要使用到isinstance
函数:
Python | |
---|---|
1 2 3 |
|
可以看到,teacher
类对象根据isinstance
函数的返回值可以判断出,其既是Teacher
类的本类对象,也是Person
类的子类对象,所以二者都返回True
除了使用isinstance
以外,还可以使用issubclass()
函数来判断类是否是某个类的子类:
Python | |
---|---|
1 |
|
既然是继承,就代表子类对象可以访问父类中所有的方法和属性,Python中的继承也不例外,但是对于私有属性和保护属性来说,尽管子类拥有,但是同样不推荐直接访问
除了拥有父类的属性和方法外,子类也可以定义自己的属性和方法或者重写父类的方法,如果子类需要访问父类的方法或者属性时,可以使用super()
,例如下面的代码:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
多态性¶
有了继承,就可以考虑实现多态,实现多态的前提条件如下:
- 当前类继承另外一个类
- 重写父类的方法
- 父类引用指向子类对象
但是,由于Python中没有变量类型限制,所以多态性更多还是体现在传递参数上,例如下面的代码:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
如果Dog
类中存在say_hello()
方法,那么尽管函数中的person.say_hello()
可以被调用,但是此时并不是多态,为了保证是多态,可以将函数改成:
Python | |
---|---|
1 2 3 4 5 |
|
此时再执行上面的代码,结果就会不一样:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
迭代器¶
前面多次提到可迭代对象,也对可迭代对象作了介绍,但是可能还是不清楚为什么可以实现for
循环的那种遍历方式,实际上这就是因为迭代器的存在。回顾一下前面使用for
循环遍历可迭代对象:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
在Python中,这种for
的底层就是调用iter()
函数,这个方法返回一个定义了__next__()
方法的迭代器对象,这个方法将逐一访问容器中的元素,当遍历完最后一个元素时,__next__()
将引发StopIteration
异常来通知终止for
循环
Note
所谓可迭代对象就是实现了__iter__()
魔法方法的对象,当调用iter()
函数时,会转向调用对象的__iter__()
方法,如果对象没有实现该方法会报错为'type' object is not iterable
,如果对象实现了__getitem__()
方法,Python会尝试创建一个迭代器来按索引访问元素
在Python中,可以通过next()
内置函数,让该函数调用__next__()
方法来了解其运作过程:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
而使用for
循环直接遍历可迭代对象就可以模拟为:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
接下来,为了更细致的了解__iter__()
方法和__next__()
方法分别在做什么,就需要看看官方是如何对他们进行定义的:
__iter__()
返回iterator
对象本身。这是同时允许容器和迭代器配合for
和in
语句使用所必须的
__next__()
iterator
中返回下一项。 如果已经没有可返回的项,则会引发StopIteration
异常
从上面的概念可以得知,__iter__()
方法本质就是返回迭代器对象本身,也就是说,如果当前类是个迭代器类,那么就直接返回self
即可,而对于__next__()
方法来说,因为其返回的是iterator
的下一项,也就是说获取到下一个内容
根据上面的概念,下面实现一个链表来详细描述这两个方法:
Python | |
---|---|
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 41 42 |
|
重点看上面代码中涉及到迭代器的部分:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
在单链表中,实现了__iter__()
方法,将头结点的下一个节点作为第一个节点创建单链表迭代器对象,因为最后遍历的是单链表,所以单链表类一定要实现__iter__()
方法,接着进入单链表迭代器类中,因为需要一个节点记录下一次需要走向的位置,所以在__init__()
方法中传入了一个参数node
,接着要使单链表能够正常向后遍历,就需要使用到__next__()
方法用于获取到下一个节点,使用cur
变量记录当前节点并作为返回值,返回之前更新node
到下一个节点的位置,如果node
变量变为None
,说明走到了链表结尾,此时就触发StopIteration
异常即可
上面的代码在下面的测试用例下是没有问题的:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
但是如果使用下面的测试用例,就会出现报错:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
可以看到,提示LinkedListNodeIterable
并不是一个可迭代对象,原因就是LinkedListNodeIterable
没有实现__iter__()
方法,而因为LinkedListNodeIterable
本身就是迭代器类,所以对于__iter__()
方法来说直接返回当前类对象self
即可,即:
Python | |
---|---|
1 2 3 4 5 6 7 |
|
再次测试即可得到正常遍历结果,此处不再演示
通过上面的例子,可以说明如果一个类想要是可迭代对象,就必须实现__init__()
方法,而为了可以获取到下一个元素,就需要实现__next__()
方法,至于这个方法是否需要实现到一个单独的迭代器类中就完全取决于每个人的习惯
生成器¶
前面在设计迭代器时,既需要实现__iter__()
方法,也需要实现__next__()
方法,为了简化这个过程,可以使用生成器辅助创建迭代器
Python中的生成器是一种特殊的迭代器,它允许程序员声明一个函数,该函数可以保存其状态并在多次调用之间保持这个状态。生成器是通过使用yield
语句来实现的,而不是传统的return
语句,即在一个函数体内使用yield
表达式会使这个函数变成一个生成器函数
例如下面的代码:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
在这个例子中,simple_generator
函数是一个生成器函数,每次调用next()
都会从上次离开的地方继续执行,直到没有更多的yield
语句为止
现在考虑使用生成器简化前面的链表迭代器的实现:
Python | |
---|---|
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 |
|
有了生成器函数generator
就只需要用类对象调用这个生成器函数就可以创建一个可迭代对象,执行步骤如下:
- 初始化:方法开始时,
cur
被设置为_head.next
,即链表的第一个实际节点 - 循环遍历:通过
while cur is not None:
循环,从第一个实际节点开始遍历整个链表 yield
语句:每当遇到yield cur
时,当前的cur
节点被返回给调用者,并且生成器函数会暂停执行。这意味着每次调用next()
或在for
循环中迭代时都会得到下一个节点- 状态保存:生成器在每次
yield
后会保存其内部状态,包括cur
变量的值。当下一次需要继续迭代时,它将从上次暂停的地方继续执行 - 移动到下一个节点:在
yield
之后,cur = cur.next
将指针移到下一个节点,准备下一次迭代