“结构化编程”是一种编程思想:每个程序,都是顺序、分支、循环结构的组合。

随着程序规模扩大,复杂度也随之提高,虽然我们可以用模块来切分代码文件,但使用起来依旧复杂。
因为面向过程标准化了程序执行过程,但它没有对数据做更多的约束,数据依旧散落在各个地方。
比如,我们想要获取某个文件的文件名和扩展名。

我们可以通过标准模块os或pathlib来完成。

os模块处理

  1. import os
  2. f_path = '/var/log/hello.txt'
  3. fpath, fname = os.path.split(f_path)
  4. fname, fext = os.path.splitext(fname)
  5. print(fname, fext)

pathlib模块处理方式

  1. import pathlib
  2. p = pathlib.Path('/var/log/hello.txt')
  3. print(p.stem, p.suffix)

从两个的对比,我们可以看到:

  • os模块用面向过程的方式,按步骤获取文件名和扩展名。
  • pathlib模块用面向对象的方式,生成了一个Path 对象,再获取其属性,suffix指扩展名,stem指不含扩展的文件名。

两者更显著的区别,在于“封装粒度”。

  • 使用os模块时,我们把获取数据的过程也体现出来;
  • 使用pathlib模块时,我们直接获取数据,并没有暴露过程。

再举个例子:获取当前文件的上两级文件夹。
如果是用命令行执行,就是连续两次的cd ..

os模块处理

  1. import os
  2. cur_fpath = os.path.dirname(os.path.abspath('hello.txt'))
  3. par_path = os.path.dirname(cur_fpath)
  4. par2_path = os.path.dirname(par_path)
  5. print(par2_path)

pathlib模块处理方式

  1. import pathlib
  2. p = pathlib.Path('hello.txt')
  3. print(p.absolute().parent.parent.parent)
  4. print(list(p.absolute().parents)[2])

可以很明显看出,pathlib的使用方式更简单,也更容易读懂。

事实上,pathlib是os.path的更高级封装,让开发者更方便处理操作系统的文件路径。
Python3.6版本中pathlib已成熟,推荐优先用它来处理文件路径。
Python是最纯正的面向对象语言,一切皆对象。

本文内容:

  1. 类和实例:面向对象基本概念
  2. 继承和多态:高级“偷懒”,少写代码
  3. 一切皆对象:再看数据类型和数据结构

1、类和实例

面向对象最核心的两个概念就是:类(class)和实例(instance)。
类就是模板,实例就是从模板“刻”出来的对象。

1.1 定义类

在生成对象之前,我们必须定义类,也就是模板。
Python中用 class开头定义类:

  1. class Dog():
  2. """some notes"""
  3. def __init__(self, name='xiaobai'):
  4. self.name = name
  5. def woof(self):
  6. """狗叫"""
  7. print('{}: Woof! Woof!'.format(self.name))

上面我们定义了一个“Dog”的类,其中:

  1. 类定义以class开头,跟着类名,接着小括号,小括号里是继承类,如果不写则默认继承自object,最后以冒号结尾。
  2. 定义了一个实例属性name,未来创建的每个实例,都会有不同的值。
  3. 定义了两个方法:init()和woof(),这两个方法第一个参数是self,当然你可以写成其他名字,但建议用self习惯,表示它代表的是未来创建的实例。方法和函数写法类似,只不过它包含在类定义中,第一个参数指向未来的实例。
  4. init()方法比较特殊,它是实例的初始化方法,我们把实例需要的数据在这里定义,比如例子中的name,表示狗的名字。
  5. woof()方法定义了类的行为,这个类创建的实例,都具备这个行为能力。

就这样,我们就定义了一个“会叫的狗”的模板。

1.2 创建实例

接下来,我们来生成几个狗的实例,比如叫”小白”、”小黄”、”大黑”。

  1. xiaobai = Dog('小白')
  2. xiaohuang = Dog('小黄')
  3. dahei = Dog('大黑')
  4. xiaobai.woof()
  5. xiaohuang.woof()
  6. dahei.woof()

我们用Dog这个类生成了三个狗的实例,并让它们都“叫”了一声。

  1. Dog(‘小白’)表示初始化一个实例,通过之前定义的init()方法为实例name属性赋值。
  2. 通过实例变量后跟.操作符调用实例方法。

Python实例化内部过程(简单了解即可): 首先会调用> new()这个类方法创建一个实例,如果你没有重新定义这个方法,类就会从父类(这里是object)那里调用这个方法,然后把实例传递给> init()的第一个参数初始化实例。

类就像一个模板,可以生成任意多的实例,只要内存够大。

比如我们创建100条狗,并让它们都叫一声。
记得Python列表生成么?可别用100个变量呐。

  1. dogs = [ Dog('dog_no_{}'.format(i)) for i in range(100)]
  2. for d in dogs:
  3. d.woof()

2、继承和多态

上面的案例,我们已经成功定义了一个“Dog”模板,并可以生成任意多“Dog实例”并让它们叫。
现在,我们开始养猫。
还是从模板定义开始,这次是“猫”模板。

  1. class Cat(object):
  2. """some notes"""
  3. def __init__(self, name='xiaobai'):
  4. self.name = name
  5. def miao(self):
  6. """猫叫"""
  7. print('{}: miao~ miao~'.format(self.name))
  8. cat = Cat('小白')
  9. cat.miao()

定义完模板,就可以生成实例了,上面生成了一只名叫“小白”的猫。

感觉挺容易嘛~这么优秀,动物园的人准备找你了:-)
请你再生成100只猴子,100匹马,100只狮子……

当你持续“复制代码”、“修改代码”的时候,是否想过:能否自动识别和共享公共部分的代码
比如name,还有发声行为(虽然叫法不同)。

恭喜,你发现了面向对象一个最大的特性:继承。

2.1 继承

既然动物都有名字,那能否先定义一个“动物”的基础类,其他动物从这个类“派生”出来呢?

我们用了“基础”和“派生”的说法,用模板理解就是:你做了一个通用PPT模板A送给了小红,她把模板颜色改成了红色,变成了模板B,又散播出去,这时候我们说A是基础类(也叫父类),B是派生类(也叫子类),B继承于A。

回到动物园的例子,我们也创建一个动物的基础类,然后让猫和狗继承它:

  1. class Animal(object):
  2. """动物基础类"""
  3. def __init__(self, name='xiaobai'):
  4. self.name = name
  5. def wow(self):
  6. """动物叫"""
  7. print('{}: wow~ wow~'.format(self.name))
  8. class Dog(Animal):
  9. def wow(self):
  10. """狗的叫法"""
  11. print('{}: Woof! Woof!'.format(self.name))
  12. class Cat(Animal):
  13. def wow(self):
  14. """猫的叫法"""
  15. print('{}: miao~ miao~'.format(self.name))
  16. dog_xiaobai = Dog('小白')
  17. cat_xiaohuang = Cat('小黄')
  18. dog_xiaobai.wow()
  19. cat_xiaohuang.wow()

这样,我们不必为每类动物都指定一样的初始化方法,而且它们都有一个行为wow(),在这个行为中,每个动物都分别采用了自己的叫法,在面向对象中叫做“方法重写”。

这时候,创建更多动物工作量就少很多了,只需要创建一个Animal的子类,并重写wow()方法即可。

如果我们随机生成100个动物实例,里面有猫有狗,让它们分别叫一色,该怎么操作呢?

2.2 多态

  1. import random
  2. animals = [ Dog('dog_no_{}'.format(i))
  3. if random.randint(0, 100)%2==0
  4. else Cat('cat_no_{}'.format(i))
  5. for i in range(100)]
  6. for a in animals:
  7. a.wow()

我们生成了一个动物列表,其中随机生成了100次整数,当这个整数是偶数时,就创建狗实例,如果是奇数就创建猫实例。然后用循环让列表中的每个动物都叫了一声。

这里,我们并没有明确列表中动物的具体类型,但它们都可以被调用wow()方法,这就是面向对象中的“多态”(多种状态)。

除了通过输出的“叫声”判别我们生成的动物,也可以用之前介绍过的isinstance()和type()来判断实例所属的类。

  1. for a in animals:
  2. print(type(a))
  3. print(isinstance(a,Dog))

但是,有些动物有毛,有些动物没毛,怎么区分它们呢?比如我们新增一类动物:蛇。

如果某个动物,比如某只狗,毛掉光了(也许被主人剃了),我们可以直接操作这个实例数据:

  1. def animal_has_hair(animal):
  2. """判断动物是否有毛"""
  3. return animal.has_hair
  4. dog1 = Dog('dog_naked')
  5. dog1.has_hair = False
  6. dog2 = Dog('dog_normal')
  7. dog2.has_hair = True
  8. animal_has_hair(dog1) # False

2.3 类属性

当然,我们可以把has_hair这个数据,作为Dog的实例属性,在初始化时赋予。
但,更准确地说,has_hair对于一类动物都一样,比如“狗和猫都长毛”,只不过有时候它们被剃光。
所以,我们应该把has_hair作为一个类的属性,于是我们重新定义猫狗和蛇的子类:

  1. class Dog(Animal):
  2. has_hair = True
  3. def wow(self):
  4. print('{}: Woof! Woof!'.format(self.name))
  5. class Cat(Animal):
  6. has_hair = True
  7. def wow(self):
  8. print('{}: miao~ miao~'.format(self.name))
  9. class Snake(Animal):
  10. has_hair = False
  11. def wow(self):
  12. print('{}: si~ si~'.format(self.name))
  13. d = Dog('小白')
  14. d.wow()
  15. s = Snake('思思')
  16. s.wow()
  17. print(animal_has_hair(s))
  18. d.has_hair = False
  19. print(animal_has_hair(d))

这时候,has_hair是一个类属性,而不是实例属性,当然我们也可以在实例中修改它。

2.4 类方法

相对于实例方法,我们自然也可以有类方法,不同的是第一个参数值。

  • 在实例方法中,第一个参数指向实例对象
  • 而类方法第一个参数指向所属类,习惯用cls命名。
  • 此外,类方法需要加上一个装饰器@classmethod,表示它是一个类方法。

比如,我们可以用类方法,来为每个动物指定它们最喜欢的食物。
写法上,我们可以为每个类写一个类方法,也可以通过基础类来支持所有子类。
比如:

  1. import random
  2. class Animal(object):
  3. def __init__(self, name='xiaobai'):
  4. self.name = name
  5. def wow(self):
  6. print('{}: wow~ wow~'.format(self.name))
  7. @classmethod
  8. def fav_food(cls):
  9. """最喜欢的食物"""
  10. if cls is Dog:
  11. return 'bones'
  12. elif cls is Cat:
  13. return 'fish'
  14. class Dog(Animal):
  15. has_hair = True
  16. def wow(self):
  17. print('{}: Woof! Woof!'.format(self.name))
  18. class Cat(Animal):
  19. has_hair = True
  20. def wow(self):
  21. print('{}: miao~ miao~'.format(self.name))
  22. print(Dog.fav_food())
  23. print(Cat.fav_food())
  24. animals = [ Dog('dog_no_{}'.format(i))
  25. if random.randint(0, 100)%2==0
  26. else Cat('cat_no_{}'.format(i))
  27. for i in range(10)]
  28. for a in animals:
  29. print(a.fav_food())

从案例中可以看到:

  • 首先,类方法可以直接从类调用,也可以从实例调用。
  • 其次,我们可以用is来判断cls的类型,在基础类中统一定义不同子类的行为,由此省去不少重复代码。

以上,就是Python中类和对象的常见用法及关系。

3、一切皆对象

我们用class来定义自己的类,而Python内置的那些数据类型和数据结构,都是提前定义好的类。
而且它们都继承自object类,可以从base这个类属性查看所继承的父类:

3.1 所有类都继承自object类

  1. def func():
  2. pass
  3. class MyClass():
  4. pass
  5. f = lambda: None
  6. types = [ bool, int, float, str, tuple, list, set, dict, type(func), MyClass, type(f)]
  7. for t in types:
  8. print(t.__base__)

可以看到,数字、字符串、元组、列表、字典、函数等等最后都继承自object类。
object是所有类的根类。但为什么它要用object(对象)来命名呢?为什么不是“RootClass”之类的?
因为object本身就是一个对象,也就是说,Python的类也可以是对象。

3.2 object是个type类的对象

我们可以用type(object)开查看它所属的类:

  1. type(object) # type(object)
  2. print(object.__class__) # <class 'type'>

结果都是type,type是定义根类object的类。
我们还可以用同样的方法查看,定义那些基本数据类型的类,也是type。
也就是说,type是定义一切类的类,而它本身是object的子类。

  1. type.__base__ # object

type和object的关系,就像鸡和蛋的关系,是同时出现的,object是type的实例,type是object的子类。

这个特性最主要作用是用来动态定义类,也就是不用class关键词定义类,而是在程序执行中用type来定义一个类,通常叫它”动态类”。它让我们能在程序执行时动态创建和修改类的特性和行为。

当然,这里大家只需简单了解即可,因为我相信大部分人在99%的情况下都不会用到。当你准备构建一个属于自己的框架时(比如Django开发框架),才可能会需要它。等到那时,你更需要提前储备设计模式相关的知识。

4、数据隐藏

关于类和实例的属性及方法,有一个约定:如果以下划线”“开头,代表它是内部数据或方法,应用调用时不应直接使用。
当我们自定义类时,如果不希望别人直接使用内部数据或方法,可以用
开头命名。当然,这并不能阻止别人访问,只不过是大家的一个“默契”。

比如,我们把has_hair改成__has_hair,如果直接使用就会出错,但我们依旧可以访问到它。

  1. class Dog(Animal):
  2. __has_hair = True
  3. @classmethod
  4. def has_hair(cls):
  5. return cls.__has_hair
  6. @classmethod
  7. def cut_hair(cls):
  8. cls.__has_hair=False
  9. d = Dog('xiaobai')
  10. d.has_hair() # True
  11. d.cut_hair()
  12. d.has_hair() # False
  13. d.__has_hair # 报错

上面,我们定义了has_hair这个内部的类属性,当最后一行我们尝试直接访问它时,程序报错AttributeError,提示无此属性。
但,我们可以用内置函数dir(d)来查看实例内部结构,它会返回实例所有的属性和方法的一个列表。
其中我们可以发现有一个属性叫_Dog
has_hair,它就是我们定义的那个内部类属性has_hair,可以被直接访问到。

  1. dir(d)
  2. d._Dog__has_hair # False

所以,在实际应用中,我们需要“自觉”遵守这个约定,把_和开头的属性和方法当成内部专有,不直接调用它们。一般开发者会留出专门访问数据的方法,比如上面我们增加的cut_hair()和has_hair()两个类方法。

总结

本文主要讲了Python面向对象中的关键概念,以及类继承和多态的使用场景。

面向对象的抽象层级更高,对使用者的门槛更低。
但相比面向过程的程序,它在执行时会有更大损耗,比如大对象的分配和回收。
所以,在选择哪种编程方式,取决于解决问题的实际情况。
比如,写一个程序批量处理一堆表格文件,面向过程即可;
如果是正在创建一个项目,比如开发一个数据采集和分享平台,那么面向对象会是更好的选择。


作者:程一初
更新时间:2020年8月
image.png