文章大纲
自定义对象是 Python 的核心概念。
对象
在面向对象中,对象意味着一系列数据(属性)以及一套访问和操作这些数据的方法。
使用对象的好处:
- 多态:对不同类型的对象执行相同的操作
- 封装:对外部隐藏有关对象工作原理的细节
- 继承:可基于通用类创建出专用类
多态
多态指的是即使不知道变量指向的是哪种对象(字符串、整型、列表、字典)也能够对其执行操作,且操作的行为将随对象所属的类型而异。
多态和方法
与对象属性相关联的函数称为方法。
每当无需知道对象是什么样类型的都能对其执行操作,都是多态在起作用。
内置运算符和大量的内置类的方法都使用了多态,例如:
1 + 2
3
'Hello' + 'World'
'HelloWorld'
'Hello'.count('o')
1
[1,2,3,1].count(1)
2
封装
封装是指向外部隐藏不必要的细节,例如方法或变量。
当调用对象中的方法时不需要操心其它事情,如避免干扰全局变量,通过属性将名称封装在对象中。
属性是归属于对象的变量,就像方法一样。
继承
如果已经有了一个类,并要创建一个与之类似的类,可能是在此基础上增加了几个方法,此时可以继承已有的类,在新创建的类中可以直接调用被继承类中的属性,如变量或方法。
类
每个对象都属于特定的类,并称为该类的实例。本质上类也是一种对象。
创建自定义类
自定义类的示例:
class Dog:
def set_name(self, name):
self.name = name
def get_name(self, name):
return self.name
def run(self):
print("Dog ", self.name, "running")
class
语句会创建独立的命名空间,用于在其中定义函数。self
是指向对象本身:
d1 = Dog()
d1.set_name('大黄')
d1.run()
Dog 大黄 running
d2 = Dog()
d2.set_name('大白')
d2.run()
Dog 大白 running
如果没有传递 self
所有的方法都无法访问对象本身(也就是所有要操作的属性所属的对象)。
也可以从外部访问这些属性:
d1.name
'大黄'
d2.name
'大白'
属性、函数和方法
方法和函数的区别表现在是否具有 self
参数上。方法将第一个参数关联到它所属的实例,所需无需提供这个参数,属性也可关联到函数,但是就没有特殊的 self
参数了。
class Demo:
def method(self):
print('I have a self')
def method():
print('I not have self')
instance = Demo()
instance.method()
I have a self
instance.method = method
instance.method()
I not have self
事实上,可以将变量指向一个方法:
instance2 = d1.run
instance2()
Dog 大黄 running
当调用该方法时,它也可以访问 self
参数。
隐藏
Python 没有为私有属性提供直接的支持,意味着能够直接从外部访问对象内部的属性。
要使方法或属性称为私有,只需让名称以两个下划线开头即可:
class Dog:
def __get_name(self):
return '大黄'
def run(self):
print('Dog', self.__get_name(), 'run')
d1 = Dog()
d1.__get_name()
Traceback (most recent call last):
File "<pyshell#63>", line 1, in <module>
d1.__get_name()
AttributeError: 'Dog' object has no attribute '__get_name'. Did you mean: '_Dog__get_name'?
d1.run()
Dog 大黄 run
当从外部直接调用方法时,出现报错,但是内部可以根据名称进行调用。
私有是不可能存在的,其实错误提示中已经告知了方法,使用 _Dog__get_name
进行访问,也就是在方法名称前面加上 _类名
即可:
d1._Dog__get_name()
'大黄'
针对变量的隐藏也是如此:
class Student:
__name = 'Bob'
def hi(self):
print('hi', Student.__name)
s1 = Student()
s1.__name
Traceback (most recent call last):
File "<pyshell#78>", line 1, in <module>
s1.__name
AttributeError: 'Student' object has no attribute '__name'
s1._Student__name
'Bob'
s1.hi()
hi Bob
如果不希望名称被修改,又向发出不要从外部修改属性或方法的信号,可以用一个下划线打头,这是一种约定俗成的操作,例如 from module import *
不会导入以一个下划线开头的名称。
类的命名空间
在 class
语句中定义的代码都是在一个特殊的命名空间(类的命名空间)内执行的,类的所有成员都可以访问这个命名空间。class
中不仅仅存在 def
语句,还可以存在变量等语句,例如:
class Num:
i = 0
def init(self):
Num.i += 1
n1 = Num()
n1.init()
n1.i
1
n2 = Num()
n2.init()
n2.i
2
可以看到每个实例都能访问类中的对象,i
被初始化两次后,值变成了 2,类似于全局变量和局部变量的遮盖问题。
如果直接对属性进行修改,就会遮住类级的变量:
n1.i = 'Bob'
n1.i
'Bob'
n2.i
2
如果是定义成以下方式就可以避免:
class Num:
def set_i(self, n):
self.i = n
def init(self):
self.i += 1
n1 = Num()
n1.set_i(100)
n1.init()
n1.i
101
n2 = Num()
n2.set_i(1000)
n2.init()
n2.i
1001
n1.i
101
指定超类
子类扩展了超类的定义,要指定超类,需要在 class
语句中的类名称后面加上超类名,并用圆括号括起:
class Filter:
def init(self):
self.blocked = []
def filter(self, sequence):
return [x for x in sequence if x not in self.blocked]
class SPAMFilter(Filter):
def init(self):
self.blocked = ['SPAM']
s = SPAMFilter()
s.init()
s.filter(['SPAM', 'hi',1,'SPAM','123'])
['hi', 1, '123']
可以看到 SPAMFilter
集成了 Filter
, Filter
是 SPAMFilter
的超类,在 SPAMFilter
中改写了 init
方法,没有修改 filter
方法,但是实例中可以直接调用。
验证集成的关系
使用内置方法 issubclass
可以确定一个类是否是另一个类的子类:
issubclass(SPAMFilter, Filter)
True
issubclass(Filter, SPAMFilter)
False
如果知道子类,想要知道其基类,可以使用类的特殊属性 __bases__
:
SPAMFilter.__bases__
(<class '__main__.Filter'>,)
Filter.__bases__
(<class 'object'>,)
要确定实例是否是特定对象的实例,可以使用 isinstance
内置方法:
s = SPAMFilter()
isinstance(s, SPAMFilter)
True
f = Filter()
isinstance(f, SPAMFilter)
False
isinstance(f, Filter)
True
也可以使用实例的 __class__
属性:
f.__class__
<class '__main__.Filter'>
s.__class__
<class '__main__.SPAMFilter'>
多个超类
基类可以有多个:
class Info:
def set_info(self, name):
self.name = name
class Print:
def print_info(self):
print('Hi, my name is ', self.name)
class PrintInfo(Info, Print):
pass
pp = PrintInfo()
pp.set_info('imxcai')
pp.print_info()
Hi, my name is imxcai
除非万不得已,应避免使用多重继承。
同时在 class
语句中,超类的位置很重要,位于前面的类中的方法将覆盖位于后面的类的方法。如果 Info
类中存在 print
方法,就会覆盖 Print
中的 print
方法:
class Info:
def set_info(self, name):
self.name = name
def print(self):
print('hi from Info class')
class Print:
def print_info(self):
print('Hi, my name is ', self.name)
class PrintInfo(Info, Print):
pass
pp = PrintInfo()
pp.set_info('imxcai')
pp.print()
hi from Info class
接口和内省
检查实例是否具有特定的方法可以通过内置方法 hasatter
:
hasattr(pp, 'print')
True
hasattr(pp, 'delete')
False
也可以通过 callable
来检查是否能够被调用。
抽象基类
Python 通过引入 abc
模块来实现接口的理念。
抽象类是不能实例化的类,其职责是定义子类应实现的一组抽象方法:
from abc import ABC, abstractmethod
class Brid(ABC):
@abstractmethod
def fly(self):
pass
Brid()
Traceback (most recent call last):
File "<pyshell#221>", line 1, in <module>
Brid()
TypeError: Can't instantiate abstract class Brid without an implementation for abstract method 'fly'
class BlueBrid(Brid):
def fly(self):
print('fly')
BlueBrid()
<__main__.BlueBrid object at 0x7f4d19c78c80>
在 Brid
类中定义了抽象基类,那么就无法进行实例化,必须使用子类实现了抽象基类才能实例化。
只要实现了 fly
方法,即使不是 Brid
的子类,依然可以通过类型检查:
class Bee:
def fly(self):
print('fly..')
b = Bee()
isinstance(b, Brid)
False
Brid.register(Bee)
<class '__main__.Bee'>
isinstance(b, Brid)
True
issubclass(Bee, Brid)
True
使用类中的 register
属性对非子类的类进行注册,这样就能通过类型检查,可以看到子类检查也通过了,但是无法使用超类中的属性。
面向对象设计的一些思考
设计对象时:
- 将相关东西放在一起
- 方法应只关心其所属实例的属性
- 慎用继承
- 保持简单
确定需要哪些类,以及类中应包含哪些方法,可以按照以下步骤筛选:
- 将问题描述记录,标记名词、动词和形容词
- 在名词中找出可能的类
- 在动词中找出可能的方法
- 在形容词中找出可能的属性
- 然后将找出的方法、属性分配给各个类
有了面对对象的模型之后,还需考虑类和对象之间的关系,加以改进。