书《Python3学习笔记》第7章 类 笔记

其实书的作者对于Python底层的研究真的很厉害,虽然书中有几处小错误,但不影响整本书的质量。

本博客主要对于书中个人认为比较重要的内容或者比较有意思的内容进行标记。

定义

类与函数类似,类也是一种小粒度复用单位,但其行为特征更为复杂。

函数像机械加工,着重于处理过程,类则关注与数据本身,使其"活过来"

作为一种符合结构,类与模块有相似之处。但不同之处在于

类可生成多个实例

类可被继承和扩展

类实例的生命周期可控

类支持远算符,可按需重载

这些是模块没有或不要的。

建议将类和业务逻辑分离。

In [110]: class A: 
     ...:     class_name = 'A' 
     ...:     def __init__(self,name): 
     ...:         self.name = name 
     ...:                                                                                                                                                          

In [111]: A.__dict__                                                                                                                                               
Out[111]: 
mappingproxy({'__module__': '__main__',
              'class_name': 'A',
              '__init__': <function __main__.A.__init__(self, name)>,
              '__dict__': <attribute '__dict__' of 'A' objects>,
              '__weakref__': <attribute '__weakref__' of 'A' objects>,
              '__doc__': None})

In [112]: a= A('lalla')                                                                                                                                            

In [113]: a.__dict__                                                                                                                                               
Out[113]: {'name': 'lalla'}

In [114]:  

 类型的名字空间返回mappingproxy只读视图,不允许直接修改。

实例的名字空间是普通字典,可直接修改

函数dir所有所有可访问成员的名字,vars直接返回实例的__dict__属性。

当通过实例或者类访问莫个成员时,会从当前对象开始,依次由近到远向祖先类查找。如此做的好处,就是祖先类的新增功能可直接"广播"给所有后代。

class A:
name = 12

print('class_local===> ', locals())

def test(self):
print("method===>", locals())
print(A.name)
print(name)


if __name__ == '__main__':
demo = A()
demo.test()
/usr/local/bin/python3.7 /Users/shijianzhong/study/Python学习笔记/第七章类/7_1.py
Traceback (most recent call last):
  File "/Users/shijianzhong/study/Python学习笔记/第七章类/7_1.py", line 14, in <module>
    demo.test()
  File "/Users/shijianzhong/study/Python学习笔记/第七章类/7_1.py", line 9, in test
    print(name)
NameError: name 'name' is not defined
class_local===>  {'__module__': '__main__', '__qualname__': 'A', 'name': 12}
method===> {'self': <__main__.A object at 0x10634a290>}
12

书中讲过class在内部以函数方式执行,接收类型名字空间字典作为堆栈帧执行的名字空间。这样的话,成员定义作用域内的locals实际指向了class.__dict__(可读写版本)

但从语法上来说,class是类型定义,而非函数定义,这与内部执行方式无关。因此,class不构成E/E-Enclosing function locals;外部嵌套函数的名字空间(例如closure)作用域

字段

类型字段在class语句块内直接定义,而实例字段(属性)必须通过实例引用(self)赋值定义

字段赋值

可使用赋值语句为类或者实例添加新的字段

可一旦以子类或实例重新赋值,就将会在其名字空间建立同名字段,并会遮币原字段。

这与"赋值总是在当前空间建立关联"规则一致

删除操作仅针对当前名字空间,而不会按搜索顺序查找类型或基类。

成员(属性)访问总是按搜索规则进行,一个实例首先查找自身的属性,没有找类的,再没有根据继承方式,找父类的。

私有字段

私有字段就是变量名前面有__双下划线,后面没有双下划线。

所谓重命名,就是被编译器附加了类型名称前缀_类名。但这种做法不能真正的防止用户访问。流畅的Python书中说到,一般__双下划线的私有变量用的比较少,单下划线就够用了。

class X_Demo:
    __table = 'user'

    def __init__(self, name):
        self.__name = name

    def get_name(self):
        return self.__name


if __name__ == '__main__':
    x = X_Demo('sidian')
    print(x.__dict__)
    print(X_Demo.__dict__)
    print(x._X_Demo__name)
{'_X_Demo__name': 'sidian'}
{'__module__': '__main__', '_X_Demo__table': 'user', '__init__': <function X_Demo.__init__ at 0x10afa6680>, 'get_name': <function X_Demo.get_name at 0x10afa6560>, '__dict__': <attribute '__dict__' of 'X_Demo' objects>, '__weakref__': <attribute '__weakref__' of 'X_Demo' objects>, '__doc__': None}
sidian

如果给类属性定义了__的私有变量,那么继承的类或者实例就无法直接读取到该属性。

class A1:
    __name = 'user'
    print(locals())

class B1(A1):
    print(A1.__name)

if __name__ == '__main__':
    b1 = B1()
/usr/local/bin/python3.7 /Users/shijianzhong/study/Python学习笔记/第七章类/7_1.py
{'__module__': '__main__', '__qualname__': 'A1', '_A1__name': 'user'}
Traceback (most recent call last):
  File "/Users/shijianzhong/study/Python学习笔记/第七章类/7_1.py", line 25, in <module>
    class B1(A1):
  File "/Users/shijianzhong/study/Python学习笔记/第七章类/7_1.py", line 26, in B1
    print(A1.__name)
AttributeError: type object 'A1' has no attribute '_B1__name'

 子类的都无法读取到父类的属性,那实例更加不可能读取到,实例需要通过自身的类去读取

所以在模块中,类属性少用__,继承容易出问题

相关函数

hansattr 按搜索规则查找整个继承层数,搜索对象是否拥有该可访问属性

setattr 设置属性

delattr 删除属性

属性

对私有字段可以通过下划线变量名保护,对于公开字段可以通过property(属性)保护

对过在方法的上面添加装饰器@property

也可以通过xx = property(fget=方法名,fset=, del=)

属性的优先级高于同名实例字段,因为属性属于数据型描述符,定义了__set__

方法

实例方法,类方法,静态方法,这个不介绍了

重点是它们都保存在类型名字空间中,所以不能重名。装饰器和参数的差异,并不能改变在同一个名字空间字典中

对同一key重复赋值会覆盖前一次赋值的现实。

绑定

class X2:
    def test(self):
        ...


if __name__ == '__main__':
    x2 = X2()
    print(x2.test.__self__)
    print(x2.test.__func__)
    print(X2.test)
<__main__.X2 object at 0x105519d10>
<function X2.test at 0x10550cb90>
<function X2.test at 0x10550cb90>

 这里面书中讲的比较少,其实主要还是描述符在起作用,__get__,当读取到该方法的时候,会返回__get__返回的数据。

所以当实例调用方法可以分为两步进行

第一步将函数包装成方法返回m = x.test.__get__(x, X)

第二步用类去执行函数X.test(m.__self__, 123)

我们执行该方法的时候,其实是执行的第二步。

几个简单的特殊方法记录下

__new__:构造方法,创建对象实例

__init__:初始化方法,设置实例的相关属性

__del__:析构方法,实例被回收时被调用。

继承

面向对象有三个基本特性:封装、继承和多态

封装将就结构复用,逻辑内敛,以固定接口对外提供服务。其遵循单一职责,规定每个类型仅有一个应发变化的原因。多于一个的耦合设计导致脆弱类型,任何职责变更都可能引发连带变故。

单一封装的核心时解耦和内聚,这让设计更简单、清晰、代码更易测试和冻结,避免了不确定性。

继承并非要复原原有类型,而是一种增量进化,在遵循原有设计和不改变既有代码的前提下,添加新功能,或改进算法。

其对应开闭原则,对扩展开放,对修改关闭。

继承要分清楚时功能扩展,还是使用某些功能。如果仅仅时为了使用某些功能,应该用组合来代替。

统一类型

__base__,__subclassses__,还有一个新学的魔法方法__init_subclass__,

class A:
    # 当所有子类继承该类时候,会直接执行,可以对继承的类进行一些拦截,操作。
    def __init_subclass__(cls, **kwargs):
        cls.name = 'sidian'
        print(cls, kwargs)

class B:
    ...

class C(A, B,info='I am C'):
    ...

if __name__ == "__main__":
    print(A.__subclasses__())
    print(C.__base__)
    print(C.__bases__)
    print(vars(C))
/usr/local/bin/python3.7 /Users/shijianzhong/study/Python学习笔记/第七章类/7_5.py
<class '__main__.C'> {'info': 'I am C'}
[<class '__main__.C'>]
<class '__main__.A'>
(<class '__main__.A'>, <class '__main__.B'>)
{'__module__': '__main__', '__doc__': None, 'name': 'sidian'}

初始化

初始化__init__是可选的

如果子类没有新的构造参数,或者新的初始化逻辑,那么没比哟创建该方法。因为按照搜索顺序,解释器会找到基类的初始化方法并执行。

也因为同样的缘故,我们须在子类的初始化方法中显式调用基类方法。

覆盖

继承覆盖方法,不应该改变方法参数列表和返回值类型,须确保不影响原有调用代码和相关文档。

class A5:
    def m(self):
        print('A.m')
    def do(self):
        self.m()

class B5(A5):
    # 如果改变了m的参数,直接死给你看
    def m(self):
        print('B.m')
if __name__ == "__main__":
    b5= B5()
    b5.do()

多继承

多继承允许由多个继承体系,这是曾被疯狂追捧,后来被嫌弃的语言特点。从有点上说,它提供了一种混入(mixin)机制,让既有体系的类型轻松扩展储其他体系的功能。

但另一方面,它却会导致严重的混乱。

比较恶心的是Python可以修改基类__bases__的属性,修改子类的继承方式以及继承顺序。那是相当的没节操

class A6:
    def a(self):...

class B6:
    def b(self):...

class C6:
    def c(self):...

class DD(A6, B6):
    ...

print([i for i in dir(DD) if not i.startswith('_')])
print(DD.__bases__)
# 直接修改一个类继承的父类,就是这么直接,这么干
DD.__bases__ = (C6, B6, A6)
print([i for i in dir(DD) if not i.startswith('_')])
['a', 'b']
(<class '__main__.A6'>, <class '__main__.B6'>)
['a', 'b', 'c']

__mro__查找继承的所有类(菱形继承),专业的说法用了一套C3算法

步骤如下:

1 按"深度优先,从左到右"顺序获取类型表

2. 移除列表中的重复元素,仅保留最后一个

3. 确保子类总在基类前,并保留错继承定义顺序。

在任何时候,都应避免多条继承线存在交叉的现象,并竭力空值多继承和继承深度。这有助于梳理起相互关系,为代码可读和可维护性提供保障。

super 这本书上我学到的大招

该函数返回基类型代理,完成对基类成员的委托访问。

这里由两个参数,第二参数对象提供__mro__列表(如果是实例,提供其类的__mro__列表,实例没有__mro__属性),第一参数则指定起点。

函数总是返回起点为止后一类型(注意是起点位置后一类型)。

这要求第二参数必须是第一参数的实例或子类型,否则第一参数就不会出现在列表中。

同时,第二餐书还是实例和类型方法所需绑定的对象。

每一个字都很重要。

def super(t, o ):
    mro = getattr(o, "__mro__", type(o).__mro__)
    index = mro.index(t) + 1
    return mro[index]

 上面些的算法伪码,只能返回当前的父类,但不能成为绑定的实例和类型方法所需的绑定对象。

如果使用绑定的方法,需要显示传参

In [137]: class A: 
     ...:     @classmethod 
     ...:     def demo(cls):... 
     ...:     def test(self):print('A') 
     ...:      
     ...: class B(A): 
     ...:     def test(self):print('B') 
     ...:      
     ...: class C(B): 
     ...:     def test(self):print('C') 
     ...:                                                                                                                                                          


# 这里直接返回的是第一个参数的上一级类的方法。 In [138]: super(C,C).test Out[138]: <function __main__.B.test(self)> In [139]: super(B,C).test Out[139]: <function __main__.A.test(self)> In [140]: In [140]: o = C() # super第二参数为实例在__self__属性就是自身 In [141]: super(C,o).__self__ == o Out[141]: True # 通过该函数可以调用父类的方法 In [142]: super(C,o).test Out[142]: <bound method B.test of <__main__.C object at 0x1073f42d0>> # 也可以调用父类的类方法 In [143]: super(C,C).demo Out[143]: <bound method A.demo of <class '__main__.C'>> In [144]:

上面比较详细的演示了

不建议直接使用__class__.__base__访问基类。还是把书中错误的例子抄一下

In [144]: class A: 
     ...:     def test(self): 
     ...:         print('A.test') 
     ...:  
     ...: class B(A): 
     ...:     def test(self): 
     ...:         print('B.test') 
          # 当这个是本层级实例调用是没事情,但子类调用就死循环了,因为子列的selfself.__class__.__base__就是B,那就是B.test一直调用自己,形成了递归 ...: self.__class__.__base__.test(self) ...: ...: class C(B): ...: ... ...: In [145]: b=B() In [146]: b.test() B.test A.test In [147]: c= C() In [148]: c.test() B.test B.test
递归

抽象类

这一节不上代码,就上文字,理解了文字应该也能理解代码

抽象类表示部分完成,且不能被实例化的类型

作为一种设计方式,抽象类可用来分离主题框架和局部实现,或将公用和定制解耦。不同与接口纯粹的调用声明,抽象类属于继承树的组成部分,其允许有实现代码。

从抽象类继承,必须实现所有层级为被实现的抽象方法,否则无法创建实例。

定义抽象类,必须继承ABC或者ABCMeta

from abc import ABC, ABCMeta, abstractmethod

class Store(metaclass=ABCMeta):
    
    @abstractmethod
    def change(self):...

 抽象方法上定义@abstractmethod

如果从抽象类继承,未实现全部方法,或添加新的抽象定义,那么该类型依旧是抽象类。另外抽象装饰器还可以用于属性、类型和静态方法。

抽象装饰器写靠近被装饰函数的这一层

既便是抽象类型方法,依然需要实现,否则无法创建实例。

开放类

将已绑定的方法添加到实例名字空间,只能算是字段赋值,算不上添加方法。而将一个函数添加到实例,即便手工设定__self__也无法构成绑定。(types.MethodType,可以给实例添加方法。)

In [1]: class X:...                                                                                                                                                

In [2]: o = X()                                                                                                                                                    

In [3]: o.test = lambda self:None                                                                                                                                  

In [4]: o.test.__self__ = o                                                                                                                                        

In [5]: o.test                                                                                                                                                     
Out[5]: <function __main__.<lambda>(self)>

In [6]: o.test()                                                                                                                                                   
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-365a749fed7a> in <module>
----> 1 o.test()

TypeError: <lambda>() missing 1 required positional argument: 'self'

In [7]: from types import MethodType                                                                                                                               

In [12]: o.test = MethodType(lambda self:None, o)                                                                                                                  

In [13]: o.test()                                                                                                                                                  

In [14]: o.test                                                                                                                                                    
Out[14]: <bound method <lambda> of <__main__.X object at 0x10d1c7c10>>

In [15]:  

                                                                                                                                                                   

 其实书中已经给函数都赋值属性__self__但还是没用,如果一定要用,可以通过Types.MethodType函数

下面是给类添加字段属性,收益与动态成员查找方式,新增的方法对已创建的实例同样有效。也就是所谓的猴子补丁

In [15]: class X:...                                                                                                                                               

In [16]: o = X()                                                                                                                                                   

In [17]: X.a = lambda self:print(f'instance method')                                                                                                               

In [18]: X.b = classmethod(lambda cls:print('class methd'))                                                                                                        

In [19]: X.c = staticmethod(lambda :print('staticmethod'))                                                                                                         

In [20]: o.a                                                                                                                                                       
Out[20]: <bound method <lambda> of <__main__.X object at 0x10d115810>>

In [21]: o.a()                                                                                                                                                     
instance method

In [22]: o.b                                                                                                                                                       
Out[22]: <bound method <lambda> of <class '__main__.X'>>

In [23]: o.b()                                                                                                                                                     
class methd

In [24]: o.c                                                                                                                                                       
Out[24]: <function __main__.<lambda>()>

In [25]: o.c()                                                                                                                                                     
staticmethod

 这很好的解释了猴子补丁,类后期添加的字段,前期的已经创建的实例,后续还能读取到,但这个很危险,很容易引起冲突,少用,慎用。

猜你喜欢

转载自www.cnblogs.com/sidianok/p/12683553.html
今日推荐