Python的杂货知识——Class相关

2021/2/3
好久不看不写Python和NN,乍一看代码就是头大,而且基本的东西都忘光了,实在丢人。
所以重新捡一捡一些基本的python知识。
本篇中的基本内容包括:

__init__方法

__init__方法的主要参考资料是一篇知乎的回答
作用:定义类的时候,如果添加__init__方法,实例化时产生的实例会自动调用这个方法,一般用来对实例的属性进行初使化。
举例:

class base_test:
    def __init__(self, prop_a):
        print("This is from base_test class.")
        self.prop_a = prop_a
        self.prop_b = 20

class succeeded_class(base_test):
    def __init__(self, prop_a, prop_c, prop_d):
        super(succeeded_class, self).__init__(prop_a)
        self.prop_c = prop_c
        self.prop_d = prop_d
        print("This is from succeeded class.")

test = succeeded_class(10, 30, 40)
print(test.prop_a)
print(test.prop_b)
print(test.prop_c)
print(test.prop_d)

运行结果显然是:

This is from base_test class.
This is from succeeded class.
10
20
30
40

上面这个例子中用到了继承,后面也得复习一下,总之是定义了两个类base_testsucceeded_class,分别称之为"基类"和"继承类",基类中的__init__方法给这个类的两个属性self.prop_aself.prop_b赋初值,实例化的时候需要传入参数prop_a;继承类在实例化的时候需要传入三个参数prop_aprop_cprop_d,其中参数prop_a是基类所需要的,调用了super函数(后面也得复习一下)来访问继承类的父类(也就是基类)中的方法或属性,这里是调用了父类中的初始化方法。
上面输出的结果"This is …"是为了说明在实例化的时候就调用这两个__init__方法,输出的顺序和子类中的定义顺序一致。
需要注意的是:

  • __init__方法的第一参数永远是self,表示创建的类实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。
  • 有了__init__方法,在创建实例的时候就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器会自己把实例变量传进去。
    在这里顺便说一下经常见到的self*args**kwargs

主要的参考文章是这个还有这个

self

  • self代表类的实例,而非类。
    • 但是,self.__class__则指向实例对应的类。
    • 注意,把self换成this,结果也一样,但最好使用Python中常用的self
  • self在定义时不可以省略,但是定义类方法时(定义和后续调用时均不传类实例)可以不写。
    • 类方法里如果不写的话会有解释器会产生警告,有点奇妙(大概是某种危险操作)

还有两条莫名其妙的性质,大概还用不到,需要的时候去查第一篇参考文章。

*args与**kwargs

  • *args是将参数打包成tuple给函数体调用
    • 例一:
    def function(x, y, *args):
      print(x, y, args)
      print(x, y, *args)
    
    function(1, 2, 3, 4, 5) 
    

    运行结果:

    1 2 (3, 4, 5)
    1 2 3 4 5
    

    看起来在调用的时候args是后续传入参数打包后的tuple,*args则是解耦的原始参数。

    • 例二:
    class base_model:
      def __init__(self, a, b):
          self.a = a
          self.b = b
    
    class second_model(base_model):
        def __init__(self, *args, **kwargs):
            super(second_model, self).__init__(*args, **kwargs)
            self.c = 30
            self.d = 40
    
        def print(self):
            print(self.a)
            print(self.b)
            print(self.c)
            print(self.d)
    
    instance = second_model(10, 20)
    instance.print()
    

    运行结果显然是:

    10
    20
    30
    40
    
  • **kwargs 将关键字参数打包成dict给函数体调用。
    • 例如:
    def function1(**kwargs):
      print(kwargs)
    
    def function2(**kwargs):
        print(**kwargs)
    
    function1(a=1, b=2, c=3)
    function2(a=1, b=2, c=3)
    

    输出结果为:

    Traceback (most recent call last):
    File "F:/codes_learning/AWNAS/aw_nas_private/aw_nas/final/test.py", line 101, in <module>
      function2(a=1, b=2, c=3)
    File "F:/codes_learning/AWNAS/aw_nas_private/aw_nas/final/test.py", line 98, in function2
      print(**kwargs)
    TypeError: 'a' is an invalid keyword argument for print()
    {'a': 1, 'b': 2, 'c': 3}
    

    显然字典就没有"拆包"一说了(合理

  • 参数arg、args、*kwargs三个参数的位置必须是一定的。必须是(arg,args,*kwargs)这个顺序,否则程序会报错。

继承

这里有篇很有趣的博文,讲的是继承。
定义:继承是一种创建新的类的方式,新创建的叫子类,继承的叫父类、超类、基类。子类可以使用父类的属性。
作用:减少代码冗余、提高重用性。
继承的要点包括:

  • 单继承
    • 类在定义的时候执行类体代码,执行顺序是从上到下,例如
    class base_class:
      #def __init__(self):
        print("This is from base class.")
    
    class second_class(base_class):
      #def __init__(self):
        print("This is from the second class.")
    
    class third_class(second_class):
      #def __init__(self):
        print("This is from the third class.")
    
    instant = third_class()
    
    • 输出的结果是:
    This is from base class.
    This is from the second class.
    This is from the third class.
    

    (实际上会在初始化的时候使用super调用父类的初始化函数吧,所以执行顺序其实是可变的)

  • 多继承
    • 继承自多个类,比如class succeeded_class(base_class1, base_class2)
  • 新式类与经典类
    • 继承了object的类以及该类的子类,都是新式类。在Python3中如果一个类没有继承任何类,则默认继承object类,因此,Python3中都是新式类。
    • 没有继承object的类以及该类的子类,都是经典类。在Python2中如果一个类没有继承任何类,不会继承object类,因此,只有Python2中有经典类。
  • 抽象类:通过抽象可以得到类,抽象是一种分析的过程。(从具体的对象中,分析抽象出一个类),没有仔细看

  • 派生类:派生,就是在子类继承父类的属性的基础上,派生出自己的属性。子类有不同于父类的属性,这个子类叫做派生类。通常情况下,子类和派生类是同一个概念,因为子类都是有不同于父类的属性,如果子类和父类属性相同,就没必要创建子类了。

  • 组合:组合指的是,在一个类A中,使用另一个类B的对象作为类A的数据属性(特征)(变量),成为类的组合。
    • 继承建立了派生类和基类的关系,是一种'是'的关系,比如白马是马,人是动物。
    • 组合建立了两个类之间'有'的关系,比如人有手机,然后人可以使用手机打电话。
    • 有点类似于C中的结构体…但是两个类之间的组合通过实例进行的。
  • 属性查找顺序:对象自己的 -> 所在类中 -> 找父类 ->父类的父类 -> … -> Object

  • 覆盖(override):子类出现了与父类名称完全一致的属性或是方法。

  • super函数与__base__方法
    • 使用__bases__方法可以获取子类继承的类
    • 使用super函数在子类中访问父类的内容,方式如下:
      方式1:
      super(当前类名称,self).你要调的父类的属性或方法
      方式2:
      super().你要调的父类的属性或方法
      方式3:
      类名称.你要调的父类的属性或方法(self)
      

      当你继承一个现有的类,并且你覆盖了父类的init方法时,必须在初始化方法的第一行调用父类的初始化方法,并传入父类所需的参数。

五种下划线

参考资料为这篇知乎专栏
单下划线和双下划线是一种名称修饰(name mangling),其中有一些仅作提醒用,有一些则被Python解释器严格执行。几种下划线可以总结为下表:

类型 形式 含义
单前导下划线 _var 提示该名称供内部使用,不强制执行
单末尾下划线 var_ 避免与Python关键字产生命名冲突,不强制执行
双前导下划线 __var 解释器重写属性名称,避免子类命名冲突,强制执行
双前导和双末尾下划线 __var__ 特殊用途
单下划线 _ 临时变量或占位符
  • 单前导下划线 _var
    单个下划线是一个Python命名约定,表示这个名称(变量和方法名)是供内部使用的。 它通常不由Python解释器强制执行,仅仅作为一种对程序员的提示。
    • 然而前导下划线会影响从模块中导入名称的方式,如果使用通配符从模块中导入所有名称(例如from my_module import *),则Python不会导入带有单前导下划线的名称(除非模块定义了覆盖此行为的__all__列表);常规导入(例如import my_module)则不受前导单个下划线命名约定的影响。
  • 单末尾下划线var_
    单个末尾下划线是一个约定,用来避免与Python关键字产生命名冲突。
  • 双前导下划线 __var
    双下划线前缀会导致Python解释器重写属性名称,以避免子类中的命名冲突。这也叫做名称修饰(name mangling) - 解释器更改变量的名称,以便在类被扩展的时候不容易产生冲突。
    (但是也有人说这是为了防止实例化后修改就是了)
    • 有一个很有趣的名称修饰的例子:
    _MangledGlobal__mangled = 23
      
    class MangledGlobal:
       def test(self):
           return __mangled
      
    >>> MangledGlobal().test()
    23
    
  • 双前导和双末尾下划线 _var_
    Python保留了有双前导和双末尾下划线的名称,用于特殊用途,如__init____call__

  • 单下划线 _
    表示某个变量是临时的或无关紧要的,用来占位。
    • 除了用作临时变量之外,_是大多数Python REPL中的一个特殊变量,它表示由解释器评估的最近一个表达式的结果,可以在一个解释器会话中访问先前计算的结果,例如:
    >>> 20 + 3
    23
    >>> _
    23
    >>> print(_)
    23
      
    >>> list()
    []
    >>> _.append(1)
    >>> _.append(2)
    >>> _.append(3)
    >>> _
    [1, 2, 3]