Python魔法方法之_init_、_del_与_new_

Stella981
• 阅读 604

今天的这篇文章来自蜗牛学院重庆校区陈南老师。

蜗牛学院资深导师,计算机本科学历,10年开发测试及管理经验。曾供职于某大型研究所与韩国互联网龙头企业Naver,并担任项目主要负责人。深厚的技术功底,具备安全邮件、存储产品、电商系统、开源数据库等多个大型项目的开发与测试经验。在Linux、C/Java/Python、数据库、自动化测试、性能测试、安全性测试与系统架构等方面均具有丰富的理论体系和实践经验。授课中注重培养学生解决问题的思维及技术的扩展。

·  正  ·  文  ·  来  ·  啦  ·

在Python语言中,有些方法名比较特别,在名称的前后各有两个下划线,这样的方法往往具有特殊的意义,我们统称为魔法方法。需要注意的是,我们在创建自定义方法时要避免这样的格式,防止造成不必要的冲突和误解。
在正式了解魔鬼方法前,有必要给大家回顾下Python中函数和方法的区别。
(1)函数:类外部定义的,跟类没有直接关系的,如:def func(*argv)。(2)方法:class内部定义的函数(对象的方法也可以认为是属性),分为两种:

  • 第一种人为自定义的方法,和普通函数没有区别,只是定义在了class中而已;

  • 第二种是Python自动产生的(魔法方法),一般形式为:__func__,Python会在对应的时机自动调用该函数,下面我们要学习的方法均为这一种。

1.__init__(self, *args, **kwargs)
最常用的魔法方法,在创建完对象后调用,对当前对象的一些实例初始化,无返回值,我们称之为构造方法,和C++中的构造方法类似。先看下面一个例子,在Demo类中定义了__init__方法和一个普通方法func,然后在类定义外进行实例化。

class Demo:
    def __init__(self):
        print('调用__init__方法')
    def func(self):
        print('这是一个普通的方法')
d = Demo()

由于只对Demo进行了实例化,并没有主动调用任何方法,在我们的设想中,不会输出任何语句,但实际运行结果如下。

调用__init__方法

这个例子印证了上述:__init__方法在创建对象后被自动调用。
下面通过另外一个例子让大家理解构造方法的重要作用:对实例初始化。同样的,类中定义了__init__和func两个方法,但这次我们看到在__init__方法中添加了两个参数n和a,并将这两个值赋给成员属性self.name和self.age,在func方法中则使用了这两个成员属性。
在类外,对Demo实例化对象d1,并且传入实参‘小明’和23,细心的话可以发现,实例化Demo时和__init__方法的恰好都是两个参数。可以理解为,实例化操作Demo('小明',23)就是在调用__init__,这里的‘小明’和23也就传递给了形参n和a。d2对象同理。

class Demo:
    def __init__(self, n, a):
        self.name = n
        self.age = a
    def func(self):
        print('我的名字是 %s, 我的年龄是 %d'%(self.name, self.age))
d1 = Demo('小明',23)
d1.func()
d2 = Demo('小红',21)
d2.func()

一系列的传递后,d1和d2对象分别代表的小明和小红,那么我们也可以继续创建对象d3、d4、d5等,继续代表其他的人,只要我们实例化时传入的值不同,最后调用func方法时就会呈现不同人的信息,而这都是通过__init__方法参数达到的,进而实现了初始化成员属性的目的。运行结果如下所示。

我的名字是 小明, 我的年龄是 23我的名字是 小红, 我的年龄是 21

2.__del__(self)
同__init__方法相反,__del__在对象销毁时被调用,往往用于清除数据或还原环境等操作,比如在类中的其他普通方法中实现了插入数据库的语句,当对象被销毁时我们需要将数据还原,那么这时可以在__del__方法中实现还原数据库数据的功能。__del__被成为析构方法,同样和C++中的析构方法类似。
下面一个例子的执行顺序可以让大家加深对其的理解。

class Demo:
    def __init__(self):
        print('调用__init__方法')
    def func(self):
        print('这是一个普通的方法')
    def __del__(self):
        print('调用__del__方法')
d = Demo()
d.func()

当d.func()执行后,对象d没有在任何一个地方被继续引用,这时Python的垃圾回收机制会主动回收这个对象,即销毁d,此时自动调用__del__方法,运行结果如下。

调用__init__方法这是一个普通的方法调用__del__方法

 3.__new__(cls, *args, **kwargs)
在过去很长的一段时间,笔者也忽略了这个方法。我们往往认为__init__是实例化时调用的第一个方法(注意前面讲解__init__方法时讲的是实例化后调用的第一个方法,一字之差),但其实实例化时调用的第一个方法是__new__方法。
官方定义如下,可以看到__new__被定义成静态方法,第一个必须传入的参数是cls,代表需要实例化的类。

@staticmethod    # known case of __new__
def __new__(cls, *more): # known special case of object.__new__
    """ Create and return a new object.  See help(type) for accurate signature. """
    pass

 下面从几个方面对__new__方法作进一步的讲解。
3.1 与__init__的关系
在一个类中,__init__和__new__同时存在时,会首先调用__new__方法,代码如下。

class Demo:
    def __new__(cls, *args, **kwargs):
        print('调用__new__方法')
    def __init__(self):
        print('调用__init__方法')# d = Demo()
d = Demo()

运行结果如下。

调用__new__方法

确实首先调用了__new__方法,但奇怪的是并没有调用__init__方法,这是为什么呢?实际上,__new__是负责对当前类进行实例化,并将实例返回,并传给__init__方法,__init__方法中的self就是指代__new__传过来的对象,所以再次强调,__init__是实例化后调用的第一个方法。

class Demo:
    def __new__(cls, *args, **kwargs):
        print('调用__new__方法')
        return object.__new__(cls)
    def __init__(self):
        print('调用__init__方法')
d = Demo()

上面的代码中,在__new__方法中添加了一条return语句,返回object的__new__方法,由于我们自定义的Demo类的父类都是object类,实际上这里是为了返回了父类object生成的对象,然后将此对象传递给__init__的self。因此只有__new__方法正确return了当前类cls的实例,__init__方法才会被调用。特别需要说明的是,__new__方法中一定是返回父类的__new__方法来创建对象,如果返回当前对象会造成死循环,大家可以自行尝试一下。

调用__new__方法调用__init__方法

从运行结果可见,依次调用了__new__和__init__方法。当然大多数情况下,我们不用去重写__new__,保持默认即可。 3.2 继承不可变类
说了这么多,__new__方法有什么作用呢?当继承一些不可变的class时(比如int, str, tuple),__new__方法提供了自定义这些类的实例化过程的途径。比如我们自定义类NewInt要继承int类,并且让传入的任何数最后都被转换成正整数,代码如下。

class NewInt(int):
    def __new__(cls, value):
        print('调用__new__方法')
        return int.__new__(cls, abs(value))
a = NewInt(-4.822)
print(a)

自定义类NewInt继承于类int,但由于类int为不可变的,那么我们可以通过重写__new__方法,在对父类int实例化时作出需要的更改,即传入的value最终都处理为正整数,并返回NewInt对象,运行结果如下。

调用__new__方法4

 3.3 实现单例模式
单例模式是一个经典设计模式,简要的说,一个类只能被实例化一次,实例对象在第一次实例化时就已经固定,从第二次以后其实一直都是用的第一次实例化的对象,相当于全局。我们先来看非单例模式的情况。

class Demo:
    def __init__(self, name):
        print('我是 %s'%(name))
d1 = Demo('小明')
d2 = Demo('小红')
print(d1)
print(d2)

Demo类正常设计了__init__方法,没有做其他特殊的处理,然后通过Demo类实例化了两个对象分别是d1和d2,最后输出两个对象,即他们的内存地址,结果如下。

我是 小明我是 小红<__main__.Demo object at 0x00000000021FC550><__main__.Demo object at 0x00000000021FC710>

可以看到d1和d2占用的是不同的内存地址,说明这里的Demo类被实例化了2次,生成了不同的对象,就好比现实生活中的‘小明’和‘小红’,分别是两个不同的个体。 接下来我们重写Demo类中的__new__方法,对上述代码进行一些改变。首先设置了一个私有的类属性,并赋值False,表示Demo类没有被实例化过,当每一次Demo类被实例化时,会优先调用__new__方法,此时__new__方法里会先检查__isinstance的值,如果他没被实例化,则调用父类object的__new__方法进行实例化,并将实例化后的对象赋给私有类属性__isinstance,如果被实例化过,则不用处理,最后返回__isinstance。这样就能保证Demo类只被实例化一次,即实现单例模式。

class Demo:
    __isinstance = False
    def __new__(cls, *args, **kwargs):
        if not cls.__isinstance:  # 如果被实例化了
            cls.__isinstance = object.__new__(cls)  # 否则实例化
        return cls.__isinstance  # 返回实例化的对象
    def __init__(self, name):        print('我是 %s'%(name))
d1 = Demo('小明')
d2 = Demo('小红')
print(d1)
print(d2)

运行结果如下,无论表面上使用Demo实例化多少次,其实都是同一个对象,内存地址也是一样的。

我是 小明我是 小红<__main__.Demo object at 0x00000000023FC710><__main__.Demo object at 0x00000000023FC710>

 至此,本文对Python中最常见的三个魔法方法作了基本的介绍,他们有各自的执行时机,也具备不同的作用。读者可以在本文的基础上多尝试更多更丰富的例子,从多个角度来加深对魔法方法的理解。

查看更多技术文章,可以WX添加Tz923521260。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Python3:sqlalchemy对mysql数据库操作,非sql语句
Python3:sqlalchemy对mysql数据库操作,非sql语句python3authorlizmdatetime2018020110:00:00coding:utf8'''
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这