文章

05 Python面向对象

05 Python面向对象

Python class 基础

定义 Python class

Python 中可以使用 class 关键字定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student(object):

    # __init__是一个特殊方法用于在创建对象时进行初始化操作
    # 通过这个方法我们可以为学生对象绑定name和age两个属性
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def study(self, course_name):
        print('%s正在学习%s.' % (self.name, course_name))

    # PEP 8要求标识符的名字用全小写多个单词用下划线连接
    # 但是部分程序员和公司更倾向于使用驼峰命名法(驼峰标识)
    def watch_movie(self):
        if self.age < 18:
            print('%s只能观看《熊出没》.' % self.name)
        else:
            print('%s正在观看岛国爱情大电影.' % self.name)

Python class 属性

  • 直接定义类属性
1
2
class Programer9(object):
    sex = 'male';
  • 在构造函数中定义
1
2
3
4
class Programer9(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

访问可见性问题

在 Python 中,属性和方法的访问权限只有两种,也就是公开的和私有的,如果希望属性是私有的,在给属性命名时可以用两个下划线 __ 作为开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test:

    def __init__(self, foo):
        self.__foo = foo # 私有属性

    def __bar(self): # 私有方法
        print(self.__foo)
        print('__bar')


def main():
    test = Test('hello')
    # AttributeError: 'Test' object has no attribute '__bar'
    test.__bar()
    # AttributeError: 'Test' object has no attribute '__foo'
    print(test.__foo)


if __name__ == "__main__":
    main()

Python 并没有从语法上严格保证私有属性或方法的私密性,它只是给私有的属性和方法换了一个名字来妨碍对它们的访问,事实上如果你知道更换名字的规则仍然可以访问到它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Test:

    def __init__(self, foo):
        self.__foo = foo

    def __bar(self):
        print(self.__foo)
        print('__bar')


def main():
    test = Test('hello')
    test._Test__bar() # _Test__bar()
    print(test._Test__foo)


if __name__ == "__main__":
    main()

在实际开发中,我们并不建议将属性设置为私有的,因为这会导致子类无法访问。
所以大多数 Python 程序员会遵循一种命名惯例就是让属性名以单下划线开头来表示属性是受保护的,本类之外的代码在访问这样的属性时应该要保持慎重,这种做法并不是语法上的规则,单下划线开头的属性和方法外界仍然是可以访问的,所以更多的时候它是一种暗示或隐喻

  • 单下划线 _,约定的受保护的,外部也可以访问
  • 双下划线 __,强制私有的

示例:定义一个类描述平面上的点并提供移动点和计算到另一个点距离的方法。

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
54
55
56
57
from math import sqrt


class Point(object):

    def __init__(self, x=0, y=0):
        """初始化方法
        
        :param x: 横坐标
        :param y: 纵坐标
        """
        self.x = x
        self.y = y

    def move_to(self, x, y):
        """移动到指定位置
        
        :param x: 新的横坐标
        "param y: 新的纵坐标
        """
        self.x = x
        self.y = y

    def move_by(self, dx, dy):
        """移动指定的增量
        
        :param dx: 横坐标的增量
        "param dy: 纵坐标的增量
        """
        self.x += dx
        self.y += dy

    def distance_to(self, other):
        """计算与另一个点的距离
        
        :param other: 另一个点
        """
        dx = self.x - other.x
        dy = self.y - other.y
        return sqrt(dx ** 2 + dy ** 2)

    def __str__(self):
        return '(%s, %s)' % (str(self.x), str(self.y))


def main():
    p1 = Point(3, 5)
    p2 = Point()
    print(p1)
    print(p2)
    p2.move_by(-1, 2)
    print(p2)
    print(p1.distance_to(p2))


if __name__ == '__main__':
    main()

setter/getter

@property 装饰器

之前的建议是将属性命名以单下划线 _ 开头,通过这种方式来暗示属性是受保护的,不建议外界直接访问,那么如果想访问属性可以通过属性的 getter(访问器)和 setter(修改器)方法进行对应的操作。如果要做到这点,就可以考虑使用 @property 包装器来包装 getter 和 setter 方法,使得对属性的访问既安全又方便

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
class Person(object):

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

    # 访问器 - getter方法
    @property
    def name(self):
        return self._name

    # 访问器 - getter方法
    @property
    def age(self):
        return self._age

    # 修改器 - setter方法
    @age.setter
    def age(self, age):
        self._age = age

    def play(self):
        if self._age <= 16:
            print('%s正在玩飞行棋.' % self._name)
        else:
            print('%s正在玩斗地主.' % self._name)


def main():
    person = Person('王大锤', 12)
    person.play()
    person.age = 22
    person.play()
    person._name = '白元芳1' # ok
    person.name = '白元芳2'  # AttributeError: can't set attribute AttributeError: property 'name' of 'Person' object has no setter


if __name__ == '__main__':
    main()

Python 类的方法

构造 和 析构方法

  • 构造函数
1
def __init__(self,[...):
  • 析构函数
1
def __del__(self,[...):

案例

1
2
3
4
5
6
class Test:
    def __init__(self):
        print "Test init"

    def __del__(self):
        print "Test del"

对象方法

在类中定义的普通方法

静态方法

静态方法:通过 @staticmethod 修饰的方法,不属于类的对象

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
from math import sqrt

class Triangle(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    @staticmethod
    def is_valid(a, b, c):
        return a + b > c and b + c > a and a + c > b

    def perimeter(self):
        return self._a + self._b + self._c

    def area(self):
        half = self.perimeter() / 2
        return sqrt(half * (half - self._a) *
                    (half - self._b) * (half - self._c))
def main():
    a, b, c = 3, 4, 5
    # 静态方法和类方法都是通过给类发消息来调用的
    if Triangle.is_valid(a, b, c):
        t = Triangle(a, b, c)
        # print(t.perimeter())
        # 也可以通过给类发消息来调用对象方法但是要传入接收消息的对象作为参数
        print(Triangle.perimeter(t))
        print(t.area())
        # print(Triangle.area(t))
    else:
        print('无法构成三角形.')


if __name__ == '__main__':
    main()

类方法

Python 还可以在类中定义类方法,类方法的第一个参数约定名为 cls ,它代表的是当前类相关的信息的对象(类本身也是一个对象,有的地方也称之为类的 元数据对象 ),通过这个参数我们可以获取和类相关的信息并且可以创建出类的对象

@classmethod 修饰的方法

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
from time import time, localtime, sleep


class Clock(object):
    """数字时钟"""

    def __init__(self, hour=0, minute=0, second=0):
        self._hour = hour
        self._minute = minute
        self._second = second

    @classmethod
    def now(cls):
        ctime = localtime(time())
        print(type(cls))
        return cls(ctime.tm_hour, ctime.tm_min, ctime.tm_sec)

    def run(self):
        """走字"""
        self._second += 1
        if self._second == 60:
            self._second = 0
            self._minute += 1
            if self._minute == 60:
                self._minute = 0
                self._hour += 1
                if self._hour == 24:
                    self._hour = 0

    def show(self):
        """显示时间"""
        return '%02d:%02d:%02d' % \
               (self._hour, self._minute, self._second)


def main():
    # 通过类方法创建对象并获取系统时间
    clock = Clock.now()
    while True:
        print(clock.show())
        sleep(5)
        clock.run()


if __name__ == '__main__':
    main()

获取对象信息

type() 判断对象类型

  • 基本类型
1
2
3
4
5
6
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
  • 函数或者类
1
2
3
4
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>
  • type() 函数返回对应的 Class 类型
1
2
3
4
5
6
7
8
9
10
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type('123')
True
>>> type('abc')==str
True
>>> type('abc')==type(123)
False
  • 基本类型可以用 intstr,判断对象类型,可以使用 types 模块中定义的常量
    • types.FunctionType 函数类型
    • types.BuiltinFunctionType 内置函数类型
    • types.LambdaType lambda 类型
    • types.GeneratorType Generator 类型
1
2
3
4
5
6
7
8
9
10
11
12
>>> import types
>>> def fn():
...     pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

isinstance() 类型判断(包括继承关系)

对于 class 的继承关系来说,使用 type() 就很不方便。我们要判断 class 的类型,可以使用 isinstance(obj, class) 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
object -> Animal -> Dog -> Husky

>>> a = Animal()
>>> d = Dog()
>>> h = Husky()

>>> isinstance(h, Husky)
True
>>> isinstance(h, Dog)
True
>>> isinstance(h, Animal)
True
>>> isinstance(d, Husky)
False

能用 type() 判断的基本类型也可以用 isinstance() 判断:

1
2
3
4
5
6
>>> isinstance('a', str)
True
>>> isinstance(123, int)
True
>>> isinstance(b'a', bytes)
True

并且还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是 list 或者 tuple:

1
2
3
4
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True

总是优先使用 isinstance() 判断类型,可以将指定类型及其子类 “ 一网打尽 “。

dir()

获得一个对象的所有属性和方法,可以使用 dir() 函数,它返回一个包含字符串的 list

1
2
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']

仅仅把属性和方法列出来是不够的,配合 getattr()setattr() 以及 hasattr() ,我们可以直接操作一个对象的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> class MyObject(object):
...     def __init__(self):
...         self.x = 9
...     def power(self):
...         return self.x * self.x
...
>>> obj = MyObject()

>>> hasattr(obj, 'x') # 有属性'x'吗?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有属性'y'吗?
False
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') # 有属性'y'吗?
True
>>> getattr(obj, 'y') # 获取属性'y'
19
>>> obj.y # 获取属性'y'
19

如果试图获取不存在的属性,会抛出 AttributeError 的错误:

```»> getattr(obj, ‘z’) # 获取属性’z’ Traceback (most recent call last): File “", line 1, in AttributeError: 'MyObject' object has no attribute 'z'

1
2
3
可以传入一个 default 参数,如果属性不存在,就返回默认值:

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

也可以获得对象的方法:

1
2
3
4
5
6
7
8
9
>>> hasattr(obj, 'power') # 有方法'power'吗?
True
>>> getattr(obj, 'power') # 获取方法'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取方法'power'并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81

slots

Python 是一门动态语言。通常,动态语言允许我们在程序运行时给对象绑定新的属性或方法,当然也可以对已经绑定的属性和方法进行解绑定。但是如果我们需要限定自定义类型的对象只能绑定某些属性,可以通过在类中定义 __slots__ 变量来进行限定。需要注意的是 slots 的限定只对当前类的对象生效,对子类并不起任何作用。

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
class Person(object):

    # 限定Person对象只能绑定_name, _age和_gender属性
    __slots__ = ('_name', '_age', '_gender')

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

    @property
    def name(self):
        return self._name

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, age):
        self._age = age

    def play(self):
        if self._age <= 16:
            print('%s正在玩飞行棋.' % self._name)
        else:
            print('%s正在玩斗地主.' % self._name)


def main():
    person = Person('王大锤', 22)
    person.play()
    person._gender = ''
    # AttributeError: 'Person' object has no attribute '_is_gay'
    # person._is_gay = True

Python 类继承

抽象类

包含了 @abstractmethod 修饰的方法 的类就是抽象类

继承

1
2
3
class 类名(要继承的父类):
    xxx
    xxx

示例:

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
from abc import ABCMeta, abstractmethod


class Pet(object, metaclass=ABCMeta):
    """宠物"""

    def __init__(self, nickname):
        self._nickname = nickname

    @abstractmethod
    def make_voice(self):
        """发出声音"""
        pass


class Dog(Pet):
    """"""

    def make_voice(self):
        print('%s: 汪汪汪...' % self._nickname)


class Cat(Pet):
    """"""

    def make_voice(self):
        print('%s: 喵...喵...' % self._nickname)


def main():
    pets = [Dog('旺财'), Cat('凯蒂'), Dog('大黄')]
    for pet in pets:
        pet.make_voice()


if __name__ == '__main__':
    main()

多继承

Python 支持多继承,用逗号 , 隔开

1
2
class Bat(Mammal, Flyable):
    pass

MixIn

MixIn 的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个 MixIn 的功能,而不是设计多层次的复杂的继承关系。

Python 自带的很多库也使用了 MixIn。举个例子,Python 自带了 TCPServerUDPServer 这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由 ForkingMixInThreadingMixIn 提供。通过组合,我们就可以创造出合适的服务来。

这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。

比如,编写一个多进程模式的 TCP 服务,定义如下:

1
2
class MyTCPServer(TCPServer, ForkingMixIn):
    pass

编写一个多线程模式的 UDP 服务,定义如下:

1
2
class MyUDPServer(UDPServer, ThreadingMixIn):
    pass

如果你打算搞一个更先进的协程模型,可以编写一个 CoroutineMixIn

1
2
class MyTCPServer(TCPServer, CoroutineMixIn):
    pass

枚举类

枚举类型,每个常量都是 class 的一个唯一实例。Python 提供了 Enum 类来实现这个功能:

1
2
3
from enum import Enum

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

可以直接使用 Month.Jan 来引用一个常量,或者枚举它的所有成员:

1
2
for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value)

value 属性则是自动赋给成员的 int 常量,默认从 1 开始计数。

如果需要更精确地控制枚举类型,可以从 Enum 派生出自定义类:

1
2
3
4
5
6
7
8
9
10
11
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

@unique 装饰器可以帮助我们检查保证没有重复值。

访问这些枚举类型可以有若干种方法:既可以用成员名称引用枚举常量,又可以直接根据 value 的值获得枚举常量。

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
>>> 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
>>> print(day1 == Weekday.Tue)
False
>>> print(Weekday(1))
Weekday.Mon
>>> print(day1 == Weekday(1))
True
>>> Weekday(7)
Traceback (most recent call last):
  ...
ValueError: 7 is not a valid Weekday
>>> for name, member in Weekday.__members__.items():
...     print(name, '=>', member)
...
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat
本文由作者按照 CC BY 4.0 进行授权