《Python高级编程》学习心得——第四章 深入类和对象
总览

鸭子类型和多态
Java中多态是通过继承实现的,子类继承父类(或接口),重写父类(或接口)的方法,从而实现多态。而在Python中,一个对象从本质上来说是一个字典的封装,在该字典中,每个key是一个属性(或方法),每个value是属性的值或者方法的实现。因此,在Python中,一个对象的类型本质上是由其属性和方法决定的,比如一个类如果实现了__next__方法和__iter__方法,那该类的对象属于迭代器类型。
下面用一个例子展示Java和Python实现多态方式的不同:
Java多态
public class Main
{
interface Animal {
public void run();
}
class Dog implements Animal {
@Override
public void run()
{
System.out.println("A dog is running");
}
}
class Cat implements Animal {
@Override
public void run()
{
System.out.println("A cat is running");
}
}
public void outerMethod()
{
Animal[] animals = new Animal[2];
animals[0] = new Dog();
animals[1] = new Cat();
for (Animal animal : animals)
{
animal.run();
}
}
public static void main(String[] args)
{
Main main = new Main();
main.outerMethod();
}
}
执行结果
A dog is running
A cat is running
Python多态
class Dog:
def run(self):
print ("A dog is running")
class Cat:
def run(self):
print ("A cat is running")
if __name__ == '__main__':
animals = (Dog(), Cat())
for animal in animals:
animal.run()
执行结果
A dog is running
A cat is running
抽象基类(abc模块)
Python的抽象基类类似于Java的接口interface的作用,但与interface不同的是,Python的抽象基类可以定义非抽象的方法。类似类方法(@classmethod)和静态方法(@staticmethod),Python中抽象方法也是用装饰器修饰定义。
视频中老师说为了避免过度设计,Python中不推荐使用抽象基类。但是通过阅读源码,我们可以发现Python源代码中使用到了抽象基类,理解抽象基类有助于我们阅读、学习Python源码。
下面用一个例子来看抽象基类的定义和使用。
import abc
class Animal(metaclass=abc.ABCMeta):
@abc.abstractmethod
def run(self):
pass
class Dog(Animal):
"""
If class Dog does not implements abstract method run, TypeError will be raised
"""
pass
if __name__ == '__main__':
dog = Dog()
执行结果
Traceback (most recent call last):
File "AbstractClass.py", line 15, in <module>
dog = Dog()
TypeError: Can't instantiate abstract class Dog with abstract methods run
但其实用NotImplementedError也可以限制Dog类必须实现run方法,只不过Dog类仍能实例化,报错会发生在Dog类实例调用run方法时。
class Animal:
def run(self):
raise NotImplementedError
class Dog(Animal):
pass
if __name__ == '__main__':
dog = Dog()
dog.run()
执行结果
Traceback (most recent call last):
File "AbstractClass.py", line 27, in <module>
dog.run()
File "AbstractClass.py", line 18, in run
raise NotImplementedError
NotImplementedError
使用isinstance而不是type
- isinstance: 子类对象仍是父类的实例
- type: 子类对象的类型仅仅是子类,而不能是父类
class Animal:
def run(self):
raise NotImplementedError
class Dog(Animal):
pass
if __name__ == '__main__':
dog = Dog()
print (isinstance(dog, Animal))
print (type(dog) is Animal)
执行结果
True
False
类属性和对象属性及其查找顺序
类属性和对象属性及其覆盖问题比较简单,仅用下面一个例子说明:
class A:
aa = 1 # class property
def __init__(self, x, y):
self.x = x # object property
self.y = y # object property
if __name__ == '__main__':
a = A(2,3)
b = A(4,5)
a.aa = 11
print(a.aa)
print(b.aa)
print(A.aa)
执行结果
11
1
1
在多继承时,属性的查找顺序遵循MRO(method resolution order). 在Python2中有经典类和新式类之分,而在Python3中,不管类定义时是否显式地继承了object, 都是新式类。新式类的MRO算法是C3算法,具体算法比较复杂,可以简单理解C3算法既不同于BFS,也不同于DFS,总的原则是优先左边的父类和优先浅层的父类. 作为属性/方法的一个特例,super关键字在调用父类的__init__方法时也遵循MRO的顺序。实际上,super不是调用父类的__init__方法,而是调用__mro__中下一个类的__init__方法。下面用两个例子来说明Python3的MRO:
Case1: 菱形继承
class A:
def __init__(self):
print('A')
class B(A):
def __init__(self):
super().__init__()
print('B')
class C(A):
def __init__(self):
super().__init__()
print('C')
class D(B, C):
def __init__(self):
super().__init__()
print('D')
if __name__ == '__main__':
d = D()
print(D.__mro__)
执行结果
A
C
B
D
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
解释:MRO为D->B->C->A->object. D的super调用B的__init__, B的super调用C的__init__, C的super调用A的__init__.
Case2: 树形继承
class A:
def __init__(self):
print('A')
class C(A):
def __init__(self):
super().__init__()
print('C')
class B:
def __init__(self):
print('B')
class D(B):
def __init__(self):
super().__init__()
print('D')
class E(C, D):
def __init__(self):
super().__init__()
print('E')
if __name__ == '__main__':
e = E()
print(E.__mro__)
执行结果
A
C
E
(<class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.B'>, <class 'object'>)
解释:MRO为E->C->A->D->B->object. E的super调用C的__init__, C的super调用A的__init__.
如果想要在子类E的__init__中同时调用所有父类的__init__,则可以改写class E为:
class E(C, D):
def __init__(self):
C.__init__(self)
D.__init__(self)
# super().__init__()
print('E')
执行结果
A
C
B
D
E
(<class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.B'>, <class 'object'>)
静态方法,类方法及对象方法
这部分笔者已经比较熟悉了,就简单说一下。静态方法、类方法及对象方法不太好拿Java类比,可能和C++做比较牵强的类比(并不完全相等,在命名空间等方面有所不同):
C++ | Python |
---|---|
全局函数 | 静态方法(@staticmethod修饰) |
类方法(static关键字修饰) | 类方法(@classmethod修饰) |
实例方法 | 对象方法 |
写一个Python的类方法的常见使用场景。众所周知,Python是不支持函数重载的(为什么Python不支持函数重载,又可以引出一个话题,在此不表)。因此,一种支持多个构造函数的迂回方案就可以利用类方法实现:
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __str__(self):
return str(self.year) + '-' + str(self.month) + '-' + str(self.day)
@classmethod
def from_str(cls, date_str):
year, month, day = date_str.split('-')
return cls(int(year), int(month), int(day))
if __name__ == '__main__':
day1 = Date(2019, 3, 14)
print(day1)
day2 = Date.from_str('2019-03-14')
print(day2)
注:cls和self一样,是类方法中大家约定俗成的第一个参数,但不是Python关键字
执行结果
2019-3-14
2019-3-14
数据封装和私有属性
Python没有数据访问控制权限关键字,Python类的所有属性和方法默认都是public权限,约定俗成地,用_(一条下划线)开头表示protected, 用__(两条下划线)开头表示private.
class A:
def __init__(self):
self.__a = 1
self._b = 2
def getA(self):
return self.__a
def getB(self):
return self._b
class B(A):
def __init__(self):
super().__init__()
self._b = 3
if __name__ == '__main__':
b = B()
print('a: {}, b: {}'.format(b.getA(), b.getB()))
可以正常执行,而直接访问私有属性
则会报错:
Traceback (most recent call last):
File "private.py", line 22, in <module>
print(b.__a)
AttributeError: 'B' object has no attribute '__a'
实际上,私有属性是可以通过"_classname__attribute"的方式直接访问的
print(b._A__a)
>> 1
由此可见,Python的访问权限控制并不严格,只是在编译的时候用了一个属性重命名的小技巧。
Python对象的自省机制
用"对象.__dict__"或"dir(对象)"可以得到对象/类的属性/方法列表。其中"对象.__dict__"返回对象相对于父类新增的(说法不严谨,望大神指正)属性名和属性值的字典,"dir(对象)"返回对象的所有属性列表(比__dict__更全,但没有值).
>>> class A:
... def __init__(self, x):
... self.x = x
...
>>>
>>> a = A(1)
>>> dir(a)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'x']
>>> dir(A)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> a.__dict__
{'x': 1}
>>> A.__dict__
mappingproxy({'__module__': '__main__', '__init__': <function A.__init__ at 0x000001DC7FA29620>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None})
with语句和上下文管理
首先视频讲了try-except-finally三部曲的一些语法规定,这里Python的try-except-finally的语法和Java的try-catch-finally一样,这里就不再赘述了。
with关键字是对try-except-finally三部曲的改进。在申请和释放资源(打开/关闭文件、数据库、socket连接)时,常用with关键字。
进一步地,with+一个符合上下文管理协议的对象+as+对象别名,构成上下文管理器的正确语法。实现了__enter__和__exit__的类是符合上下文管理协议的类。以我们常用的with关键字操作文件为例,用open创建了文件对象file后,dir(file)可以看到,file是实现了"__enter__“和”__exit__"方法的,因此file是一个上下文管理器。
>>> file = open('Main.java', 'r')
>>> dir(file)
['_CHUNK_SIZE', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', '_finalizing', 'buffer', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'line_buffering', 'mode', 'name', 'newlines', 'read', 'readable', 'readline', 'readlines', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'writelines']
>>> file.close()