面向对象编程

2019-05-28  本文已影响0人  Vector_Wan

主要包括:

1. 基本概念

先来看一个例子:

class Cat(object):
    def __new__(cls, *args, **kwargs):
        # __new__ 方法主要用来分配内存,首先运行
        print("__new__")
        return super().__new__(cls)
    
    def __init__(self, color, name):
        # 构造方法,该方法会在创建对象(实例)的时候自动调用
        self.color = color
        self.name = name
        
    def catch_rat(self):
        print(self.name + '抓到了老鼠')

# 创建一个实例
tom = Cat('Blue', 'Tom')
tom.catch_rat()
__new__
Tom抓到了老鼠

class后面紧接着是类名,即Cat,类名通常使用‘驼峰法’命名,首字母大写,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

定义好了类,就可以根据Cat类创建出Cat的实例,创建实例是通过类名+()实现的。

可以自由地给一个实例变量绑定属性,比如,给实例tom绑定一个sex属性:

tom.sex = 'male'
tom.sex
'male'

由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把namecolor等属性绑上去:

这个 __init__ 方法,它是构造方法,该方法会在创建对象(实例)的时候自动调用。(是不是有点像 C++ 中的构造函数)

另外__init__方法的第一个参数永远是 self ,表示方法的调用者(有点像 this 指针的感觉),有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去。

接下来我们先简单看看实例化对象的过程

内存分析

正如上图描述的那样:

  1. 分配内存(__new__ 方法)(类也是一个对象,或者说是一个模板对象,这个我们后面再说)
  2. 初始化属性值(__init__方法)

python 中没有属性控制符,对于每一个实例我们可以修改私有属性:

print(tom.name)
tom.name = 'JiaFeiCat'
print(tom.name)
Tom
JiaFeiCat

2. 属性私有化(类的封装)

python 中没有属性控制符,这就有点难受了,外部函数不就可以随便改我的属性了吗,不要慌一般有下面两种方法控制访问:

  1. 约定:单下划线开头,不要修改,但是!!小白来了给我修改了就不爽了对不对!!还有下面强制的方法:(后面就会看到还是挡不住大佬啊,挡不住挡不住)
  2. 属性名两个下滑线开头,
class Cat:
    def __init__(self, color, name):
        # 构造方法
        self.__color = color
        self.__name = name
        
    def catch_rat(self):
        print(self.__name + '抓到了老鼠')

# 创建一个实例
tom2 = Cat('Blue', 'Tom')
print(tom2.name)
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-9-631bcdb4adc7> in <module>
     10 # 创建一个实例
     11 tom2 = Cat('Blue', 'Tom')
---> 12 print(tom2.name)


AttributeError: 'Cat' object has no attribute 'name'

ok 报错了,外部无法访问。只能自己的方法访问 or 修改。

双下滑线开头为什么可以隐藏变量呢?

我们使用__dict__来查看一下实例的所有属性:(储存在一个字典中

print(tom.__dict__)
print(tom2.__dict__)
{'color': 'Blue', 'name': 'JiaFeiCat'}
{'_Cat__color': 'Blue', '_Cat__name': 'Tom'}

从上面可以看出:双下滑线开头的属性名字会被自动转换: _类名+属性名

有一个小坏的想法我们其实还是可以通过访问 _类名+属性名 修改,(这谁能抗的住啊)

print(tom2._Cat__color)
Blue

在上面的例子中我们确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。

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

在这个例子中我们加了一个函数 set_name 用于修改对象的 name 属性,和一个用于访问的 get_name 函数。

class Student:
    '''
    学生类
    '''
    def __init__(self, name, age):
        self.__name = name
        self.__age = age
        
    def set_name(self, name):
        if not len(name)<1:
            self.__name = name
            
    def get_name(self):
        return self.__name
    
s = Student("Vector", 20)
print(s.get_name())
s.set_name("HHM")
print(s.get_name())
Vector
HHM

我们在上面的例子中是自己实现的 get 函数和 set 函数。有小可爱问,反正都要被外部访问,为啥不直接把name属性设置成公有的,其实我们手动设置外部访问函数还有一个好处就是,我们可以对传进来的参数先进行一个处理(或者说检查),

但是我们每次修改或访问属性都要调用函数,能不能有一个方法让我们像对普通变量一样进行修改和赋值,又可以进行处理呢?

可以的!装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的,@property的实现比较复杂,我们先考察如何使用。

把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作。

这个东西很好呀 如果我们把函数名字就设置成我们想要修改和访问的属性名,那么我们就可以像一个普通变量一样访问啦~(语法糖哦)

class Student:
    '''
    学生类
    '''
    def __init__(self, name, age):
        self.__name = name
        self.__age = age
    
    @property
    def age(self):
        print('get函数')
        return self.__age
    
    @age.setter
    def age(self,age):
        print('set函数')
        if age > 0:
            self_age = age
    
s = Student("Vector", 20)
s.age = 50   # 调用了 set 函数
print(s.age) # 调用了 get 函数,进行了检查。

set函数
get函数
20

@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

3. 实例属性与类属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性。

给实例绑定属性的方法是通过实例变量,或者通过self变量:

class Student(object):
    def __init__(self, name):
        self.name = name

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

但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:

class Student(object):
    name = 'Student'

当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。来测试一下:

s = Student() # 创建实例s
print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
print(Student.name) # 打印类的name属性
print('-'*20)

s.name = 'Michael' # 给实例绑定name属性
print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
print('-'*20)

del s.name # 如果删除实例的name属性
print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student
Student
--------------------
Michael
Student
--------------------
Student

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

4. 静态方法、类方法、与实例方法

静态方法是指类中无需实例参与即可调用的方法(不需要self参数),在调用过程中,无需将类实例化,直接在类之后使用.号运算符调用方法。

通常情况下,静态方法使用@staticmethod装饰器来声明。

class ClassA(object):

    @staticmethod
    def func_a():
        print('Hello Python')

if __name__ == '__main__':
    ClassA.func_a()
    # 也可以使用实例调用,但是不会将实例作为参数传入静态方法
    ca = ClassA()
    ca.func_a()
Hello Python
Hello Python

类方法在Python中使用比较少,类方法传入的第一个参数为cls,是类本身。并且,类方法可以通过类直接调用,或通过实例直接调用。但无论哪种调用方式,最左侧传入的参数一定是类本身。

通常情况下,类方法使用@classmethod装饰器来声明。

class ClassA(object):

    @classmethod
    def func_a(cls):
        print(type(cls), cls)


if __name__ == '__main__':

    ClassA.func_a() # 类调用

    ca = ClassA()
    ca.func_a() # 实例调用
<class 'type'> <class '__main__.ClassA'>
<class 'type'> <class '__main__.ClassA'>

从运行结果可以看出,无论是类调用还是实例调用,类方法都能正常工作。且通过打印cls,可以看出cls传入的都是类实例本身。

我们再来看一个实例:

class User:
    number = 0
    
    def  __init__(self, name):
        self.name = name
        User.number = User.number + 1
        
    def print_name(self):
        print(self.name)
        
    @classmethod
    def creat_user(cls, name, age):
        print(cls)
        user = cls(name)
        user.age = age
        return user
        
u2 = User.creat_user('zs', 20)
print(u2.age)
print(u2.name)
<class '__main__.User'>
20
zs

这里需要注意的是,如果存在类的继承,那类方法获取的类是类树上最底层的类。

实例方法,除静态方法与类方法外,类的其他方法都属于实例方法。

实例方法需要将类实例化后调用,如果使用类直接调用实例方法,需要显式地将实例作为参数传入。最左侧传入的参数self,是实例本身。

class User:
    number = 0
    
    def  __init__(self, name):
        self.name = name
        User.number = User.number + 1
    
    @staticmethod
    def sum(a,b):
        return a+b
    
print(User.sum(2,3))
5

以上的三种方法调用方式总结成一张表就是:

所以这么多方法到底有啥用呀??我们来看一个实例:类方法用在模拟java定义多个构造函数的情况。由于python类中只能有一个初始化方法,不能按照不同的情况初始化类。我们可以采取下面的方法:

class Book(object):
 
    def __init__(self, title):
        self.title = title
 
    @classmethod
    def class_method_create(cls, title):
        book = cls(title=title)
        return book
 
    @staticmethod
    def static_method_create(title):
        book= Book(title)
        return book
 
book1 = Book("use instance_method_create book instance")
book2 = Book.class_method_create("use class_method_create book instance")
book3 = Book.static_method_create("use static_method_create book instance")
print(book1.title)
print(book2.title)
print(book3.title)
use instance_method_create book instance
use class_method_create book instance
use static_method_create book instance

顺便提一下:在静态方法中直接使用类名,在类方法中使用cls,在实例方法中使用self

5. 继承与多态

继承

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

比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:

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

当我们需要编写DogCat类时,就可以直接从Animal类继承:

class Dog(Animal):
    pass

class Cat(Animal):
    pass

对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。CatDog类似。

继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此,DogCat作为它的子类,什么事也没干,就自动拥有了run()方法:

dog = Dog()
dog.run()

cat = Cat()
cat.run()
Animal is running...
Animal is running...

当然,也可以对子类增加一些方法,比如Dog类:

class Dog(Animal):

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

    def eat(self):
        print('Eating meat...')

继承的第二个好处需要我们对代码做一点改进。你看到了,无论是Dog还是Cat,它们run()的时候,显示的都是Animal is running...,符合逻辑的做法是分别显示Dog is running...Cat is running...,因此,对DogCat类改进如下:

class Dog(Animal):

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

class Cat(Animal):

    def run(self):
        print('Cat is running...')
        
dog = Dog()
dog.run()

cat = Cat()
cat.run()
Dog is running...
Cat is running...

当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。

多态

要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如strlistdict没什么两样。

要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:

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的实例时,run_twice()就打印出:

run_twice(Cat())
Cat is running...
Cat is running...

看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生:

class Tortoise(Animal):
    def run(self):
        print('Tortoise is running slowly...')
        
run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...

当我们调用run_twice()时,传入Tortoise的实例,你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态

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

对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在AnimalDogCat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则

对扩展开放:允许新增Animal子类;

对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树。比如如下的继承树:

动态语言的“鸭子类型”

对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。

对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了:

class Timer(object):
    def run(self):
        print('Start...')
        
run_twice(Timer())
Start...
Start...

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

Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。

动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。

关于继承的其他细节

在子类中如果想使用父类使用super()表示。

class A:
    def __init__(self):
        self.name = 'A'

class B:
    def __init__(self):
        self.name = 'B'

class C(A):
    def __init__(self):
        super().__init__()  # super 指定是 父类
        self.age = 20

c = C()
print(c.name)
print(c.age)
A
20

另外python支持多继承,继承内容与继承顺序相关,例如下面的例子中,在搜索方法时先搜索C 再搜索 B 再搜索A ,最后搜索对象。

可以使用__mro__查看搜索顺序。

class A:
    def __init__(self):
        self.name = 'A'

class B:
    def __init__(self):
        self.name = 'B'

class C(B, A):
    def __init__(self):
        super().__init__()  # super 指定是 父类
        self.age = 20

c = C()
print(c.name)
print(c.age)
B
20
print(C.__mro__)
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

6. 获取对象信息

当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?

class User:
    number = 0
    
    def  __init__(self, name):
        self.name = name
        
        
    def __str__(self):
        _str = ''
        for k ,v in self.__dict__.items():
            _str += 
        return self.__dict__

u = User('kk')
print(u)
上一篇 下一篇

猜你喜欢

热点阅读