Python 学习笔记之类与实例

一 定义

类 (class) 封装一组相关数据,使之成为一个整体,并使用一种方法持续展示和维护。

这有点像把零件组装成整车提供给用户,无须了解汽车的内部结构和工作原理,只要知道方向盘,刹车和油门这些外部接口就可以正常行驶。

类存在两种关系

1、继承(inheritance,is-a)自某个族类

继承可以用来表达本车属于某厂的哪个车族系列,除了继承原车系的技术和优势,还可基于某些环境进行改进。

2、组合(composition,has-a)了哪些部件

组合可用来表述该车使用了哪些零部件,比如最新的发动机。

类与模块的不同之处

1、类可生成多个实例。2、类可被继承和扩展。3、类实例的生命周期可控。4、类支持运算符,可按需重载。

这些特性模块没有或者不需要,同时,模块粒度大,模块可用来提供游戏场景级别的解决方案,而类则是该场景下的特定家族和演员。

1.2、创建

定义类,以此为个体为例。关键字 class 同样是运行期指令,用于完成类型对象的创建。

class User:
   pass

可在函数内定义,以限制其作用范围。

类型与实例

如果类在模块中定义,那么其生命周期与模块等同,如果被放在函数内,那么每次都是新建。即便名字和内容相同,也属于不同类型。

def test():
   class X:
       pass
   return X()

>>> a,b = test(),test()
>>> a.__class__ is b.__class__
Out[1]: False

函数内定义的类型对象,在所有实例死亡后,会被垃圾回收。

类型对象除了用来创建实例,也为所有实例定义了基本操作接口,其负责管理整个家族的可共享数据和行为目标。

实例只保存私有特征,其以内部引用从所属类型或其它所属祖先类查找所需的方法,用来驱动展现个体面貌。

名字空间

类型有自己的名字空间,存储当前类型定义的字段和方法。这其中并不包括所继承的祖先成员,其同样以引用关联祖先类型,无须复制到本地。

class A:
   a = 100                 #类字段
   
   def __init__(self, x):  #实例初始化方法
       self.x = x          #实例字段
   
   def_get_x(self):        #实例方法
       return self.x
   
class B(A):                 #继承自A
   b = "hello"
   
   def __init__(self, x, y):
       super().__init__(self,x)    #调用父类的初始化方法
       self.y = y
       
   def get_y(self):
       return self.y

实例 instance o 会保存所有继承层次的实例字段,因为这些都属于其私有数据。

>>> o = B(1,2)
>>> print(A.__dict__)
{
   'a': 100,
   '__init__': <function A.__init__ at 0x109d04f28>, 
   '_get_x': <function A._get_x at 0x109d046a8>, 
   ...
}
>>> print(B.__dict__)
{
   'b': 'hello', 
   '__init__': <function B.__init__ at 0x109d272f0>, 
   'get_y': <function B.get_y at 0x109d27378>, 
   ...
}

当通过实例或类访问某个成员时,会从当前对象开始(instance o 开始查找),依次由近到远向祖先类查找

(即 o --> class B --> class A 进行成员查找)。

如此做的好处就是祖先类的新增功能可以直接 【广播】给所有后代。

在继承层次的不同名字空间中允许有同名成员,并按顺序优先命中。

二 字段

依照所处空间不同,我们将字段分为类型字段实例字段

官方将成员统称为 Attribute,我们可按例将数据当做字段。

2.1、类型字段

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

仅从执行方式来看,无论实例方法存在于哪级类型,其隐式参数 self 总指向当前调用实例。

class A:
    def test(self):             #self 总是指向引用的当前实例,这与继承层次无关
        print(self)
        self.x = 100
        
class B(A):
    pass

if __name__=="__main__":
    o = B()
    print(hex(id(o)))
    print(o.test())
    
0x109d2f358         
<__main__.B object at 0x109d2f358>

实例参数 self 只是约定成俗的名字,这类似于其它语言中的 this,换成 this 同样有效。

2.2、字段赋值

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

class A:      
    pass

if __name__=="__main__":
    A.x = 100          #新增类型字段
    print(A.x)
    print(vars(A))

'''
100
{x': 100, ...}
'''

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

a = A()
a.b = 200
print(a.b)
print(vars(a))
'''
200
{'b': 200}
'''

2.3、私有字段

将私有字段暴露给用户是很危险的。因为无论是修改还是删除都无法截获,由此可能引发意外错误。因为语言没有严格意义上的访问权限设置,所以只好将它们隐藏起来。

如果成员名字以双下划线开头,但没有以双下划线结尾,那么编译器会自动对其重命名。

class X:
    __table = "user"            #类型变量
    
    def __init__(self,name):
        self.name = name        #实例变量
        
    def get_name(self):
        return self.name

同时双下划线开头课结尾的,通常是系统方法,比如 __ init __ ,__ hash __ ,__ main __等。

所谓重命名,就是编译器附加了类型名称前缀。虽然这种做法不能真正阻止用户访问,但基于名字的约定也算一种提示。这种方式让继承类也无法访问。

重命名机制总是针对当前类型,继承类型无法访问重命名后的基类成员。

可将双下划线前缀改为单下划线,这样虽然不能自动重命名,不过提示作用依旧。

class A:
    __name = "user"    #双下划线成员
    
class B(A):
    def test(self):
        print(self.__name)

>>> B().test()
'''
AttributeError: 'B' object has no attribute '_B__name'
'''

class A:
    _name = "user"    #单下划线成员
    
class B(A):
    def test(self):
        print(self._name)
        
>>> B().test()
'''
user
'''

三 属性

对私有字段会进行重命名保护,那公开字段如何处理呢?

问题是核心在于访问拦截,必须由内部逻辑决定如何返回结果。而属性(property)机制就是将读、写和删除操作映射到指定的方法调用上,从而实现操作控制。

class C:
    def __init__(self, name):
        self.__name = name
    
    @property
    def name(self):                 #读
        return self.__name

    @name.setter
    def name(self, value):          #写
        self.__name = value         
        
    @name.deleter
    def name(self):                 #删除
        raise AttributeError("can't delete attribute")

c = C("user")
print(c.name)
# user    
c.name = "abc"
print(c.name)
# abc
del c.name
print(c.name)
#can't delete attribute

这种 @ 语法被称作装饰器(decorator)。 多个方法名必须相同,默认从读方法尅是定义属性,随后以属性名定义写和删除。 如果实现只读,或禁止删除,则只需去掉对应的方法即可。

四、方法

方法是一种特殊函数,其与特定对象绑定,用来获取或修改对象状态。

实际上,无论是对象构造,初始化,析构还是运算符,都以方法实现。根据绑定目标和调用方法的不同,方法可分为实例方法,类型方法,以及静态方法

名字以上下划线开始和结束的方法,通常有特殊用途,其由解释器和内部机制调用。

实例方法

实例方法与实例对象绑定,在其参数列表中,将绑定对象作为第一参数,以便在方法中读取或修改数据状态。在以实例引用调用方法时,无须显式传入第一实参,而由解释器自动完成。

官方建议参数名用 self,同样以 cls 作为类型方法的第一参数名。

    def __init__(self, name):
        self.__name = name
    
    def get(self):              #以实例引用调用,自动传入 self 参数
        return self.__name
    
    def set(self, value):
        self.__name = value
        
>>> w = W("python")
>>> w.get()          #忽略第一参数
'''
python
'''

类型方法

类型方法用来维护类型状态,面向族群提供服务接口。除绑定的第一参数名称不同外,还需添加专门的装饰器,以便解释器将其实例方法区分开来。

class D:
    __data = "D.data"
    
    @classmethod        #定义为类型方法
    def get(cls):        #解释器自动将类型对象 D 作为 cls 参数传入
        return cls.__data
    
    @classmethod
    def set(cls, value):
        cls.__data = value
        
>>> D.get()            #同样忽略 cls 参数
'''
D.data
'''

静态方法

静态方法,则更像是普通函数。其既不接收实例引用,也不参与类型处理,所以就没有自动传入第一参数。使用静态方法,更多原因是将类型作为一个作用域,或者当前类型添加便捷接口。

class DES:
    def __init__(self, key):
        self.__key = key
        
    def encrypt_bytes(self, value):
        return str(self.__key) + str(value)
    
    @staticmethod
    def encrypt(key, s):
        return DES(key).encrypt_bytes(s.encode("utf-8"))
    
>>> DES.encrypt("key", "value")
'''
keyb'value'
'''

特殊方法

下面又解释器自动调用,与对象生命周期相关的方法。

  1. __ new __:构造方法,创建对象实例
  2. __ init __:初始化方法,设置实例的相关属性
  3. __ del __:析构方法,实例被回收时调用

创建实例时,会先调用析构方法初始化方法

class E:
    def __new__(cls, *args):            #与__init__接收相同的调用函数
        print("__new__", args)
        return super().__new__(cls)
    
    def __init__(self, *args):          ##self 由 __new__创建并返回
        print("__init__", args)
        
>>> E(1,2)
'''
__new__ (1, 2)
__init__ (1, 2)
'''

如果 __ new __ 返回实例与 cls 类型不符,将导致 __ init __ 无法执行。

五 总结

学习到此,我总算把类的创建,属性和方法等弄清楚了,我最想强调一点,希望读者把 实例 self 参数弄明白,后续编码过程中使用较多。

还要清楚实例方法和静态方法的区别。

下一节将详细介绍类的继承及重载。

PS:文章中有你没掌握的吗?留言告诉我!

END

原文发布于微信公众号 - Python梦工厂(AzMark950831)

原文发表时间:2018-07-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏猿人谷

《C++ primer》--第11章

习题11.1 algorithm头文件定义了一个count的函数,其功能类似于find。这个函数使用一对迭代器和一个值做参数,返回这个值出现次数的统计结果。编写...

20350
来自专栏大闲人柴毛毛

图的邻接表示法Java版

边节点 ? 一个边节点有一条边 和 一个终止节点组成。 /** * 边节点(由一条边和一个终止节点构成) */ class ENode{ i...

37370
来自专栏小樱的经验随笔

记一次拿webshell踩过的坑(如何用PHP编写一个不包含数字和字母的后门)

这一串代码描述是这样子,我们要绕过A-Za-z0-9这些常规数字、字母字符串的传参,将非字母、数字的字符经过各种变换,最后能构造出 a-z 中任意一个字符,并且...

21020
来自专栏老马说编程

计算机程序的思维逻辑 (12) - 函数调用的基本原理

栈 上节我们介绍了函数的基本概念,在最后我们提到了一个系统异常java.lang.StackOverflowError,栈溢出错误,要理解这个错误,我们需要理...

223100
来自专栏PHP实战技术

你应该这个姿势学习PHP(1)

  应用场景:能防止sql的注入(当然并不完全是可以,我们可以使用pdo进行预处理然后方式sql的注入,安全不能只靠一种方式防止事情的发生)

536170
来自专栏Java帮帮-微信公众号-技术文章全总结

Java基础-11总结Eclipse使用,API,Object类

1:Eclipse的概述使用(掌握) 1:Eclipse的安装 2:用Eclipse写一个HelloWorld案例,最终在控制台输出你的名字 A:创建项目 ...

36660
来自专栏Pulsar-V

GTK基础操作类

1 类型定义 整数类型:gint8、guint8、gint16、guint16、gint31、guint32、gint64、guint64。不是所有的平台都提...

28650
来自专栏用户2442861的专栏

Java String类型含普通字符以及中文字符,计算等价的中文字符串长度

        向Oracle数据库中一varchar2(64)类型字段中插入一条String类型数据,程序使用String.length()来进行数据的长度校...

37420
来自专栏C/C++基础

C++命名方式建议

一个大型项目,参与开发人员众多,每个人的编码风格迥异,为保持代码风格统一,提高代码可读性与可维护性,一个重要的约定就是命名方式。良好统一的命名方式能让我们在不需...

7740
来自专栏前端进阶之路

JS学习系列 07 - 标签声明(Label Statement)

我想要当 j = 2 的时候就退出所有的for语句,打印最后的 done ,你会怎么做?

11820

扫码关注云+社区

领取腾讯云代金券