最简单的Python面对对象攻略来啦

本文详细介绍了Python的面向对象编程,涵盖了类的实例化、封装、构造器、析构器、作用域、属性和方法、类方法、静态方法、私有方法、`property`装饰器、继承、多态、运算符重载等核心概念。通过实例展示了如何创建和使用类,以及如何通过`__init__`、`__del__`、`__getattribute__`等特殊方法实现对象的初始化和销毁。文章还讨论了类装饰器、上下文管理、描述器和元编程,提供了实现`contextlib.contextmanager`、`super`等工具的示例。此外,文章强调了Python中类的创建与销毁,包括新式类和经典类的区别,以及如何创建独一无二的对象。
摘要由CSDN通过智能技术生成

面向对象是一种编程范式。范式,可以认为是一组方法论,编程范式是一组如何组织代码的方法论。主流的编程范式有:PP、IP、FP、LP、OOP、AOP。

  • PP:面向过程,代表是 C 语言;
  • IP:面向指令,代表是汇编;
  • FP:函数式编程,把世界抽象成一个个的函数,它要求的是无副作用,即相同的输入会产生相同的输出。因此它是一种无副作用的过程;
  • LP:面向逻辑的编程,把世界抽象成与或非,代表是 prolog;
  • AOP:面向方面,它用来解决一类问题,Python 中的装饰器就是 AOP 的思想。代表是 SQL 语句;

OOP 就是今天的主角,它的世界观为:

  • 世界由对象组成;
  • 对象具有运动规律和内部状态;
  • 对象之间可以相互作用。

以目前人类的认知来说,OOP 是最接近真实世界的编程范式。

设计大型软件时,面向对象比面向过程更容易实现。程序由指令加数据组成,代码可以选择以指令为核心或以数据为核心进行编写:

  • 以指令为核心:围绕“正在发生什么”进行编写。这是面向过程编程,程序具有一系列线性步骤,主体思想是代码作用于数据;
  • 以数据为核心:围绕“将影响谁”进行编写。这就是面向对象编程(OOP),围绕数据及为数据严格定义的接口来组织程序,用数据控制对代码的访问。

类和对象是面向对象中的两个重要概念。

  • 类:是对事物的抽象,如人类、球类等。类有静态属性和静态方法,类无法访问动态属性和动态方法;
  • 对象:是类的一个实例,如足球、篮球。对象有动态属性和动态方法,对象可以访问静态属性和静态方法;

面向对象的特性:

  1. 唯一性:对象是唯一的,不存在两个相同的对象,除非他们是同一个对象。就像我作为一个对象,世界上只有一个我;
  2. 分类性:对象是可分类的,比如动物、食物。

实例说明:球类可以对球的特征和行为进行抽象,然后可以实例化一个真实的球实体出来。比如我们对人类进行实例化,可以实例化出张三、李四、王五等。

所有编程语言的最终目的都是提供一种抽象方法。在机器模型("解空间"或“方案空间”)与实际解决的问题模型(“问题空间”)之间,程序员必须建立一种联系。面向对象是将问题空间中的元素以及它们在解空间中的表示物抽象为对象,并允许通过问题来描述问题而不是方案。可以把实例想象成一种新型变量,它保存着数据,但可以对自身的数据执行操作。

类型由状态集合(数据)和转换这些状态的操作集合组成。

类是抽象的概念,实例才是具体的。但是要先设计类,才能完成实例化。类是定一个多个同一类型对象共享的结构和行为(数据和代码)。就像 list 就是一种类型,使用 list. 然后就可tab补全一堆方法,但是我们使用 list.pop() 是会报错的,因为它是一个抽象的概念。

类内部包含数据和方法这两个核心,二者都是类成员。其中数据被称为成员变量或实例变量;而方法被称为成员方法,它被用来操纵类中的代码,用于定义如何使用这些成员变量。因此一个类的行为和接口是通过方法来定义的。

方法和变量就是数据和代码,如果是私有的变量和方法,只能够在实例内部使用。如果是公共的,可以在实例外部调用。

在面对对象的程序设计中,所有的东西都是对象,我们尽可能把所有的东西都设计为对象。程序本身也就是一堆对象的集合,如果要有对象,事先要有类,如果没有类,用户就要自定义类,所以用户自己写的类就成为自定义类型。也就是说如果程序里面有类,那我们就可以直接创建实例,如果没有那我们就要创建,然后实例化。程序的运行过程就是这些对象彼此之间互相操作的过程,通过消息传递,各对象知道自己该做什么。如果传递消息?每个对象都有调用接口,也就是方法,我们向方法传递一个参数就表示我们调用了该对象的方法,通过参数传递消息。从这个角度来讲,消息就是调用请求。

每个对象都有自己的存储空间,并可容纳其他对象。比如我们定义列表l1,里面有三个元素,那l1是对象,三个元素也是对象。通过封装现有对象,我们可以制作新型对象。每个对象都属于某一个类型,类型即为类,对象是类的实例。类的一个重要特性为“能发什么样的消息给它”,同一个类的所有对象都能接受相同的消息。类的消息接口就是它提供的方法,我们使用l1.pop(),就相当于给类发送了消息。而不同的类消息的接口并不相同,就像我们不能对字串类型使用pop方法一样。

定义一个类后,可以根据需要实例化出多个对象,如何利用对象完成真正有用的工作?必须有一种办法能向对象发出请求,令其做一些事情。这就是所谓的方法,这些方法加起来就表现为该类的接口。因此每个对象仅能接受特定的请求,对象的“类型”或“类”规定了它的接口类型。

数据保存在变量中,变量就是所谓的属性,方法就是函数。

类间的关系:

  • 依赖("uses-a"):一个类的方法操纵另一个类的对象;
  • 聚合("has-a"):类 A 的对象包含类 B 的对象;
  • 继承("is-a"):描述特殊与一般关系。

面对对象的特征:

  • 封装(Encapsulation):隐藏实现方案细节,并将代码及其处理的数据绑定在一起的一种编程机制,用于保证程序和数据不受外部干扰且不会被误用。类把需要的变量和函数组合在一起,这种包含称为封装。比如 list.pop() 实现的细节我们并不知道,这就是一种封装;
  • 继承(Inheritance):一个对象获得另一个对象属性的过程,用于实现按层分类的概念。一个深度继承的子类继承了类层次中它的每个祖先的所有属性,因此便有了超类、基类、父类(都是上级类)以及子类、派生类(继承而来);
  • 多态(Polymorphism):允许一个接口被多个通用的类动作使用的特性,具体使用哪个动作于应用场合相关。一个接口多种方法。意思是,同样是 x+y,如果 xy 都是数字,那就是从数学运算;如果 xy 是字串,那就是字串连接;如果是列表,则是列表连接,这就是一个接口多种方法。用于为一组相关的动作设计一个通用的接口,以降低程序复杂性。

在几乎所有支持面向对象的语言中,都有 class 关键字,并且这个关键字和面向对象息息相关。而在 Python 中,通过 class 关键字来定义一个类。

我们定义了一个类之后,只要在程序中执行了class class_name,就会在内存中生成以这个类名被引用的对象。但是类中的代码并不会真正执行,只有在实例化时才会被执行。里面的方法也不会执行,只有对实例执行方法时才会执行。类是对象,类实例化出来的实例也是对象,叫实例对象。因此类包含了类对象和实例对象,类对象是可以调用的对象,而实例对象只能调用实例中的方法。

>>> type(list)
<type 'type'>
>>> l1 = [1, 2, 3]
>>> type(l1)
<type 'list'>

list 是类,l1 是类实例化后的对象。

实例化

创建对象的过程称之为实例化。当一个对象被创建后,包含三个方面的特性:对象句柄、属性和方法。句柄用于区分不同的对象,对象的属性和方法与类中的成员变量和成员函数对应。

定义一个最简单的类:

>>> class TestClass():
...   pass
... 
>>> type(TestClass)
<type 'classobj'> # 类对象

调用这个类,让其实例化一个对象:

>>> obj1 = TestClass() # 这就是实例化的过程
>>> type(obj1)
<type 'instance'> # 这是一个实例

通过 obj1 = TestClass() 实例化了一个对象,之所以在类名后加上括号表示执行这个类中的构造器,也就是类中的 __init__ 方法。其实就和函数名后面加上括号表示执行这个函数是一样的道理。

从上面可以看出实例初始化是通过调用类来创建实例,语法为:

instance = ClassName(args…)

Python 中,class 语句类似 def,是可执行代码,直到运行 class 语句后类才会存在:

>>> class FirstClass: # 类名
        spam = 30 # 类数据属性
        def display(self): # 类方法,属于可调用的属性
            print self.spam

>>> x = FirstClass() # 创建类实例,实例化
>>> x.display() # 方法调用

class 语句中,任何赋值语句都会创建类属性,每个实例对象都会继承类的属性并获得自己的名称空间。

>>> ins1 = FirstClass()
>>> ins1.
ins1.__class__   ins1.__doc__     ins1.__module__  ins1.display(    ins1.spam

# 这个类就出现了所有的方法,可以看到spam是属性

封装

封装是面对对象的三大特性之一。在了解封装之前,我们必须知道什么是 self。

self是啥

通过下面的例子就知道 self 是啥了。

class Foo(object):
    def fun1(self, arg1):
        print(arg1, self)

i1 = Foo()
print(i1)
i1.fun1('hehe')

执行结果:

<__main__.Foo object at 0x00000000006C32B0>
hehe <__main__.Foo object at 0x00000000006C32B0>

可以看出 i1 和 self 是同一个东西,由此 self 就是实例化对象后的对象自身,也就是 i1。类只有一个,但是实例化的对象可以有无数个,不同的对象的 self 自然都不相同。

self 是一个形式参数,python 内部自动传递。

在了解了什么是 self 之后,现在就可以聊聊封装了。看下面的例子:

class Foo(object):
    def fetch(self, start):
        print(start)

    def add(self, start):
        print(start)

    def delete(self, start):
        print(start)

上面的代码中,同样的参数 start 被传递到了三个函数中,这样就显得很累赘,能否不需要这么麻烦呢?肯定是可以的。如下:

class Foo(object):
    def fetch(self):
        print(self.start)

    def add(self):
        print(self.start)

    def delete(self):
        print(self.start)

obj1 = Foo()
obj1.start = 'hehe'
obj1.fetch()

修改后三个函数不再接受参数,这就达到了我们的需求。由于 self 就是对象本身,因此 self.start 就是我们传递的“hehe”,这就是类的封装。

通过在对象中封装数据,然后在类中通过 self 进行获取。这是函数式编程无法做到的。这只是类封装的一种方式,也是一种非主流的方式,下面将会提到主流的方式。

构造器

构造器就是所谓 __init__,它是类的内置方法。创建实例时,Python 会自动调用类中的 __init__ 方法。

class Foo(object):
    def __init__(self):
        print('init')

Foo()

执行结果:

init

可以看到,我们只要在类名的后面加上括号,就会自动执行类中的 __init__ 函数。通过 __init__ 的这种特性,我们就可以实现主流的封装方式。

我们可以看到 __init__ 中并没有 return 语句,但是类初始化后的返回值却并不为空,因此,实例化一个对象时,还会执行其他的方法。我们可以得出结论:__init__ 不是创建对象,它做的只是初始化对象。

实例化一个对象的过程为:

  1. 创建对象;
  2. 对象作为 self 参数传递给 __init__;
  3. 返回 self。

以上就是一个对象创建的过程,事实上这个过程我们是可以手动控制的。

class Foo(object):
    def __init__(self):
        self.start = 'hehe'

    def fetch(self):
        print(self.start)

    def add(self):
        print(self.start)

    def delete(self):
        print(self.start)

obj1 = Foo()
obj1.fetch()

这种方式就比较主流了,当我们要封装多个变量时,可以通过向 __init__ 函数中传递多个参数实现。

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

    def fun1(self):
        print('姓名:{},年龄:{}'.format(self.name, self.age))


obj1 = Foo('小红', 7)
obj2 = Foo('小明', 23)
obj1.fun1()
obj2.fun1()

执行结果:

姓名:小红,年龄:7
姓名:小明,年龄:23

__init__ 方法被称为构造器,如果类中没有定义 __init__ 方法,实例创建之初仅是一个简单的名称空间。类的 __varname__ 这种方法会被 python 解释器在某些场景下自动调用,就向a+b实际上调用的是 a.__add__(b);l1 = ['abc', 'xyz'] 实际上是调用 list.__init__()。

构造函数的作用就是不需要我们手动调用类中的属性或方法,如果想要在实例化成对象的时候执行,就可以将操作写入到 __init__ 方法下。

析构器

析构器又称为解构器,定义的是一个实例销毁时的操作。也就是当使用 del() 函数删除这么一个类时,它会自动调用这个类中的 __del__。但是一般而言,解释器会自动销毁变量的,因此大多情况下,析构函数都无需重载,但是构造器则不同,它是实现实例变量的一种重要接口。

析构函数就是用于释放对象占用的资源,python 提供的析构函数就是 __del__()。__del__() 也是可选的,如果不提供,python 会在后台提供默认析构函数。

析构器会在脚本退出之前执行,我们可以用它来关闭文件:

class People(object):
    color = 'yellow'
    __age = 30

    def __init__(self,x):
        print "Init..."
        self.fd = open('/etc/passwd')

    def __del__(self):
        print 'Del...'
        self.fd.close()

ren = People('white')
print 'Main end' # 通过这个判断__del__是否在脚本语句执行完毕后执行

可以看出是在脚本退出之前执行的:

[root@node1 python]# python c3.py 
Init...
Main end
Del...

下面是一个析构器的示例:

class Animal:
  name = 'Someone' # 数据属性(成员变量)
  def __init__(self,voice='hi'): # 重载构造函数
    self.voice = voice # voice有默认值
  def __del__(self): # 这个del就是析构函数,但是它没有起到任何作用,因为pass了
    pass
  def saysomething(self): # 方法属性(成员函数)
    print self.voice

>>> tom = Animal()
>>> tom.saysomething()
hi # 默认值为hi
>>> jerry = Animal('Hello!')
>>> jerry.saysomething()
Hello!

例二:

>>> class Person:
...     def __init__(self,name,age): # 定义一个构造器
...         print 'hehe'
...         self.Name = name
...         self.Age = age
...     def test(self):
...         print self.Name,self.Age
...     def __del__(self): # 定义解构器
...         print 'delete'
...
>>> p = Person('Tom',23)
hehe
>>> del(p) # 删除实例时立即调用解构器
delete

作用域

函数是作用域的最小单元,那么在类中有何表现呢?

class E:
    NAME = 'E' # 类的直接下级作用域,叫做类变量

    def __init__(self, name):
        self.name = name # 关联到对象的变量,叫做实例变量

>>> e = E('e')
>>> e.NAME
Out[4]: 'E'
>>> E.NAME
Out[5]: 'E'

从上面可以看出,类变量对类和实例都可见。

>>> E.name
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-6-b6daf181be33>", line 1, in <module>
    E.name
AttributeError: type object 'E' has no attribute 'name'

可以看到,实例变量对实例化后的对象可见,但对类本身并不可见。

>>> e2 = E('e2')
>>> e2.NAME
Out[8]: 'E'

可以看到,所有实例共享类变量。但是,当其中一个实例修改了类变量呢?

>>> e2.NAME = 'e2'
>>> e.NAME
Out[10]: 'E'

既然共享了,为什么其中一个实例修改后不会影响到其他实例呢?实例变量到底是不是共享的呢?我们再看一个例子。

>>> e.xxx = 1 # 可以给对象任意增加属性
>>> e2.xxx
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-13-8ca2718f6555>", line 1, in <module>
    e2.xxx
AttributeError: 'E' object has no attribute 'xxx'

之所以出现这样的情况,是因为 Python 可动态的给对象增减属性。当给实例的类变量增加属性时,相当于动态的给这个实例增加了一个属性,覆盖了类变量。因为,类变量是共享的这句话并没有错。

我们继续往下看:

>>> E.NAME = 'hehe' # 直接修改类变量
>>> e.NAME
Out[15]: 'hehe'
>>> e2.NAME
Out[16]: 'e2'

不要感到慌张和迷茫,这里恰好说明之前的说法都是正确的。因为 e.NAME 并没有修改,因为使用的仍然是类变量,当类变量修改了,通过 e 去访问时肯定也会发生变化。而 e2 由于之前修改了,因为这个类变量被覆盖了,变成了这个对象的私有属性了,因此不受类变量的影响。

因此,始终都要牢记 Python 中的一大准则,赋值即创建

属性的查找顺序

事实上通过 e.NAME 访问,相当于 e.__class__.NAME。而 e.NAME = 1 相当于 e.__dict__['NAME'] = 1。虽然如此,但是会发生下面这样的情况。

>>> e.NAME
Out[6]: 'E'
>>> e.__dict__['NAME'] = 1
>>> e.NAME
Out[8]: 1
>>> e.__class__.NAME
Out[9]: 'E'

通过 e.NAME 和 e.__class__.NAME 访问的结果却不一样,这是为什么呢?这就涉及到属性的查找顺序了:

__dict__ -> __class__

由于在 __dict__ 中可以找到 NAME,所以就直接返回了,而不会在 __class__ 中继续找。

__class__ 是实例对类的一个引用。因此 e.__class__.NAME 这个值就是类中 NAME 的值,它不会改变。而我们修改实例的属性,则是添加到实例的 __dict__ 中。

装饰器装饰类

给类加装饰器就是动态的给类增加一些属性和方法。

看下面的例子:

def set_name(cls, name):
    cls.NAME = name
    return cls

class F:
    pass

>>> F1 = set_name(F, 'F')
>>> F1.NAME
Out[16]: 'F'

事实上 set_name 就相当于一个装饰器。那我们可以使用装饰器的语法将其重写一下:

def set_name(name):
    def wrap(cls):
        cls.NAME = name
        return cls
    return wrap
    
@set_name('G')
class G:
    pass
 
>>> G.NAME
Out[19]: 'G'

结果证明是没有问题的,其实使用装饰器语法就相当于:

G = set_name('G')(F) # F 是前面定义的类

还可以通过装饰器给类添加方法:

def print_name(cls):
    def get_name(sel): # 必须传递一个参数给它,不然不能通过实例来调用
        return cls.__name__
    cls.__get_name__ = get_name
    return cls

@print_name
class H:
    pass

>>> h = H()
>>> h.__get_name__()
Out[24]: 'H'

只不过类装饰器通常用于给类增加属性的,而增加方法则有更好的方式。

属性和方法

类中的变量称为属性、函数称为方法。它们又有静态属性、静态方法、动态属性、动态方法、类方法等之分。

方法的定义都是类级的,但是有的方法使用实例调用,用的方法通过类调用。

实例方法和属性

实例方法和属性都是与 self 相关的,因此只能通过实例进行访问。实例方法的第一个参数是实例名,默认即是如此。由于类根本不知道实例(self)是什么(因为还没有实例化),因此不能通过类直接实例方法和实例属性。

class Foo:
    def __init__(self, name):
        self.name = name # 实例属性

    def f1(self): # 实例方法
        print('f1')

类方法和属性

类属性前面提到过了,定义在类作用域下的变量就是类属性。它可以通过类和实例直接访问。

类方法类似于静态方法,它可以通过类直接访问。与静态方法的区别在于,它可以获取当前类名。第一个参数为类本身的方法叫做类方法。类方法可以通过实例进行调用,但是第一个参数依然是类本身。

class Foo:
    @classmethod # 修饰为类方法
    def f2(cls): # 必须接受一个参数

类方法必须接受一个参数,它是由类自动传递的,它的值为当前类名。也就是说,通过 classmethod 装饰器会将自动传递给方法的第一个参数(之前为实例名)改为类名。而被装饰的方法的参数名和 self 一样,不强制要求为 cls,只是习惯这么写而已。

类方法的最大的用处就是无需实例化即可使用。

静态方法

不同于实例方法和类方法的必须拥有一个参数,静态方法不需要任何参数。

class Foo:
    @staticmethod # 装饰为静态方法
    def f1(): # 没有任何参数
        print('static method')

被 staticmethod 装饰器装饰后,访问的时候不会自动传递第一个参数。静态方法和类方法一样,可以同时被类和实例访问。

class Foo:
    @staticmethod
    def f1():
        print('static method')

    def f2(): # 可以被类访问
        print('hehe')

f1 和 f2 的区别在于,f2 无法通过实例访问。

私有方法和属性

以双下划线开头,且非双下划线结尾的函数/变量就是私有方法/属性,在类的外部无法访问。我们可以得出结论:所有以双下划线开头,且非双下划线结尾的成员都是私有成员。

通过下面的例子可以看到它的用处。

class Door:
    def __init__(self, number, status):
        self.number = number
        self.__status = status

    def open(self):
        self.__status = 'opening'
    
    def close(self):
        self.__status = 'closed'

>>> door = Door(1, 'closed')
>>> door.__status # 直接访问会报错
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-35-d55234f04e7f>", line 1, in <module>
    door.__status
AttributeError: 'Door' object has no attribute '__status'

但是可以直接修改它的属性:

>>> door.__status
Out[37]: 'hehe'
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值