“结构化编程”是一种编程思想:每个程序,都是顺序、分支、循环结构的组合。
随着程序规模扩大,复杂度也随之提高,虽然我们可以用模块来切分代码文件,但使用起来依旧复杂。
因为面向过程标准化了程序执行过程,但它没有对数据做更多的约束,数据依旧散落在各个地方。
比如,我们想要获取某个文件的文件名和扩展名。
我们可以通过标准模块os或pathlib来完成。
os模块处理
import os
f_path = '/var/log/hello.txt'
fpath, fname = os.path.split(f_path)
fname, fext = os.path.splitext(fname)
print(fname, fext)
pathlib模块处理方式
import pathlib
p = pathlib.Path('/var/log/hello.txt')
print(p.stem, p.suffix)
从两个的对比,我们可以看到:
- os模块用面向过程的方式,按步骤获取文件名和扩展名。
- pathlib模块用面向对象的方式,生成了一个Path 对象,再获取其属性,suffix指扩展名,stem指不含扩展的文件名。
两者更显著的区别,在于“封装粒度”。
- 使用os模块时,我们把获取数据的过程也体现出来;
- 使用pathlib模块时,我们直接获取数据,并没有暴露过程。
再举个例子:获取当前文件的上两级文件夹。
如果是用命令行执行,就是连续两次的cd ..
os模块处理
import os
cur_fpath = os.path.dirname(os.path.abspath('hello.txt'))
par_path = os.path.dirname(cur_fpath)
par2_path = os.path.dirname(par_path)
print(par2_path)
pathlib模块处理方式
import pathlib
p = pathlib.Path('hello.txt')
print(p.absolute().parent.parent.parent)
print(list(p.absolute().parents)[2])
可以很明显看出,pathlib的使用方式更简单,也更容易读懂。
事实上,pathlib是os.path的更高级封装,让开发者更方便处理操作系统的文件路径。
Python3.6版本中pathlib已成熟,推荐优先用它来处理文件路径。
Python是最纯正的面向对象语言,一切皆对象。
本文内容:
- 类和实例:面向对象基本概念
- 继承和多态:高级“偷懒”,少写代码
- 一切皆对象:再看数据类型和数据结构
1、类和实例
面向对象最核心的两个概念就是:类(class)和实例(instance)。
类就是模板,实例就是从模板“刻”出来的对象。
1.1 定义类
在生成对象之前,我们必须定义类,也就是模板。
Python中用 class开头定义类:
class Dog():
"""some notes"""
def __init__(self, name='xiaobai'):
self.name = name
def woof(self):
"""狗叫"""
print('{}: Woof! Woof!'.format(self.name))
上面我们定义了一个“Dog”的类,其中:
- 类定义以class开头,跟着类名,接着小括号,小括号里是继承类,如果不写则默认继承自object,最后以冒号结尾。
- 定义了一个实例属性name,未来创建的每个实例,都会有不同的值。
- 定义了两个方法:init()和woof(),这两个方法第一个参数是self,当然你可以写成其他名字,但建议用self习惯,表示它代表的是未来创建的实例。方法和函数写法类似,只不过它包含在类定义中,第一个参数指向未来的实例。
- init()方法比较特殊,它是实例的初始化方法,我们把实例需要的数据在这里定义,比如例子中的name,表示狗的名字。
- woof()方法定义了类的行为,这个类创建的实例,都具备这个行为能力。
就这样,我们就定义了一个“会叫的狗”的模板。
1.2 创建实例
接下来,我们来生成几个狗的实例,比如叫”小白”、”小黄”、”大黑”。
xiaobai = Dog('小白')
xiaohuang = Dog('小黄')
dahei = Dog('大黑')
xiaobai.woof()
xiaohuang.woof()
dahei.woof()
我们用Dog这个类生成了三个狗的实例,并让它们都“叫”了一声。
- Dog(‘小白’)表示初始化一个实例,通过之前定义的init()方法为实例name属性赋值。
- 通过实例变量后跟.操作符调用实例方法。
Python实例化内部过程(简单了解即可): 首先会调用> new()这个类方法创建一个实例,如果你没有重新定义这个方法,类就会从父类(这里是object)那里调用这个方法,然后把实例传递给> init()的第一个参数初始化实例。
类就像一个模板,可以生成任意多的实例,只要内存够大。
比如我们创建100条狗,并让它们都叫一声。
记得Python列表生成么?可别用100个变量呐。
dogs = [ Dog('dog_no_{}'.format(i)) for i in range(100)]
for d in dogs:
d.woof()
2、继承和多态
上面的案例,我们已经成功定义了一个“Dog”模板,并可以生成任意多“Dog实例”并让它们叫。
现在,我们开始养猫。
还是从模板定义开始,这次是“猫”模板。
class Cat(object):
"""some notes"""
def __init__(self, name='xiaobai'):
self.name = name
def miao(self):
"""猫叫"""
print('{}: miao~ miao~'.format(self.name))
cat = Cat('小白')
cat.miao()
定义完模板,就可以生成实例了,上面生成了一只名叫“小白”的猫。
感觉挺容易嘛~这么优秀,动物园的人准备找你了:-)
请你再生成100只猴子,100匹马,100只狮子……
当你持续“复制代码”、“修改代码”的时候,是否想过:能否自动识别和共享公共部分的代码。
比如name,还有发声行为(虽然叫法不同)。
恭喜,你发现了面向对象一个最大的特性:继承。
2.1 继承
既然动物都有名字,那能否先定义一个“动物”的基础类,其他动物从这个类“派生”出来呢?
我们用了“基础”和“派生”的说法,用模板理解就是:你做了一个通用PPT模板A送给了小红,她把模板颜色改成了红色,变成了模板B,又散播出去,这时候我们说A是基础类(也叫父类),B是派生类(也叫子类),B继承于A。
回到动物园的例子,我们也创建一个动物的基础类,然后让猫和狗继承它:
class Animal(object):
"""动物基础类"""
def __init__(self, name='xiaobai'):
self.name = name
def wow(self):
"""动物叫"""
print('{}: wow~ wow~'.format(self.name))
class Dog(Animal):
def wow(self):
"""狗的叫法"""
print('{}: Woof! Woof!'.format(self.name))
class Cat(Animal):
def wow(self):
"""猫的叫法"""
print('{}: miao~ miao~'.format(self.name))
dog_xiaobai = Dog('小白')
cat_xiaohuang = Cat('小黄')
dog_xiaobai.wow()
cat_xiaohuang.wow()
这样,我们不必为每类动物都指定一样的初始化方法,而且它们都有一个行为wow(),在这个行为中,每个动物都分别采用了自己的叫法,在面向对象中叫做“方法重写”。
这时候,创建更多动物工作量就少很多了,只需要创建一个Animal的子类,并重写wow()方法即可。
如果我们随机生成100个动物实例,里面有猫有狗,让它们分别叫一色,该怎么操作呢?
2.2 多态
import random
animals = [ Dog('dog_no_{}'.format(i))
if random.randint(0, 100)%2==0
else Cat('cat_no_{}'.format(i))
for i in range(100)]
for a in animals:
a.wow()
我们生成了一个动物列表,其中随机生成了100次整数,当这个整数是偶数时,就创建狗实例,如果是奇数就创建猫实例。然后用循环让列表中的每个动物都叫了一声。
这里,我们并没有明确列表中动物的具体类型,但它们都可以被调用wow()方法,这就是面向对象中的“多态”(多种状态)。
除了通过输出的“叫声”判别我们生成的动物,也可以用之前介绍过的isinstance()和type()来判断实例所属的类。
for a in animals:
print(type(a))
print(isinstance(a,Dog))
但是,有些动物有毛,有些动物没毛,怎么区分它们呢?比如我们新增一类动物:蛇。
如果某个动物,比如某只狗,毛掉光了(也许被主人剃了),我们可以直接操作这个实例数据:
def animal_has_hair(animal):
"""判断动物是否有毛"""
return animal.has_hair
dog1 = Dog('dog_naked')
dog1.has_hair = False
dog2 = Dog('dog_normal')
dog2.has_hair = True
animal_has_hair(dog1) # False
2.3 类属性
当然,我们可以把has_hair这个数据,作为Dog的实例属性,在初始化时赋予。
但,更准确地说,has_hair对于一类动物都一样,比如“狗和猫都长毛”,只不过有时候它们被剃光。
所以,我们应该把has_hair作为一个类的属性,于是我们重新定义猫狗和蛇的子类:
class Dog(Animal):
has_hair = True
def wow(self):
print('{}: Woof! Woof!'.format(self.name))
class Cat(Animal):
has_hair = True
def wow(self):
print('{}: miao~ miao~'.format(self.name))
class Snake(Animal):
has_hair = False
def wow(self):
print('{}: si~ si~'.format(self.name))
d = Dog('小白')
d.wow()
s = Snake('思思')
s.wow()
print(animal_has_hair(s))
d.has_hair = False
print(animal_has_hair(d))
这时候,has_hair是一个类属性,而不是实例属性,当然我们也可以在实例中修改它。
2.4 类方法
相对于实例方法,我们自然也可以有类方法,不同的是第一个参数值。
- 在实例方法中,第一个参数指向实例对象
- 而类方法第一个参数指向所属类,习惯用cls命名。
- 此外,类方法需要加上一个装饰器@classmethod,表示它是一个类方法。
比如,我们可以用类方法,来为每个动物指定它们最喜欢的食物。
写法上,我们可以为每个类写一个类方法,也可以通过基础类来支持所有子类。
比如:
import random
class Animal(object):
def __init__(self, name='xiaobai'):
self.name = name
def wow(self):
print('{}: wow~ wow~'.format(self.name))
@classmethod
def fav_food(cls):
"""最喜欢的食物"""
if cls is Dog:
return 'bones'
elif cls is Cat:
return 'fish'
class Dog(Animal):
has_hair = True
def wow(self):
print('{}: Woof! Woof!'.format(self.name))
class Cat(Animal):
has_hair = True
def wow(self):
print('{}: miao~ miao~'.format(self.name))
print(Dog.fav_food())
print(Cat.fav_food())
animals = [ Dog('dog_no_{}'.format(i))
if random.randint(0, 100)%2==0
else Cat('cat_no_{}'.format(i))
for i in range(10)]
for a in animals:
print(a.fav_food())
从案例中可以看到:
- 首先,类方法可以直接从类调用,也可以从实例调用。
- 其次,我们可以用is来判断cls的类型,在基础类中统一定义不同子类的行为,由此省去不少重复代码。
以上,就是Python中类和对象的常见用法及关系。
3、一切皆对象
我们用class来定义自己的类,而Python内置的那些数据类型和数据结构,都是提前定义好的类。
而且它们都继承自object类,可以从base这个类属性查看所继承的父类:
3.1 所有类都继承自object类
def func():
pass
class MyClass():
pass
f = lambda: None
types = [ bool, int, float, str, tuple, list, set, dict, type(func), MyClass, type(f)]
for t in types:
print(t.__base__)
可以看到,数字、字符串、元组、列表、字典、函数等等最后都继承自object类。
object是所有类的根类。但为什么它要用object(对象)来命名呢?为什么不是“RootClass”之类的?
因为object本身就是一个对象,也就是说,Python的类也可以是对象。
3.2 object是个type类的对象
我们可以用type(object)开查看它所属的类:
type(object) # type(object)
print(object.__class__) # <class 'type'>
结果都是type,type是定义根类object的类。
我们还可以用同样的方法查看,定义那些基本数据类型的类,也是type。
也就是说,type是定义一切类的类,而它本身是object的子类。
type.__base__ # object
type和object的关系,就像鸡和蛋的关系,是同时出现的,object是type的实例,type是object的子类。
这个特性最主要作用是用来动态定义类,也就是不用class关键词定义类,而是在程序执行中用type来定义一个类,通常叫它”动态类”。它让我们能在程序执行时动态创建和修改类的特性和行为。
当然,这里大家只需简单了解即可,因为我相信大部分人在99%的情况下都不会用到。当你准备构建一个属于自己的框架时(比如Django开发框架),才可能会需要它。等到那时,你更需要提前储备设计模式相关的知识。
4、数据隐藏
关于类和实例的属性及方法,有一个约定:如果以下划线”“开头,代表它是内部数据或方法,应用调用时不应直接使用。
当我们自定义类时,如果不希望别人直接使用内部数据或方法,可以用开头命名。当然,这并不能阻止别人访问,只不过是大家的一个“默契”。
比如,我们把has_hair改成__has_hair,如果直接使用就会出错,但我们依旧可以访问到它。
class Dog(Animal):
__has_hair = True
@classmethod
def has_hair(cls):
return cls.__has_hair
@classmethod
def cut_hair(cls):
cls.__has_hair=False
d = Dog('xiaobai')
d.has_hair() # True
d.cut_hair()
d.has_hair() # False
d.__has_hair # 报错
上面,我们定义了has_hair这个内部的类属性,当最后一行我们尝试直接访问它时,程序报错AttributeError,提示无此属性。
但,我们可以用内置函数dir(d)来查看实例内部结构,它会返回实例所有的属性和方法的一个列表。
其中我们可以发现有一个属性叫_Doghas_hair,它就是我们定义的那个内部类属性has_hair,可以被直接访问到。
dir(d)
d._Dog__has_hair # False
所以,在实际应用中,我们需要“自觉”遵守这个约定,把_和开头的属性和方法当成内部专有,不直接调用它们。一般开发者会留出专门访问数据的方法,比如上面我们增加的cut_hair()和has_hair()两个类方法。
总结
本文主要讲了Python面向对象中的关键概念,以及类继承和多态的使用场景。
面向对象的抽象层级更高,对使用者的门槛更低。
但相比面向过程的程序,它在执行时会有更大损耗,比如大对象的分配和回收。
所以,在选择哪种编程方式,取决于解决问题的实际情况。
比如,写一个程序批量处理一堆表格文件,面向过程即可;
如果是正在创建一个项目,比如开发一个数据采集和分享平台,那么面向对象会是更好的选择。
作者:程一初
更新时间:2020年8月