Python中的面向对象

Python中通过class关键字定义类,object表示从哪个类继承下来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student(object):
# 通过 __init__ 方法添加初始化变量
def __init__(self, name, age):
self.name = name
self.age = age

def print_age(self):
print('age = ', self.age)

# 提供初始化属性
stu = Student('Tom', 18)
# Tom
stu.name
# age = 18
stu.print_age()

访问权限控制

Python中通过把属性的名称钱加上两个下划线__,这就表明这是一个私有变量,只有内部可以访问。有时候,可能会遇到单下划线开头的实例变量,比如:_name。这样的实例变量外部是可以访问的,但是当我们看到这样的实例变量的时候,要把它看做实例变量,不要随意访问。注意:变量名类似于 __xxx__ 这种变量是特殊变量,可以直接访问,不是私有变量。我们不能用 __name__ 这样的变量名称

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
class Student(object):
def __init__(self, name, age):
self.__name = name
self.__age = age

def print_age(self):
print_age('%s : %s', self.__name, self.__age)

# 如果想访问私有变量,可以给 __name, __age 添加可读写的接口
def get_name(self):
return self.__name

def set_name(self, name):
self.__name = name

def get_age(self):
return self.__age

def set_age(self, age):
self.__age = age

stu = Student('Tom', 18)

# 私有变量,不能直接从外部访问。
# AttributeError: 'Student' object has no attribute '__name'
stu.__name

外部不能访问__name的原因是:Python解释器将__name变量改成了_Student__name,所以仍然可以通过_Student__name来访问__name变量,不过不推荐这么做。

1
2
# Tom
stu._Student__name

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal(object):
def run(self):
print('Animal is running ...')

class Dog(Animal):
def run(self):
print('Dog is running ...')

# 当子类重写了父类的方法时,总是调用子类的方法。
dog = Dog()
# Dog is running ...
dog.run()

animal = Animal()
animal.run()

多态

由于Dog是从Animal继承而来,所以Dog可以看成为Animal类型,而Animal不能看成Dog。可以用isinstance()来判断变量是否是某个类型:

1
2
3
4
>>> isinstance(animal, Animal)
true
>>> isinstance(animal, Dog)
false

现在给Animal对象增加一个函数,接受一个Animal类型的变量:

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
def run_twice(animal):
animal.run()
animal.run()

# 当传入的对象是`Animal`的实例时,`run_twice()`就打印出:
>>> run_twice(Animal())
Animal is running ...
Animal is running ...

# 当传入的对象是`Dog`的实例时,`run_twice()`就打印出:
>>> run_twice(Dog())
Dog is running ...
Dog is running ...

# 现在增加一个 Cat 类,继承自 Animal
class Cat(Animal):
def run(self):
print('Cat is running ...')

# 当传入 Cat 实例,run_twice() 打印出:
>>> run_twice(Cat())
Cat is running ...
Cat is running ...

# 可以发现,每增加一个 Animal 的子类,不必对 run_twice() 做任何修改,任何依赖 Animal 作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。

使用多态的好处:当传入Cat或者Dog时,我们只需要接收Animal类型就可以了。因为 Cat、Dog 都是Animal类型,然后按照Animal类型进行操作,由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法。

对于一个变量,只需要知道是Animal类型,无需确切的知道其子类型,就可以放心的调用run()方法,具体是调用Dog还是Cat类型的run()方法,由运行时该对象的确切类型决定。这就是多态的厉害之处,调用方只管调用,不管细节。当我们新增一种Animal的子类时,只要确保run()方法编写正确,不管原来的代码是如何调用的。这就是著名的开闭原则

对扩展开放:允许新增Animal子类;
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

鸭子类型

鸭子类型并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。对于静态语言来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者其子类。对于Python,只要保证传入的对象有一个run()方法就可以了

1
2
3
def Timer(object):
def run(self):
print('timer is running ...')

获取对象信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 使用 type 判断对象类型
>>> type(123)
<class 'int'>

# 使用 isinstance() 判断对象是否是某种类型,或者是在继承链上
>>> isinstance(dog, Animal)
True

# 有属性'x'吗?
>>> hasattr(obj, 'x')
True

# 设置一个属性'y'
>>> setattr(obj, 'y', 19)

>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404

# 使用 dir() 获取一个对象的所有属性和方法
>>> dir('123')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

实例属性、类属性

1
2
3
4
5
6
7
8
9
10
11

class Student(object):
# 增加 name、age 类属性
name = 'Tom'
age = 18
# 通过实例变量,或者 self 变量给实例绑定属性
def __init__(self, name):
self.name = name

s = Student('Bob')
s.score = 90

类属性虽然归类所有,但是类的实例都可以访问到,实例属性千万不能与类属性同名,相同名称的实例属性会屏蔽掉类属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> s = Student()
>>> print(s.age) # 打印 name 属性,因为实例没有 age 属性,所以访问类的 age 属性
18
>>> print(Student.age) # 访问类属性
18

s.age = 30 # 绑定实例属性
>>> print(s.age) # 实例属性会屏蔽到类属性
30
>>> print(Student.age) # 类属性并没有被屏蔽,使用 Student.age 仍可以访问
18

>>> del s.age # 删除实例属性 age
>>> print(s.age) # 因为实例没有 age 属性,所以访问类的 age 属性
18

__slots__

使用__slots__可以限定实例的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student(object):
# 使用元组来限制class实例能添加的属性。
__slots__ = ('name', 'age')

s = Student()
s.name = 'Tom'
s.age = 18

# 由于 __slots__ 限定类实例属性只能绑定 name 和 age,绑定其他的属性会导致错误
# ERROR: AttributeError: 'Student' object has no attribute 'score'
try:
s.score = 90
except AttributeError as e:
print('AttributeError: ', e)

注意:使用__slots__仅对当前的类实例有效,对其子类是无效的。

__str__

__str__用来做格式化输出:

1
2
3
4
5
6
7
8
9
10
11
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name: %s)' % self.name

# 通过变量调用的方式打印对象,是通过调用 __repr__ 函数
__repr__ = __str__

>>> print(Student('Michael'))
Student object (name: Michael)

__iter__

如果一个类想被用于for ... in循环,类似于list那样,必须实现一个 __iter__() 方法,改方法返回一个迭代对象,然后,for循环就会不断的调用该对象的__iter__() 方法获取的循环的下一个值,直到遇到StopIteration错误时退出循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1

def __iter__(self):
# 实例是迭代对象,所以返回自己
return self

def __next__(self):
self.a, self.b = self.b, self.a + self.b
if self.a > 100:
raise StopIteration();
return self.a

# 由于Fib实例实现 __iter__ 方法,所以实例可以作用于 for 循环
for n in Fib():
print(n)

__getitem__

如果让一个类像数组那样支持下表运算,需要实现__getitem__()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b

# 现在可以像数组那样访问任意一项
>>> f = Fib()
>>> f[0]
1
>>> f[1]
1
>>> f[2]
2
>>> f[3]
3
...

__getattr__

当调用类的方法或属性时,如果不存在,就会报AttributeError错误,使用__getattr__(),动态返回一个属性:

1
2
3
4
5
6
7
8
9
10
11
class Student(object):
def __init__(self):
self.name = 'Tom'

def __getattr__(self, attr):
if attr=='age':
return 18

s = Student()
# 如果没有定义 __getattr__ 方法,则会报AttributeError错误
s.age

注意:在没有实现__getattr__方法时,Student类并没有定义age属性,如果访问age属性,会出现AttributeError: 'Student' object has no attribute 'age'错误。如果实现了__getattr__方法,就会动态添加属性。只有没有找到某个属性的情况下,才会调用__getattr__属性。

这里有一个利用 __getattr__设计 REST API 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Chain(object):
def __init__(self, path=''):
self._path = path

def __getattr__(self, path):
return Chain('%s/%s' % (self._path, path))

def __str__(self):
return self._path

__repr__ = __str__


>>> Chain().status.user.timeline.list
'/status/user/timeline/list'

__call__

在调用实例方法的时候,通常使用instance.method()这种方式调用。然而,也可以通过实现__call__()实现实例本身调用。

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

def __call__(self):
print('self.name = %s', self.name)

# 调用方式,直接调用实例
>>> s = Student('Tom')
>>> s()
self.name = Tom

# 判断变量是函数还是对象可以通过 Callable ,如果对象能被调用,那么这个对象就是 Callable 对象。这样就可以判断一个对象是否是可调用对象。
>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable('str')
False

MixIn

Python是支持多继承的,在不支持多继承的编程语言当中,通常需要设计复杂的继承关系。而Python可以通过MixIn这种方式,为类增加额外的功能。在编写程序的过程中,只要保证主线采取单一继承,通过MixIn来给一个类增加额外的功能。

Enum

Python中也支持Enum类:

1
2
3
4
5
6
7
8
from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

# 这样我们就获得了Month类型的枚举类。可以枚举它的所有成员:
# value 属性是自动赋给成员的 int 常量,默认为1开始
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)

也可以更精确的控制枚举类型,从Enum派生出自定义类。@unique装饰器用来保证没有重复值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from enum import Enum, unique

@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6

>>> day1 = Weekday.Mon
>>> print(day1)
Weekday.Mon
>>> print(Weekday.Tue)
Weekday.Tue
>>> print(Weekday['Tue'])
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> print(day1 == Weekday.Mon)
True


参考

Python官方文档
廖雪峰Python教程