Python进阶

1.  真假构造函数 __init__和__new__的区别是什么?

在Python当中__init__并不是构造函数,__new__才是。那么为什么我们创建类的时候从来不用它呢?

首先我们回顾一下__init__的用法,我们随便写一段代码:

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

我们在Python当中怎么实现单例(Singleton)的设计模式呢?怎么样实现工厂呢?

从这个问题出发,你会发现只使用__init__函数是不可能完成的,因为__init__并不是构造函数,它只是初始化方法。也就是说在调用__init__之前,我们的实例就已经被创建好了,__init__只是为这个实例赋上了一些值。如果我们把创建实例的过程比喻成做一个蛋糕,__init__方法并不是烘焙蛋糕的,只是点缀蛋糕的。那么显然,在点缀之前必须先烘焙出一个蛋糕来才行,那么这个烘焙蛋糕的函数就是__new__。

__new__函数

我们来看下__new__这个函数的定义,我们在使用Python面向对象的时候,一般都不会重构这个函数,而是使用Python提供的默认构造函数,Python默认构造函数的逻辑大概是这样的:

def __new__(cls, *args, **kwargs):
    return super().__new__(cls, *args, **kwargs)

从代码可以看得出来,函数当中基本上什么也没做,就原封不动地调用了父类的构造函数。这里隐藏着Python当中类的创建逻辑,是根据继承关系一级一级创建的。根据逻辑关系,我们可以知道,当我们创建一个实例的时候,实际上是先调用的__new__函数创建实例,然后再调用__init__对实例进行的初始化。我们可以简单做个实验:

class Test:
    def __new__(cls):
        print('__new__')
        return object().__new__(cls)
    def __init__(self):
        print('__init__')

当我们创建Test这个类的时候,通过输出的顺序就可以知道Python内部的调用顺序。

从结果上来看,和我们的推测完全一样。

单例模式

那么我们重载__new__函数可以做什么呢?一般都是用来完成__init__无法完成的事情,比如前面说的单例模式,通过__new__函数就可以实现。我们来简单实现一下:

class SingletonObject:
    def __new__(cls, *args, **kwargs):
        if not hasattr(SingletonObject, "_instance"):
            SingletonObject._instance = object.__new__(cls)
        return SingletonObject._instance
    
    def __init__(self):
        pass

当然,如果是在并发场景当中使用,还需要加上线程锁防止并发问题,但逻辑是一样的。

除了可以实现一些功能之外,还可以控制实例的创建。因为Python当中是先调用的__new__再调用的__init__,所以如果当调用__new__的时候返回了None,那么最后得到的结果也是None。通过这个特性,我们可以控制类的创建。比如设置条件,只有在满足条件的时候才能正确创建实例,否则会返回一个None。

比如我们想要创建一个类,它是一个int,但是不能为0值,我们就可以利用__new__的这个特性来实现:

class NonZero(int):
    def __new__(cls, value):
        return super().__new__(cls, value) if value != 0 else None

那么当我们用0值来创建它的时候就会得到一个None,而不是一个实例。

工厂模式

理解了__new__函数的特性之后,我们就可以灵活运用了。我们可以用它来实现许多其他的设计模式,比如大名鼎鼎经常使用的工厂模式

所谓的工厂模式是指通过一个接口,根据参数的取值来创建不同的实例。创建过程的逻辑对外封闭,用户不必关系实现的逻辑。就好比一个工厂可以生产多种零件,用户并不关心生产的过程,只需要告知需要零件的种类。也因此称为工厂模式。

比如说我们来创建一系列游戏的类:

class Last_of_us:
    def play(self):
        print('the Last Of Us is really funny')
        
        
class Uncharted:
    def play(self):
        print('the Uncharted is really funny')
        

class PSGame:
    def play(self):
        print('PS has many games')

然后这个时候我们希望可以通过一个接口根据参数的不同返回不同的游戏,如果不通过__new__,这段逻辑就只能写成函数而不能通过面向对象来实现。通过重载__new__我们就可以很方便地用参数来获取不同类的实例:

class GameFactory:
    games = {'last_of_us': Last_Of_us, 'uncharted': Uncharted}
    def __new__(cls, name):
        if name in cls.games:
            return cls.games[name]()
        else:
            return PSGame()
        

uncharted = GameFactory('uncharted')
last_of_us = GameFactory('last_of_us')

关于__new__这个函数的用法应该都能理解了。一般情况下我们是用不到这个函数的,只会在一些特殊的场景下使用。虽然如此,我们学会它并不只是用来实现设计模式,更重要的是可以加深我们对于Python面向对象的理解。

除此之外,另一个经常使用__new__场景是元类。

2. 元类metaclass的原理和用法 

metaclass

metaclass的英文直译过来就是元类,在这个用法当中,支持我们自己定义一个类,使得它是后面某一个类的元类。

之前使用type动态创建类的时候,我们传入了类名,和父类的tuple以及属性的dict。在metaclass用法当中,其实核心相差不大,只是表现形式有所区别。我们来看一个例子即可:

class AddInfo(type):
    def __new__(cls, name, bases, attr):
        attr['info'] = 'add by metaclass'
        return super().__new__(cls, name, bases, attr)
        
        
class Test(metaclass=AddInfo):
    pass

在这个例子当中,我们首先创建了一个类叫做AddInfo,这是我们定义的一个元类。由于我们希望通过它来实现元类的功能,所以我们需要它继承type类。我们在之前的文章当中说过,在Python面向对象当中,所有的类的根本来源就是type。也就是说Python当中的每一个类都是type的实例。

我们在这个类当中重载了__new__方法,我们在__new__方法当中传入了四个参数。眼尖一点的小伙伴一定已经看出来了,这个函数的四个参数,正是我们调用type创建类的时候传入的参数。其实我们调用type的方法来创建类的时候,就是调用的__new__这个函数完成的,这两种写法对应的逻辑是完全一样的。

我们之后又创建了一个新的类叫做Test,这个当中没有任何逻辑,直接pass。但是我们在创建类的时候指定了一个参数metaclass=AddInfo,这里这个参数其实就是指定的这个类的元类,也就是指定这个类的创建逻辑。虽然我们用代码写了类的定义,但是在实际执行的时候,这个类是以metaclass为元类创建的。

根据上面的逻辑,我们可以知道,Test类在创建的时候就被赋予了类属性info。我们可以验证一下:

拓展类功能

使用元类究竟能够做什么呢?

这里有一个经典的例子,我们都知道Python原生的list是没有'add'这个方法的。假设我们习惯了Java当中list的使用,习惯用add来为它添加元素。我们希望创建一个新的类,在这个新的类当中,我们可以通过add来添加函数。通过元类可以很方便地使用这一点。

class ListMeta(type):
    def __new__(cls, name, bases, attrs):
        # 在类属性当中添加了add函数
        # 通过匿名函数映射到append函数上
        attrs['add'] = lambda self, value: self.append(value)
        return super().__new__(cls, name, bases, attrs)
    
    
class MyList(list, metaclass=ListMeta):
    pass

我们首先是定义了一个叫做ListMeta的元类,在这个元类当中我们给类添加了一个属性叫做add。它只是包装了一下而已,底层是通过append方法实现的。我们来实验一下:

从结果来看也没什么问题,我们成功通过调用add方法往list当中插入了元素。这里藏着一个小细节,我们在ListMeta当中为attrs添加了一个名叫'add'的属性。这个属性是添加给类的,而不是类初始化出来的实例的。所以如果我们print出MyList这个类当中的所有属性,也能看到add的存在。

涉及类属性变更和类创建的时候, 我们必须要使用元类不可。

我们创建了三种游戏的类和一个工厂类,我们重载了工厂类的__new__函数。使得我们可以根据实例化时传入的参数返回不同类型的实例。

class Last_of_us:
    def play(self):
        print('the Last Of Us is really funny')
        
        
class Uncharted:
    def play(self):
        print('the Uncharted is really funny')
        

class PSGame:
    def play(self):
        print('PS has many games')
        
        
class GameFactory:
    games = {'last_of_us': Last_of_us, 'uncharted': Uncharted}
    def __new__(cls, name):
        if name in cls.games:
            return cls.games[name]()
        else:
            return PSGame()
        

uncharted = GameFactory('uncharted')
last_of_us = GameFactory('last_of_us')

假设这个需求完成得很好顺利上线了,但是运行了一段时间之后我们发现下游有的时候为了偷懒会不通过工厂类来创建实例,而是直接对需要的类做实例化。原本这没有问题,但是现在产品想要在工厂类当中加上一些埋点,统计出访问我们工厂的访问量。所以我们需要限制这些游戏类不能直接实例化,必须要通过工厂返回实例

那么这个功能我们怎么实现呢?

我们分析一下问题就会发现,这一次不是需要我们在创建实例的时候做动态的添加,而是直接限制一些类不允许直接调用进行创建。限制的方法比较常用的一种就是抛出异常,所以我们希望可以给这些类加上一个逻辑,实例化类的时候传入一个参数,表明是否是通过工厂类进行的,如果不是,则抛出异常

这里,我们需要用到另外一个默认函数,叫做__call__,它是允许将类实例当做函数调用。我们通过类名来实例化,其实也是一个调用逻辑。这个__call__的逻辑并不难写,我们随手就来:

def __call__(self, *args, **kwargs):
    if len(args) == 0 or args[0] != 'factory':
        raise TypeError("Can't instantiate directly")

但问题是这个__call__函数并不能直接加在类当中,因为它的应用范围是实例,而不是类。而我们希望的是在创建实例的时候进行限制,而不是对调用实例的时候进行限制,所以这段逻辑只能通过元类实现

我们直接创建类的时候就会触发异常,因为不是通过工厂创建的。我们这里判断是否是工厂创建的逻辑简化掉了,只是通过一个简单的字符串来进行的判断,实际上会用一些更加复杂的逻辑,这不是本文的重点,我们了解即可。

整体运行的逻辑和我们设想的一样,说明这样实现是正确的。

元类一般都是在一些高端开发的场景当中。比如说开发一些框架或者是中间件,为了方便下游的使用,需要创建一些关于类属性的动态逻辑,才会用到元类。另外,元类的概念和动态类、动态语言的概念有关,Python语言的动态特性很多正是通过这一点体现的。所以随着我们对于Python动态特性理解的加深,理解元类也会变得越来越容易,同样也会理解越来越深刻。如果我们把Python的元类和装饰器做一个类比的话,会发现两者的核心逻辑是很类似的。本质上都是在原有的逻辑之外封装新的逻辑,只不过装饰器针对的是一段逻辑,而元类针对的是类的属性和创建过程。

3. 详解__slots__,property和命名规范

__slots__关键字究竟是做什么的呢?

它主要有两个功能,我们先来说第一个功能,就是限制用户的使用

class Exp:

    __slots__ = ['a', 'b']
    def __init__(self):
        self.a = None
        self.b = None


if __name__ == "__main__":
    exp = Exp()
    exp.c = 3
    print(exp.c)

如果你运行这段代码的话,你会得到一个报错,提示你Exp这个对象当中并没有c这个成员,也就是说我们只能运用__slots__这个关键字当中定义的成员,对于没有定义的成员不能随意创建,这样就限制了用户的使用。

虽然现在大部分人使用这个关键字都是报着这个目的,但是很遗憾的是,Python创建者的初衷其实并不是这个。这就谈到了__slots__关键字的第二个作用,就是节省内存

如果了解过Python底层的实现原理,你会发现在Python当中为每一个实例都创建了一个字典,就是大名鼎鼎的__dict__字典。正是因为背后有一个字典,所以我们才可以创造出原本不存在的成员,也才支持这样动态的效果。我们可以人工地调用这个字典输出其中的内容,我们在加上__slots__关键字之前,输出的结果是这样的:

{'a': None, 'b': None}

但是加上了这个关键字之后,会得到一个报错,会告诉你Exp这个对象当中没有__dict__这个成员。原因很简单,因为使用dict来维护实例,会消耗大量的内存,额外存储了许多数据,而使用__slots__之后,Python内部将不再为实例创建一个字典来维护,而是会使用一个固定大小的数组,这样就节省了大量的空间。这个节省可不是一点半点,一般可以节省一半以上。也就是说牺牲了一定的灵活性,保证了性能。这一点也是__slots__这个关键字设计的初衷,但是现在很多人都用错了地方。

命名规范

在Python当中没有对public和private的字段做区分,所有的字段都是public的,也就是说用户可以拿到类当中所有的字段和方法。为了规范,程序员们约定俗成,决定所有加了下划线的方法和变量都看成是private的,即使我们能调用,但是一般情况下我们也不这么干。

所以我们通常会写两个方法,一个是公开的接口,一个是内部的实现。我们调用的时候只调用公开的接口,公开的接口再去调用内部的实现。这在Python当中已经成了惯例,因为我们在调用内部方法的时候,往往还会传入一些内部的参数。

我们来看个简单的例子:

class ExpA:

    def __init__(self):
        pass

    def public_func(self):
       self._private_func()

    def _private_func(self):
        print('private ExpA')


if __name__ == "__main__":
    exp = ExpA()
    exp.public_func()

除了_之外我们经常还会看到一些两个下划线的变量和方法,那么它们之间又有什么区别呢?

为了回答这个问题,我们来看下面这个例子:

class ExpA:

    def __init__(self):
        pass

    def public_func(self):
       self.__private_func()

    def __private_func(self):
        print('private ExpA')

class ExpB(ExpA):

    def __init__(self):
        pass

    def public_func(self):
       self.__private_func()

    def __private_func(self):
        print('private ExpB')

if __name__ == "__main__":
    exp = ExpB()
    exp.public_func()
    exp._ExpB__private_func()
    exp._ExpA__private_func()

请问最后会输出什么?

我们试一下就知道,第一行输出的是private ExpB,这个没有问题。但是后面两个是什么?

后面两个就是__private_func,只不过系统自动将它重新命名了。重新命名的原因也很简单,因为Python禁止加了两个下划线的方法被子类覆盖。所以这两者的区别就在这里,它们都被认为是private的方法和属性,但是一个下划线允许子类覆盖,而两个下划线不行。所以如果我们在开发的时候希望我们某一个方法不会被子类覆盖,那么我们就需要加上两个下划线。

最后,我们来看一个小问题。在C++当中当我们的变量名和系统的关键字冲突的时候,我们往往会在变量前面加上一个_来作为区分。但是由于Python当中下划线被赋予了含义,所以我们不能这么干,那么当变量冲突的时候应该怎么办呢?答案也很简单,我们可以把下划线加在后面,比如lambda_。

property

这里的property注解会在我们调用.param的时候被执行,而param.setter会在我们为param这个属性赋值的时候被执行。所以你可能会奇怪,为什么我们在__init__方法当中初始化的时候用的是self.param = param而不是self._param = param,这是因为我们在执行前者的时候,Python一样会调用@param.setter这个注解,所以我们没有必要写成后者的形式。当然你也可以这么写,不过两者是完全等价的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值