面向对象编程

参考

面向过程: 把计算机程序视为一系列的命令集合,即一组函数的顺序执行。
为了简化程序设计,即把大块函数通过切割成小块函数来降低系统的复杂度。

面向对象编程: Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。
面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

面向对象的设计思想是抽象出Class,根据Class创建Instance。

class区别于函数的是:既要包含数据,又要包含操作数据的方法

类:抽象的模板
实例:根据类创建出来的一个个具体的对象


Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。

1. __init__ 方法

  • 第一个参数永远是self ,表示创建的实例本身
    因此, __init__ 方法内部,就可以把各种属性绑定到self ,因为self 就指向创建的实例本身。
  • 实例化的时候,self不需要传,Python解释器自己会把实例变量传进去。

2.实例化

  • 类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
  • 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;
  • 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。
  • 和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同:
>>> bart = Student('Bart Simpson', 59)
>>> lisa = Student('Lisa Simpson', 87)
>>> bart.age = 8  --age是实例化之后声明的变量,所以另一个实例不一定能拥有
>>> bart.age
8
>>> lisa.age
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'age'```

3.访问限制-私有变量
私有变量:如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。

class Student(object):

    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def print_score(self):
        print('%s: %s' % (self.__name, self.__score))
>>> bart = Student('Bart Simpson', 59)
>>> bart.__name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'

但是如果外部代码要获取name和score怎么办?可以给Student类增加get_nameget_score 这样的方法:

class Student(object):
    ...

    def get_name(self):
        return self.__name

    def get_score(self):
        return self.__score

如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score 方法:

class Student(object):
    ...

    def set_score(self, score):
        self.__score = score

注意

  • 特殊变量__xxx__ :也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name____score__ 这样的变量名。——不建议使用
  • _xx:以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
  • __xx:双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name——不同版本可能不一样,不建议直接访问,可以通过方法来获取或者设置。

4.继承和多态
4.1继承
在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。

继承的好处: 子类获得了父类的全部功能,子类实例可以直接调用父类方法。

4.2多态

子类和父类存在相同名字的方法时,子类调用这个方法的时候,子类的方法覆盖父类的方法,总是调用子类的方法。

当我们定义一个class的时候,我们实际上就定义了一种数据类型。
判断一个变量是否是某个类型可以用isinstance()判断

>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True

在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。

动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。(具体例子看参考文档)

多态真正的威力:调用方只管调用,不管细节

5.获取对象信息
当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?
5.1type()
判断对象类型

>>> type(abs)
<class 'builtin_function_or_method'> ——abs是函数、方法
>>> type(a)
<class '__main__.Animal'>   ——a是类Animal的实例化对象?

5.2isinstance()
判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。

>>> isinstance(h, Husky)
True

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

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

5.4getattr()、setattr()以及hasattr()

>>> 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

获取不存在的属性

>>> getattr(obj, 'z') # 获取属性'z'  ---如果试图获取不存在的属性,会抛出AttributeError的错误
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404

获取对象的方法

>>> 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

假设我们希望从文件流fp中读取图像,我们首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr() 就派上了用场。

def readImage(fp):
    if hasattr(fp, 'read'):
        return readData(fp)
    return None

6.实例属性和类属性

6.1实例绑定属性:

  • 实例变量
  • self变量
class Student(object):
    def __init__(self, name):
        self.name = name

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

6.2类属性

class Student(object):
    name = 'Student'

这个属性虽然归类所有,但类的所有实例都可以访问到

>>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student

注意:从上面的例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

类属性在类中引用方法如下:

class Student:
    count = 0
    def __init__(self,name):
        self.name = name
        Student.count += 1  # 类属性在类中也是 类名.类属性



class Student(object):

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

>>> bart = Student('Bart Simpson', 59)  ---实例化,self不需要传,Python解释器自己会 
>>> bart                                ---把实例变量传进去
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>
>>> bart.name = 'Bart Simpson'  ---给实例bart绑定一个name属性
>>> bart.name
'Bart Simpson'

面向对象的高级编程

1.使用__slots__

如果我们想要限制实例的属性,比如只允许对Student实例添加nameage 属性/变量
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__ 变量,来限制该class实例能添加的属性:

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称

做下实验

>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

注意: 使用__slots__ 要注意,__slots__ 定义的属性仅对当前类实例起作用,对继承的子类是不起作用的

2.使用property ——decorator

Python内置的@property 装饰器就是负责把一个方法变成属性(变量)调用的

class Student(object):

    @property    ---一个getter方法变成属性,只需要加上@property就可以了
    def score(self):
        return self._score

    @score.setter            —@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法
    def score(self, value):   ---变成属性赋值
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

测试

>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

3.多重继承 MixIn

如果需要“混入”额外的功能,通过多重继承就可以实现。

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

4.定制类

这种形如__xxx__ 的变量或者函数名,都是python中有特殊用途的。Python的class中这样有特殊用途的函数,可以帮助我们定制类。

(1) __slots__ 限制实例属性
(2) __len__ 让class作用于len()函数
(3)__str__ 方法 打印实例

>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return 'Student object (name: %s)' % self.name
...
>>> print(Student('Michael'))
Student object (name: Michael)
>>> s = Student('Michael')
>>> s
<__main__.Student object at 0x109afb310>  --这里使用的是 __repr__()

(4) __repr__
通常__str__()__repr__() 代码都是一样的,所以,有个偷懒的写法:

class Student(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Student object (name=%s)' % self.name
    __repr__ = __str__

(5)__iter__
如果一个类想被用于for ... in 循环,类似list或tuple那样,就必须实现一个__iter__() 方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__() 方法拿到循环的下一个值,直到遇到StopIteration 错误时退出循环。
我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:

class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1 # 初始化两个计数器a,b

    def __iter__(self):
        return self # 实例本身就是迭代对象,故返回自己

    def __next__(self):
        self.a, self.b = self.b, self.a + self.b # 计算下一个值
        if self.a > 100000: # 退出循环的条件
            raise StopIteration()
        return self.a # 返回下一个值

现在,试试把Fib实例作用于for循环:

>>> for n in Fib():
...     print(n)
...
1
1
2
3
5
...
46368
75025

(6)__getitem__ 索引取元素 切片
Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行。

class Fib(object):
    def __getitem__(self, n):
        if isinstance(n, int): # n是索引
            a, b = 1, 1
            for x in range(n):
                a, b = b, a + b
            return a
        if isinstance(n, slice): # n是切片(未对负数做处理,步长也没有)
            start = n.start
            stop = n.stop
            if start is None:
                start = 0
            a, b = 1, 1
            L = []
            for x in range(stop):
                if x >= start:
                    L.append(a)
                a, b = b, a + b
            return L

__setitem__() 把对象视作list或dict来对集合赋值
__delitem__() 用于删除某个元素
(7)__getattr__
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。

class Student(object):
    
    def __init__(self):
        self.name = 'Michael'
>>> s = Student()
>>> print(s.name)
Michael
>>> print(s.score)     --不存在score属性
Traceback (most recent call last):
  ...
AttributeError: 'Student' object has no attribute 'score'  --没有找到score这个attribute。

要避免这个错误,除了可以加上一个score 属性外,Python还有另一个机制,那就是写一个__getattr__() 方法,动态返回一个属性。修改如下:

class Student(object):

    def __init__(self):
        self.name = 'Michael'

    def __getattr__(self, attr):   
        if attr=='score':
            return 99

当调用不存在的属性时,比如score ,Python解释器会试图调用__getattr__(self, 'score') 来尝试获得属性,这样,我们就有机会返回score 的值:

>>> s = Student()
>>> s.name
'Michael'
>>> s.score
99

注意,只有在没有找到属性的情况下,才调用__getattr__ ,已有的属性,比如name ,不会在__getattr__ 中查找。

5.使用枚举类

当我们需要定义常量时,一个办法是用大写变量通过整数来定义, 好处是简单,缺点是类型是int ,并且仍然是变量。
解决办法:
1.方式一
枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum 类来实现这个功能:

from enum import Enum

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

这样我们就获得了Month 类型的枚举类,可以直接使用Month.Jan 来引用一个常量,或者枚举它的所有成员:

Month.Jan

for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value)  --value属性则是自动赋给成员的int常量
                                                 -- 默认从1开始计数。

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

from enum import Enum, unique

@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

6.使用元类

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

6.1 type() 动态创建类
两种方式:

  • 查看类、函数类型
  • 动态创建类

type() 函数可以查看一个类型或变量的类型

class Hello(object):
    def hello(self, name='world'):
        print('Hello, %s.' % name)
>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))   
<class 'type'>                      ---Hello是一个class,它的类型就是type
>>> print(type(h))
<class 'hello.Hello'>            ---- 而h是一个实例,它的类型就是class Hello

创建class的方法就是使用type() 函数
type() 函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type() 函数创建出Hello 类,而无需通过class Hello(object)... 的定义:

>>> def fn(self, name='world'): # 先定义函数
...     print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

要创建一个class对象,type()函数依次传入3个参数:

  1. class的名称;
  2. 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
  3. class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。

通过type() 函数创建的类和直接写class是完全一样的

6.2metaclass 元类 ——控制类的创建行为 基本不会用到

先定义metaclass,就可以创建类,最后创建实例。

所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。

总会遇到需要通过metaclass修改类定义的。ORM就是一个典型的例子。

ORM全称“Object Relational Mapping”,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。

要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
来源