Python面向对象基础篇

基础篇包括面向对象的三大要素: 继承, 封装, 多态, 以及抽象类和抽象方法, 类方法, 静态方法等.

define: python-version 3.5.2

1. 继承

虽然在Python中允许多重继承, 但是我们如果能够单根继承的话, 尽量选择单根继承. 继承的目的是为了代码的复用, 抽离重复代码; 或者是因为设计上的考量, 需要使用继承的手段来完成.
假设说我们有一个卖书的网站, 需要卖书, 然后对书籍做一个展示.
最开始我们可以使用一个大类来实现:

1
2
3
4
5
6
7
8
9
10
11
class Book:
def __init__(self, name, type, isbn, price):
self.name = name
self.type = type
self.isbn = isbn
self.price = price

# 展示各种各样的书籍
python_cookbook = Book("python_cookbook", "computer", "123", 89)
fluent_python = Book("fluent_python", "computer", "124", 109)
hackers_and_painters = Book("hackers_and_painters", "internet", "252", 49)

后面儿有大佬投资我们的破烂儿卖书网站, 还得卖键盘鼠标CPU显卡手办塑料小人儿这些乱七八糟的东西.
好嘛, 为了钱, 啥都能干.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Keyboard:
def __init__(self, name, type, barcode, price):
self.name = name
self.type = type
self.barcode = barcode
self.price = price

class Mouse:
def __init__(self, name, type, barcode, price):
self.name = name
self.type = type
self.barcode = barcode
self.price = price

...

可以看到Book, Keyboard, Mouse的实例化方法非常的相似, 都是描述一个商品信息的数据. 那么我们就可以抽离出相似的特征, 形成一个基类, 具体的商品由基类派生.

1
2
3
4
5
6
7
8
9
10
11
12
13
class BaseProduct:
def __init__(self, name, type, barcode, price):
self.name = name
... # 一堆初始化

class Book(BaseProduct):
def __init__(self, name, type, barcode, price, isbn):
super(Book, self).__init__(name, type, barcode, price)
self.isbn = isbn

class Mouse(BaseProduct):
def __init__(self, name, type, barcode, price, isbn):
super(Mouse, self).__init__(name, type, barcode, price)

我们知道任何一件商品都会有条形码, 但是只有书籍才会有isbn号, 而我们将isbn写入到了BaseProduct这个类中. 此时就有了2种选择: 一是其余非书籍的商品isbn号为None; 二是将isbn这个特征单独放入到Book类中, 就像上面的代码一样.

继承就是将相同的代码进行抽离, 比如微星的显卡, 华硕的显卡, 它们都是显卡. 虽然在用料上有所区别, 但是同一型号的显卡核心是相同的, 由英伟达生产. 那么抽离出英伟达的核心, 能够减少重复代码, 使得代码更加易于维护.
当我们抽离出一个父类之后, 可能父类和父类又有相同的地方, 这样层层抽象, 最终达到一个无法继续抽象的地步.

继承我认为适用于拥有多个相似类型的类, 或者某一个类型的数据在将来可能需要进行扩展, 使用继承未雨绸缪. 但是不能够因为为了使用继承而继承.

2. 封装

类其实就是数据和方法的集合, 将数据和方法聚合起来, 形成一个紧密的结构体, 我认为这就是封装.
参考上面被大佬投资的破烂儿书店. 一个商品会有很多种行为, 这些行为是与自身的数据紧密相关的.

1
2
3
4
5
6
7
8
9
10
class BaseProduct:
def __init__(self, name, type, barcode, price):
self.name = name
... # 一堆初始化

def get_discount_price(self):
return self.price * 0.8

def get_long_name(self):
return "[限时特惠] " + self.name

封装其实就是将与类数据相关的行为写入到类中, 而不是其他地方(比如一个全局函数). 通常来讲, 判断一个类的封装性好坏, 可以看看类中的方法是否都是静态方法或者只有少部分的实例方法(或类方法).

2.1 实例方法, 类方法, 静态方法

简单的来说, 实例方法由实例对象调用; 类方法可以直接使用类对象来调用, 而不必使用实例化对象调用; 静态方法, 和普通的函数没啥区别, 只是碰巧的出现在了一个类之中. 函数内部不会使用与该类有关的任何数据.

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
43
44
45
46
47
48
49
50
class Person:
class_contribute = 0

def __init__(self, name, age):
self.name = name
self.age = age

def get_age_and_name(self):
return self.name, self.age

@classmethod
def add_contribute(cls):
cls.class_contribute += 1

@staticmethod
def say_hello():
print("Hello~ I'm static method")


In [5]: smart = Person("smart", 18)

In [6]: smart.get_age_and_name()
Out[6]: ('smart', 18)

In [7]: smart.add_contribute()

In [8]: smart.class_contribute
Out[8]: 1

In [9]: smart.say_hello()
Hello~ I'm static method

In [10]: Person.get_age_and_name()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-10-04272c476174> in <module>()
----> 1 Person.get_age_and_name()

TypeError: get_age_and_name() missing 1 required positional argument: 'self'

In [11]: Person.add_contribute
Out[11]: <bound method Person.add_contribute of <class '__main__.Person'>>

In [12]: Person.add_contribute()

In [13]: Person.class_contribute
Out[13]: 2

In [14]: Person.say_hello()
Hello~ I'm static method

那么从上面的调用我们可以发现: 实例对象, 什么方法都能调用; 类对象, 只能调用类方法和静态方法.
在理解classmethodstaticmethod的时候可以这样来想. 但实际上类对象也是可以调用实例方法的, 只不过此时需要显示的将实例对象传递给相应的实例方法.

1
2
smart.get_age_and_name()
Person.get_age_and_name(smart)

这两者是等价的, 其实后一种调用方法其实就是实例方法调用的本质.

假如说我们的自己封装的一类里面全部都是@staticmethod, 那么其实这个类根本就没有任何意义, 还不如直接写函数.

3. 多态

其实多态的真正威力在强类型语言(比如Java)中更能够得到体现, 在Python这样的弱类型语言中, 会有一些威力, 但是威力没有那么强.
多态其实就是动态获取类型信息的一种机制. 以Java为例我认为会更好一些:

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
43
44
45
46
47
48
49
50
51
52
53
class Product {
protected String name;
private String type;
private int price;

public Product() {}

public Product(String name, String type, int price){
this.name = name;
this.type = type;
this.price = price;
}

public String getProductName() {
return " ";
}
}

class Book extends Product {

public Book(String name, String type, int price) {
super(name, type, price);
}

public String getProductName() {
return "[限时促销]" + name;
}
}

class Mouse extends Product {

public Mouse(String name, String type, int price) {
super(name, type, price);
}

public String getProductName() {
return "[限时促销]" + name;
}
}

public class ProductTest {

public static void getName(Product product) {
System.out.println(product.getProductName());
}

public static void main(String[] args){
Product book = new Book("Python Cookbook", "unknown", 99);
Product mouse = new Mouse("Mouse", "unknown", 199);
ProductTest.getName(book);
ProductTest.getName(mouse);
}
}

代码比较长, 其实主要看main函数中的代码即可. BookMouse均继承于Product, 那么也就是说Book类型其实也是Product类型, 可以进行向上转型. 在向上转型之后, bookmouse均为Product对象. 然后传入getName函数进行调用getProductName函数. 我们故意让父类的getProductName返回空字符串, 然后审视调用结果.

1
2
[限时促销]Python Cookbook
[限时促销]Mouse

完全就是对应的对象所调用方法的结果, 而不是父类的调用结果. 这个就是多态: 在使用父类对象来实例化一个具体的子类时, 使用了泛化的父类来进行调用, 也能够产生正常的行为.

Python中, 多态的机制会被无限的放大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person:
def __init__(self, name):
self.__name = name

def get_name(self):
return self.__name

class smart(Person):
def __init__(self, name):
super(smart, self).__init__(name)

def get_person_name(person):
print(person.get_name())

In [26]: person = Person("person")

In [27]: smart_instance = smart("smart")

In [28]: get_person_name(person)
person

In [29]: get_person_name(smart_instance)
smart

看起来没有什么问题, 但是如果我们有另外一个对象, 也有get_name方法的话, 同样可以被调用:

1
2
3
4
5
6
7
8
9
10
11
class Product:
def __init__(self, name):
self.__name = name

def get_name(self):
return self.__name

In [31]: product = Product("mouse")

In [32]: get_person_name(product)
mouse

然而其实product对象并不能称为person…这就是Pytho语言多态机制的乏力性. 不管你是什么类型, 只要你有这个方法, 你就能被当做统一的对象来调用该方法. 也称为鸭子类型.
这也是Python灵活的原因之一, 没有那么多的条条框框, 想做什么就做什么, 不需要关注类型.

4. 抽象类

Python中的抽象类实现并没有像Java中那么简单, 需要额外的辅助:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import abc

class Product(metaclass=abc.ABCMeta):
def __init__(self, name):
self.name = name

@abc.abstractmethod
def should_be_implement(self):
pass

class Book(Product):
def __init__(self, name):
super(Book, self).__init__(name)

def should_be_implement(self):
print("has been implemented")

In [40]: book = Book("python")

In [41]: book.should_be_implement()
has been implemented

可以看到此时Product无法被实例化, 只能通过继承, 然后实现所定义的抽象方法才能够正常使用.
在上面儿的继承有提到: 父类之间可能还会有相似之处, 我们可以继续的进行抽象, 那么此时, 我们就可以使用抽象类来完成这一工作.

ABCMeta, abstractmethod源码分析:
需要注意一点的是, 不是说一个类的metaclass我们将其换成ABCMeta之后这个类就是抽象类, 不能被实例话了. 而是需要和我们的abstractmethod进行配合使用. 应用代码还是以上面儿的Product为例:

1
2
3
4
5
6
7
8
9
from abc import ABCMeta, abstractmethod

class Product(metaclass=ABCMeta):
def __init__(self, name):
self.name = name

@abstractmethod
def should_be_implement(self):
pass

那么有一个小问题: 是metaclass先创建类, 还是装饰器先对实例函数进行装饰呢?
答案是装饰器先对函数进行装饰, 然后才进入元类的类对象创建之中. 至于为什么, 现在的水平没法儿回答这个问题.

  • abstractmethod
1
2
3
def abstractmethod(funcobj):
funcobj.__isabstractmethod__ = True
return funcobj

这是一个相当简单的装饰器, 就只是给函数对象做了一个__isabstractmethod__的属性赋值, 然后没了.

  • ABCMeta
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
class ABCMeta(type):
_abc_invalidation_counter = 0

def __new__(mcls, name, bases, namespace):
# 这里首先就创建了一个正常的类对象出来
cls = super().__new__(mcls, name, bases, namespace)

# 然后在这里进行了自定义抽象类的属性字典遍历, 来获取含有__isabstractmethod__的对象, 不管是函数还是其它, 只要有这个属性, 都会被丢到abstracts这个set中, 注意a = {1, 3}, a是一个set, 而不是dict
abstracts = {name
for name, value in namespace.items()
if getattr(value, "__isabstractmethod__", False)}
for base in bases:
for name in getattr(base, "__abstractmethods__", set()):
value = getattr(cls, name, None)
if getattr(value, "__isabstractmethod__", False):
abstracts.add(name)
# frozenset, 不可变集合, 一旦创建, 无法更改
cls.__abstractmethods__ = frozenset(abstracts)
# Set up inheritance registry
# 这里是一个弱引用的key集合
cls._abc_registry = WeakSet()
cls._abc_cache = WeakSet()
cls._abc_negative_cache = WeakSet()
cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
return cls

可以看到, 该元类只是做了一些属性上的赋值, 以及抽取抽象类中的所有抽象方法, 并存入到自身的属性之中. 然而我并没有找到这些属性具体在哪个地方使用, 应该是Python在运行时的内部检查. 目前只能分析到这里了.