22.面向对象编程基础

一、面向对象编程:OOP

面向过程:分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用,不支持丰富的“面向对象”特性(比如继承、多态)

函数式编程:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可

面向对象和面向过程的优缺点对比

面向过程

优点

    复杂的问题流程化,进而简单化(一个复杂的问题,分成一个个小的步骤去实现,实现小的步骤将会非常简单)
    举个典型的面向过程的例子, 写一个数据远程备份程序, 分三步,本地数据打包,上传至云服务器,测试备份文件可用性。

 缺点

    一套流水线或者流程就是用来解决一个问题,比如生产汽水的流水线无法生产汽车,即便是能,也得是大改,改一个组件,与其相关的组件都需要修改,牵一发而动全身,扩展性极差。
    比如我们修改了步骤二的函数cloud_upload的逻辑,那么依赖于步骤二结果才能正常执行的步骤三的函数data_backup_test相关的逻辑也需要修改,
   这就造成了连锁反应,而这一弊端会随着程序的增大而变得越发的糟糕,我们程序的维护难度将会越来越大。

应用场景

面向过程的程序设计思想一般用于那些功能一旦实现之后就很少需要改变的场景, 如果你只是写一些简单的脚本,去做一些一次性任务,用面向过程的方式是极好的,
著名的例子有Linux內核,git,以及Apache HTTP Server等。但如果你要处理的任务是复杂的,且需要不断迭代和维护 的, 那还是用面向对象最方便了。

面向对象

优点:

解决了面向过程可扩展性低的问题,需要强调的是,对于一个软件质量来说,面向对象的程序设计并不代表全部,面向对象的程序设计只是用来解决扩展性问题。

 缺点:

编程的复杂度远高于面向过程,不了解面向对象而立即上手并基于它设计程序,极容易出现过度设计的问题,而且在一些扩展性要求低的场景使用面向对象会徒增编程难度,
比如管理linux系统的shell脚本程序就不适合用面向对象去设计,面向过程反而更加适合。

应用场景:

当然是应用于需求经常变化的软件中,一般需求的变化都集中在用户层,互联网应用,企业内部软件,游戏等都是面向对象的程序设计大显身手的好地方。

为什么要用面向对象?

 

1.使程序更加容易扩展和易更改,使开发效率变的更高

2.基于面向对象的程序可以使它人更加容易理解你的代码逻辑,从而使团队开发变得更从容。

 面向对象名词解释

类:一个类即是对一类拥有相同属性的对象的抽象、蓝图、原型、模板。在类中定义了这些对象的都具备的属性(variables(data))、共同的方法

  属性:人类包含很多特征,把这些特征用程序来描述的话,叫做属性,比如年龄、身高、性别、姓名等都叫做属性,一个类中,可以有多个属性

  方法:人类不止有身高、年龄、性别这些属性,还能做好多事情,比如说话、走路、吃饭等,相比较于属性是名词,说话、走路是动词,这些动词用程序来描述就叫做方法。

  实例(对象):一个对象即是一个类的实例化后实例,一个类必须经过实例化后方可在程序中调用,一个类可以实例化多个对象,每个对象亦可以有不同的属性,就像人类是指所有人,每个人是指具体的对象,人与人之前有共性,亦有不同

  实例化:把一个类转变为一个对象的过程就叫实例化

面向对象三大特性

1,Encapsulation 封装

所谓封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。。封装是面向对象的特征之一,是对象和类概念的主要特性。 简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。

2,Inheritance 继承

  

所谓继承指提供了同一类对象共性的处理方法,子类继承父类共性的东西。 这样有利于代码的复用性,即子类拥有父类的方法。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。

3,Polymorphism 多态

  多态是面向对象的重要特性,简单点说:“一个接口,多种实现”,指一个基类中派生出了不同的子类,且每个子类在继承了同样的方法名的同时又对父类的方法做了不同的实现,这就是同一种事物表现出的多种形态。

常用术语

封装

封装是面向对象编程的一大特点,面向对象编程的第一步将属性和方法封装到一个抽象类中,外界使用类创建对象然后让对象调用方法,对象方法的的细节都被封装在类的内部。

封装的优点与原则

【优点】 

将变化隔离; 便于使用;提高复用性; 提高安全性。

【封装原则】

将不需要对外提供的内容都隐藏起来; 把属性都隐藏,提供公共方法对其访问。

派生/继承:

派生描述了子类的创建,新类保留已存类类型中所有需要的数据和行为,但允许修改或者其它的自定义操作,都不会修改原类的定义;

继承描述了子类属性从祖先类继承这样一种方式

 父类也叫基类,派生类也叫子类

多态

 

多态指的是同一种事物的多种状态:水这种事物有多种不同的状态:冰,水蒸气

多态性的概念指出了对象如何通过他们共同的属性和动作来操作及访问,而不需考虑他们具体的类。即有多种形态

冰,水蒸气,都继承于水,它们都有一个同名的方法就是变成云,但是冰.变云(),与水蒸气.变云()是截然不同的过程,虽然调用的方法都一样

泛化/特化:

泛化表示所有子类与其父类及祖先类有一样的特点;

特化描述所有子类的自定义,也就是什么属性让它与其祖先类不同

自省/反射:

 

 自省也称作反射,这个性质展示了某对象是如何在运行期取得自身信息的。如果传一个对象给你,你可以查出它有什么能力,这是一项强大的特性。如果Python不支持某种形式的自省功能,dir和type内建函数,将很难正常工作。还有那些特殊属性,像__dict__,__name__及__doc__

 

二、类

 

创建类

1、类是一种数据结构,我们可以用它来定义对象,对象把数据值和行为特性融合在一起

2、python使用class关键字来创建类,简单的类的声明可以是关键字后紧跟类名

3、通常类名的第一个字母大写

class ClassName(bases):
  'class documentation string' #'类文档字符串'
  class_suite #类体

注意:如果没有bases,则创建经典类;bases填写 ClassName的父类;推荐使用新式类,新式类中,object是一切类的父类

 

三、实例

1、如果说类是一种数据结构定义类型,那么实例则声明了一个这种类型的变量

2、类被实例化得到实例,该实例的类型就是这个被实例化的类

 

初始化:通过调用类对象来创建实例

1、创建实例与调用函数类似,调用一个类就创建了它的一个实例

#!/usr/bin/env python
#coding:utf8

class C(object):  #定义类
    foo = 100
 
c = C()   #初始化类
print c

执行结果:

<__main__.C object at 0x050E5670>

 __init__() 构造器方法

当类被调用,实例化的第一步是创建实例对象。一旦对象创建了, Python 检查是否实现了__init__()方法

默认情况下,如果没有定义或覆盖特殊方法__init__(),对实例不会施加任何特别的操作
任何所需的特定操作,都需要程序员实现__init__(),覆盖它的默认行为。

如果__init__()没有实现,则返回它的对象,实例化过程完毕
如果__init__()已经被实现,那么它将被调用,实例对象作为第一个参数(self)被传递进去,像标准方法调用一样。调用类时,传进的任何参数都交给了__init__()

__init__()是实例创建后第一个被调用的方法

1.设置实例的属性可以在实例创建后任意时间进行,但是通常情况下优先在__init__方法中实现

2.__init__也叫构造方法,它在创建一个对象时被自动调用,不需要手动调用
3.__init__(self)中,默认有1个参数名字为self,如果在创建对象时传递了2个实参,那么__init__(self)中除了self作为第一个形参外还需要2个形参,
例如__init__(self,x,y)


4.__init__(self)中的self参数,不需要开发者传递,python解释器会自动把当前的对象引用传递进去

例如:

#!/usr/bin/env python
class AddrBook(object):
    def __init__(self, nm, ph):
        self.name = nm
        self.phone = ph
>>> import AddrBook
>>> bob = AddrBook.AddrBook('bob green', '13322334455') #将bob green和13322334455分别封装到name和phone属性中
>>> bob
<AddrBook.AddrBook object at 0x7f2108f8ba50>
>>> bob.name
'bob green'
>>> bob.phone
'13322334455'

或者:

#!/usr/bin/env python
#coding:utf8

class AddrBook(object):
    def __int__(self,nm,ph):
        self.name =nm
        self.phone = ph



AddrBook.name = ('bob green')
AddrBook.phone = ('13322334455')

print AddrBook.phone
print AddrBook.name

执行结果:

13322334455
bob green

在创建实例后会立即执行对象的__init__方法。该方法必须接受一个位置参数(self),然后可以接受任意数量的必要或可选位置参数,以及任意数量的关键字参数。

__init__方法并没有创建新对象,该操作由__new__完成。该方法旨在为创建后的对象提供初始化数据。

实际上__init__方法并不返回也不应该返回任何值。在Python中所有的__init__都不返回值,如果用return返回值则会导致TypeError错误。

addrbook1.py内容:不建议使用,在类的外面直接使用类的变量名(直接调用),即通过实例.属性名

 
#!/usr/bin/env python
#coding:utf8

class AddrBook(object):
    def __init__(self,nm,ph):
        self.name = nm 
        self.phone = ph 
        
    
if __name__ == "__main__":
    bob = AddrBook("Bob Green","15011223344")
    alice = AddrBook("Alice Smith","13355667788")
    print "%s: %s" % (bob.name,bob.phone) #直接调用bob实例的name和phone属性
    print "%s: %s" % (alice.name,alice.phone) #直接调用alice实例的name和phone属性
 

执行结果:

Bob Green: 15011223344
Alice Smith: 13355667788

addrbook2.py内容:建议使用,即通过self间接调用被封装的内容;执行类中的方法时,需要通过self间接调用被封装的内容

 
#!/usr/bin/env python
#coding:utf8

class AddrBook(object):
    def __init__(self,nm,ph):
        self.name = nm 
        self.phone = ph 
        
        
    def get_name(self):
        return self.name
    def get_phone(self):
        return self.phone
    
    def update_phone(self,newph):
        self.phone = newph
        print "Now ,%s phone number is : %s"  %(self.name,self.phone)
    
if __name__ == "__main__":
    bob = AddrBook("Bob Green","15011223344")
    alice = AddrBook("Alice Smith","13355667788")
    print "%s: %s" % (bob.get_name(),bob.get_phone())
    print "%s: %s" % (alice.get_name(),alice.get_phone())
    bob.update_phone("18612345678")
    print "%s: %s" % (bob.get_name(),bob.get_phone())
 

执行结果:

Bob Green: 15011223344
Alice Smith: 13355667788
Now ,Bob Green phone number is : 18612345678
Bob Green: 18612345678

__new__() 构造器方法

__new__方法实际上在__init__方法之前执行,用于创建类的实例。

然而__init__方法负责在实例创建后对其进行自定义,__new__方法才是实际上创建并返回实例的方法

__new__方法永远是静态的。同样它也无需显式装饰。第一个也是最重要的参数是创建实例所需要的类,按照惯例,命名为cls。

在大多数情况下,__new__方法的其他参数会被完整复制到__init__方法中。参数在调用类构造函数时首先会被传递给__new__方法

(这是由于其先被调用),然后再传递给__init__方法。

在实际应用中,大多数类无需定义__new__方法。该方法在Python中的内置实现已经足够。

只有在通过__new__方法返回当前类的实例时才会执行__init__方法。如果返回的不是当前类的实例,那么就不会调用__init__方法。

这样做主要是因为在某些情况下返回其他类的实例,__init__也会被其他类中定义的__new__方法触发,而执行两个不同类的__init__方法很可能会导致问题。

__new__和__init__的作用

#!/usr/bin/env python
#coding:utf8

class A(object):
    def __init__(self):
        print "这是init方法"

    def __new__(cls, *args, **kwargs):
        print "这是new方法"
        return object.__new__(cls)

A()

执行结果:

这是new方法
这是init方法

总结:

1.__new__至少要有一个参数cls,代表要实例化的类,此参数在实例化时由Python解释器自动提供

2.__new__必须要有返回值,返回实例化出来的实例,这点在自己实现__new__时要特别注意,可以return父类__new__出来的实例,或者直接是object的__new__出来的实例

3.__init__有一个参数self,就是这个__new__返回的实例,__init__在__new__的基础上可以完成一些其它初始化的动作,__init__不需要返回值

4.我们可以将类比作制造商,__new__方法就是前期的原材料购买环节,__init__方法就是在有原材料的基础上,加工,初始化商品环节

注意:

#!/usr/bin/env python
#coding:utf8

class A(object):
    def __init__(self):
        print "这是init方法"

    def __new__(cls, *args, **kwargs):
        print "这是new方法"
        #return object.__new__(cls)

A()

执行结果:

这是new方法

__del__() 解构器方法

 

与 __init__() 方法对应的是 __del__() 方法,__init__() 方法用于初始化 Python 对象,而 __del__() 则用于销毁 Python 对象,即在任何 Python 对象将要被系统回收之时,系统都会自动调用该对象的 __del__() 方法。

Python 采用自动引用计数(ARC)方式来回收对象所占用的空间,当程序中有一个变量引用该 Python 对象时,Python 会自动保证该对象引用计数为 1
当程序中有两个变量引用该 Python 对象时,Python 会自动保证该对象引用计数为 2,依此类推,如果一个对象的引用计数变成了 0,则说明程序中不再有变量引用该对象,
表明程序不再需要该对象,因此 Python 就会回收该对象。

大部分时候,Python 的 ARC 都能准确、高效地回收系统中的每个对象。但如果系统中出现循环引用的情况,比如对象 a 持有一个实例变量引用对象 b,而对象 b 又持有一个实例变量引用对象 a,此时两个对象的引用计数都是 1,而实际上程序已经不再有变量引用它们,系统应该回收它们,此时 Python 的垃圾回收器就可能没那么快,要等专门的循环垃圾回收器(Cyclic Garbage Collector)来检测并回收这种引用循环。

当一个对象被垃圾回收时,Python 就会自动调用该对象的 __del__ 方法。需要说明的是,不要以为对一个变量执行 del 操作,该变量所引用的对象就会被回收,只有当对象的引用计数变成 0 时,该对象才会被回收。因此,如果一个对象有多个变量引用它,那么 del 其中一个变量是不会回收该对象的。

#!/usr/bin/env python
#coding:utf-8

class Item:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    # 定义析构函数
    def __del__(self):
        print('del删除对象')


# 创建一个Item对象,将之赋给im变量
im = Item('鼠标', 29.8)
x = im  # ①
# 打印im所引用的Item对象
del im
print '--------------'

程序中重写了 Item 类的 __del__() 方法,该方法就是 Item 类的析构函数,当系统将要回收 Item 时,系统会自动调用 Item 对象的 __del__() 方法。

上面程序先创建了一个 Item 对象,并将该对象赋值给 im 变量,① 号代码又将 im 赋值给变量 x,这样程序中有两个变量引用 Item 对象,接下来程序执行 del im 代码删除 im 对象,此时由于还有变量引用该 Item 对象,因此程序并不会回收 Item 对象。

运行上面程序,可以看到如下输出结果:

--------------
del删除对象

从上面程序的输出结果可以看到,del im 执行之后,程序并没有回收 Item 对象,只有等到程序执行将要结束时(系统必须回收所有对象),系统才会回收 Item 对象。

如果将程序中 ① 号代码注释掉,再次运行上面程序,将会看到如下输出结果:

del删除对象
--------------

注释掉 ① 号代码之后,当程序执行 del im 之后,此时程序中不再有任何变量引用该 Item 对象,因此系统会立即回收该对象,则无须等到程序结束之前。

 

最后需要说明的是,如果父类提供了 __del__() 方法,则系统重写 __del__() 方法时必须显式调用父类的 __del__() 方法,这样才能保证合理地回收父类实例的部分属性。

四、属性

属性就是属于另一个对象的数据或者函数元素,可以通过句点属性标识法来访问

类属性

 

类属性就是类对象所拥有的属性,它被所有类对象的实例对象所共有,在内存中只存在一个副本,这个和C++中类的静态成员变量有点类似。对于公有的类属性,在类外可以通过类对象和实例对象访问

 类有两种属性:数据属性和函数属性

类的数据属性是所有对象共享的

类的函数数据是绑定给对象用的,称为绑定到对象的方法

例子:

class People(object):
    name = 'Tom'  #公有的类属性
    __age = 12     #私有的类属性

p = People()

print(p.name)           #正确,使用实例访问类属性
print(People.name)      #正确,使用类访问类属性
print(p.__age)            #错误,不能在类外通过实例对象访问私有的类属性
print(People.__age)        #错误,不能在类外通过类对象访问私有的类属性

 

要知道一个类有哪些属性,有两种方法。最简单的是使用 dir()内建函数。另外是通过访问类的字典属性__dict__,这是所有类都具备的特殊属性之一

 函数  dir()  就能查看对象的属性:

#!/usr/bin/env python
#coding:utf8


class Test(object):
    name = 'python'

a = Test()
a.abc = 123
print dir(Test)
print dir(a)

dir()返回的仅是对象的属性的一个名字列表

执行结果:

['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name']
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'abc', 'name']

它返回一个列表,包含所有能找到的属性的名字,这里我们为实例 a 创建了 abc 属性,这里就能看到了。

有些同学会有疑问,为什么我才写了几个属性,结果却多出一堆我不认识的?

因为我们这里用的是新式类,新式类继承于父类 object ,而这些我们没有写的属性,都是在 object 中定义的。

python3中不管有没有显示的去继承object,默认都会继承的

Python2中如果显示的继承object就是新式类,没有继承object就是经典类

如果我们用经典类的话:

为了方便演示,下面都使用经典类,若没有特殊说明,新旧两式基本是一样的。

使用特殊类属性__dict__来查看一下类的属性

#!/usr/bin/env python
#coding:utf8


class Test(object):
    name = 'python'

a = Test()
a.abc = 123
print Test.__dict__
print Test.__dict__

__dict__返回的是一个字典,它的键(keys)是属性名,键值(values)是相应的属性对象的数据值

执行结果:

{'__dict__': <attribute '__dict__' of 'Test' objects>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Test' objects>, 'name': 'python', '__doc__': None}
{'__dict__': <attribute '__dict__' of 'Test' objects>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Test' objects>, 'name': 'python', '__doc__': None}

其中 __doc__ 是说明文档,在类中的第一个没有赋值的字符串就是说明文档,一般在class的第二行写,没有就为空字符串。

__module__ 表示这个类来自哪个模块,我们在主文件中写的类,其值应该为 '__main__',在其他模块中的类,其值就为模块名。

class Test:
    """文档字符串"""
    name = 'scolia'

print Test.__doc__
print Test.__module__

执行结果:

文档字符串
__main__

文档字符串不一定非要用三引号,只是一般习惯上用三引号表示注释。文档注释也可以使用字符串的形式,实例也可以访问,实际访问的是创建它的类的文档字符串,但是子类并不会继承父类的文档字符串,关于继承的问题以后再讲。

除了这两个特殊的属性之外,还有几个常用的,虽然没有显示出来,但也是可以用的。

__dict__

class Test:
    '文档字符串'
    name = 'python'

a = Test()
a.name = 'good'
print Test.__dict__
print a.__dict__

这个属性就是将对象内的属性和值用字典的方式显示出来。这里可以明显的看出来,实例创建了一个同名的属性。

__class__

class Test:
    pass

a = Test()
print a.__class__
print Test.__class__

得到的结果是不一样的

a.__class结果是__main__.Test  ---它表示创建这个实例的类是哪个,这里显示是 __main__ 模块,也就是主文件中的 Test 这个类创建的。

Test.__class__结果是type    ------表示创建类对象的元类是type

特殊的类属性

1、__name__是给定类的字符名字。它适用于那种只需要字符串(类对象的名字),而非类对象本身的情况。 甚至一些内建的类型也有这个属性


2、__doc__是类的文档字符串,与函数及模块的文档字符串相似,必须紧随头行( header line)后的字符串。
文档字符串不能被派生类继承,也就是说派生类必须含有它们自己的文档字符串

3、__base__# 类的第一个父类

4、__bases__用来处理继承,它包含了一个由所有父类组成的元组

5、__dict__属性包含一个字典,由类的数据属性组成 ;访问一个类属性的时候,Python 解释器将会搜索字典以得到需要的属性。 
  如果在__dict__中没有找到, 将会在基类的字典中进行搜索,采用“深度优先搜索”顺序。基类集的搜索是按顺序的, 
  从左到右,按其在类定义时,定义父类参数时的顺序。对类的修改会仅影响到此类的字典;基类的__dict__属性不会被改动的

6、Python 支持模块间的类继承 。1.5 版本中引入了__module__,这样类名就完全由模块名所限定

7、类名.__class__# 实例对应的类(仅新式类中)

 

实例属性(对象属性)

 

#!/usr/bin/env python
#coding:utf8
class People(object):
    address = '山东'  # 类属性

    def __init__(self):
        self.name = 'xiaowang'  # 实例属性
        self.age = 20  # 实例属性


p = People()
p.age = 12  # 实例属性
print(p.address)  # 正确
print(p.name)  # 正确
print(p.age)  # 正确

print(People.address)  # 正确
print(People.name)  # 错误
print(People.age)  # 错误

内建函数 dir()可以显示类属性,同样还可以打印所有实例属性

与类相似,实例也有一个__dict__特殊属性(可以调用 vars()并传入一个实例来获取),它是实例属性构成的一个字典:

类与实例属性对比

1、类属性仅是与类相关的数据值,类属性和实例无关

2、这些值像静态成员那样被引用,即使在多次实例化中调用类,它们的值都保持不变
3、静态成员(类的数据属性)不会因为实例而改变它们的值,除非实例中显式改变它们的

4、类和实例都是名字空间,类是类属性的名字空间,实例则是实例属性的名称空间
5、可采用类来访问类属性,如果实例没有同名的属性的话,你也可以用实例来访问

 

>>> class my(object):
...     myversion=1
...     def __init__(self,name):
...         self.name=name
...
>>> class my(object):
...     myversion=1
...     def __init__(self,name):
...         self.name=name
...     def show(self,age):
...         print "name is %s age is %s"%(self.name,age)
...

>>> c=my('cmustard')
>>> c.name   #访问实例属性
'cmustard'
>>> c.myversion  #访问myversion,实例属性中没有,于是访问的是类属性中的变量
1
>>> c2=my("abner")
>>> c2.show(22)  #访问方法
name is abner age is 22
>>> c.show(18)
name is cmustard age is 18
>>> c2.myversion  #访问myversion,实例属性中没有,于是访问的是类属性中的变量
1
>>> my.myversion  #通过类直接访问类属性
1
>>> c.myversion=100 #在实例c中修改myversion,相当与在实例c中动态添加了一个新的属性
>>> c.myversion
100
>>> c2.myversion  #在实例c2中myversion的值依然没有改变
1
>>> my.myversion=150 #通过类直接修改myversion的值
>>> c.myversion  #实例c中的值没有发生改变,因为实例c中已经有一个名叫myversion的变量了
100
>>> c2.myversion #实例c2中值随着类的改变而改变,因为实例c2中没有myversion这个变量,只能访问类属性中的变量
150 
>>> my.myversion
150
>>>

通过实例(对象)去修改类属性

 

#!/usr/bin/env python
#coding:utf8
class People(object):
    country = 'china'  # 类属性


print(People.country)
p = People()
print(p.country)
p.country = 'janpan'
print(p.country)  # 实例属性会屏蔽掉同名的类属性
print(People.country)
del p.country  # 删除实例属性
print(p.country)

总结

如果需要在类外修改类属性,必须通过类对象去引用然后进行修改。如果通过实例对象去引用,会产生一个同名的实例属性,这种方式修改的是实例属性,不会影响到类属性,
并且之后如果通过实例对象去引用该名称的属性,实例属性会强制屏蔽掉类属性,即引用的是实例属性,除非删除了该实例属性。

核心提示:使用类属性来修改自身(不是实例属性)

1、使用实例属性来试着修改类属性是很危险的。原因在于实例拥有它们自已的属性集,在 Python 中没有明确的方法来指示你想要修改同名的类属性

2、修改类属性需要使用类名,而不是实例名

 

Python 私有化

 

默认情况下,Python中的成员函数和成员变量都是公开的(public),在python中没有类public,private等关键词来修饰成员函数和成员变量。其实,Python并没有真正的私有化支持,但可用下划线得到伪私有。   尽量避免定义以下划线开头的变量!

_xxx:单下划线开始的成员变量叫做保护变量,意思是只有类实例和子类实例能访问到这些变量,需通过类提供的接口进行访问;不能用'from module import *'导入

__xxx :类中的私有变量/方法名 (Python的函数也是对象,所以成员方法称为成员变量也行得通。),
双下划线开始的是私有成员,意思是只有类对象自己能访问,连子类对象也不能访问到这个数据。 避免与子类中的属性命名冲突,无法在外部直接访问(名字重整所以访问不到)
__xxx__ 系统定义名字,前后均有一个双下划线,用户名字空间的魔法对象或属性,代表python里特殊方法专用的标识,如 __init__()代表类的构造函数。
xx_:单后置下划线,用于避免与Python关键词的冲突

 

 在内部,python使用一种 name mangling 技术,将 __membername替换成 _classname__membername,

也就是说,类的内部定义中,所有以双下划线开始的名字都被"翻译"成前面加上单下划线和类名的形式。
例如:为了保证不能在class之外访问私有变量,Python会在类的内部自动的把我们定义的__spam私有变量的名字替换成为_classname__spam

(注意,classname前面是一个下划线,spam前是两个下划线),因此,用户在外部访问__spam的时候就会提示找不到相应的变量。  

python中的私有变量和私有方法仍然是可以访问的;访问方法如下:
私有变量:实例._类名__变量名
私有方法:实例._类名__方法名()

单下划线(_)

1、简单的模块级私有化只需要在属性名前使用一个单下划线字符。这就防止模块的属性用from mymodule import *来加载。

2、这是严格基于作用域的,所以这同样适合于函数。

3、 在一个模块中以单下划线开头的变量和函数被默认当作内部函数;如果使用from a_module import * 导入时,这部分变量和函数不会被导入。

不过值得注意的是,如果使用 import a_module 这样导入模块,仍然可以用a_module._some_var 这样的形式访问到这样的对象

Python中没有真正的私有属性或方法,可以在你想声明为私有的方法和属性前加上单下划线,以提示该属性和方法不应在外部调用.如果真的调用了也不会出错,但不符合规范.

下面看一下例子:

test.py文件内容:

 
num = 100

_num2 = 150

__num3 = 300

def printName():
    print "sy"

def _printName1():
    print "sy1"

def __printName2():
    print "sy2"
 

执行结果:

 
>>> from test import *
>>> num
100
>>> _num2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name '_num2' is not defined
>>> __num3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name '__num3' is not defined
>>> printName()
sy
>>> _printName1()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name '_printName1' is not defined
>>> __printName2()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name '__printName2' is not defined
>>> 
 

只要是带着下划线的,全部都是未定义错误

 现在再看一个例子:import test

 
>>> import test>>> test.num
100
>>> test._num2
150>>> test.__num3
300
>>> test.printName()
sy
>>> test._printName1()
sy1
>>> test.__printName2()
sy2
>>> 
 

这样就可以使用了。

 

lass A:
    def _method(self):
        print('约定为不在类的外面直接调用这个方法,但是也可以调用')
    def method(self):
        return self._method()   
​
a = A()
a.method()
a._method()

输出:

约定为不在类的外面直接调用这个方法,但是也可以调用
约定为不在类的外面直接调用这个方法,但是也可以调用

类A中定义了一个_method方法,按照约定是不能在类外面直接调用它的
为了可以在外面使用_method方法,又定义了method方法,method方法调用_method方法。但是加了_的方法也可以在类外面调用:

 

双下划线(__)

1、由双下划线开始的属性在运行时被“混淆”,所以直接访问是不允许的。实际上,会在名字前面加上下划线和类名 。把类名加上后形成的新的“混淆”结果将可以防止在祖先类或子孙类中的同名冲突。

这种名字混淆的另一个目的,是为了保护__XXX 变量不与父类名字空间相冲突。如果在类中有一个__XXX 属性,它将不会被其子类中的__XXX 属性覆盖。

使用__XXX,子类的代码就可以安全地使用__XXX,而不必担心它会影响到父类中的__XXX。

 
#!/usr/bin/env python
#coding:utf8

class C(object):
    def __init__(self,nm):
        self.__name = nm

    def getName(self):
        return self.__name


c = C('bob')
print c.getName() #类内部可以访问
print c.__name #私有化的数据不能在外部直接使用
 

执行结果:

bob
print c.__name #私有化的数据不能在外部直接使用
AttributeError: 'C' object has no attribute '__name'

 如果坚持要访问的话,可以这样做:不建议

 
#!/usr/bin/env python
#coding:utf8

class C(object):
    def __init__(self,nm):
        self.__name = nm

    def getName(self):
        return self.__name


c = C('bob')
print c.getName() #类内部可以访问
#print c.__name #私有化的数据不能在外部直接使用

#如果坚持要访问的话,可以这样做:不建议
print c._C__name
 

执行结果:

bob
bob

例子:

class A:
​
    def __method(self):
        print('This is a method from class A')
​
    def method(self):
        return self.__method()
​
class B(A):
    def __method(self):
        print('This is a method from calss B')

​
a=A()
b=B()
a.method()
b.method()
 
输出:
 
This is a method from class A
This is a method from class A

在类A中,__method方法其实由于name mangling技术的原因,变成了_A__method,所以在A中method方法返回的是_A__method,B作为A的子类,只重写了__method方法,并没有重写method方法,所以调用B中的method方法时,调用的还是_A__method方法:
​因此,在我们创建一个以"__"两个下划线开始的方法时,这意味着这个方法不能被重写,它只允许在该类的内部中使用。

在A中没有method方法,有的只是_Amethod方法,也可以在外面直接调用,所以python中没有真正的私有化,看下例:
 
a.__method()
#如果你试图调用a.__method,它还是无法运行的,就如上面所说,只可以在类的内部调用__method。


输出:
 
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-b8e0b1bf4d09> in <module>()
----> 1 a.__method()

AttributeError: 'A' object has no attribute '__method'

可以这样调用

a._A__method()

This is a method from class A

在B中重写method

class B(A):
    def __method(self):
        print('This is a method from calss B')
​
    def method(self):
        return self.__method()
#现在B中的method方法会调用_B__method方法:
​
b=B()
b.method()
 
输出:
 
This is a method from calss B

 双下划线开头双下划线结尾的是一些 Python 的特殊对象,如类成员的 __init__、__del__、__add__、__getitem__等。 Python 官方推荐永远不要将这样的命名方式应用于自己的变量或函数,而是按照文档说明来使用。

注意,私有化都是针对外部而言,在类内部,依然可以使用正常的访问方式,在类的外部就必须进行“混淆”了

 

封装

 

一、什么是封装

 

在程序设计中,封装(Encapsulation)是对具体对象的一种抽象,即将某些部分隐藏起来,在程序外部看不到,其含义是其他程序无法调用。

要了解封装,离不开“私有化”,就是将类或者是函数中的某些属性限制在某个区域之内,外部无法调用。

 

二、为什么要封装

 

封装数据的主要原因是:保护隐私(把不想别人知道的东西封装起来)

封装方法的主要原因是:隔离复杂度(比如:电视机,我们看见的就是一个黑匣子,其实里面有很多电器元件,对于用户来说,我们不需要清楚里面都有些元件,电视机把那些电器元件封装在黑匣子里,提供给用户的只是几个按钮接口,通过按钮就能实现对电视机的操作。)

  提示:在编程语言里,对外提供的接口(接口可理解为了一个入口),就是函数,称为接口函数,这与接口的概念还不一样,接口代表一组接口函数的集合体。

三、封装分为两个层面

 

封装其实分为两个层面,但无论哪种层面的封装,都要对外界提供好访问你内部隐藏内容的接口(接口可以理解为入口,有了这个入口,使用者无需且不能够直接访问到内部隐藏的细节,只能走接口,并且我们可以在接口的实现上附加更多的处理逻辑,从而严格控制使用者的访问)

  

第一个层面的封装(什么都不用做):创建类和对象会分别创建二者的名称空间,我们只能用类名.或者obj.的方式去访问里面的名字,这本身就是一种封装。

注意:对于这一层面的封装(隐藏),类名.和实例名.就是访问隐藏属性的接口

 

 

第二个层面的封装:类中把某些属性和方法隐藏起来(或者说定义成私有的),只在类的内部使用、外部无法访问,或留下少量接口(函数)供外部访问。

 

 

 

封装的真谛在于明确地区分内外,封装的属性可以直接在内部使用,而不能被外部直接使用,然而定义属性的目的终归是要用,外部要想用类隐藏的属性,需要我们为其开辟接口,让外部能够间接地用到我们隐藏起来的属性(隐藏+对外接口)

 

先看如何隐藏

在python中用双下划线开头的方式将属性隐藏起来(设置成私有的)

在准备私有化的属性(包括方法、数据)名字前面加两个下划线即可。

类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式:(所以在外部不能用__x来调用了,用_类名__x来调用时可以的)

 

#其实这仅仅这是一种变形操作且仅仅只在类定义阶段发生变形
#类中所有双下划线开头的名称如__x都会在类定义时自动变形成:_类名__x的形式:
 
class A(object):
    __N=0 #类的数据属性就应该是共享的,但是语法上是可以把类的数据属性设置成私有的如__N,会变形为_A__N
    def __init__(self):
        self.__X=10 #变形为self._A__X
    def __foo(self): #变形为_A__foo
        print('from A')
    def bar(self):
        self.__foo() #只有在类内部才可以通过__foo的形式访问到.  

    #定义访问__N的接口函数
    def get_N(self):
        return A.__N
    #定义访问self.__X的即扣函数
    def get_X(self):
        return self.__X

a = A()
# print(A.__N)  #AttributeError: class A has no attribute '__N'

print(A._A__N)  #0  利用变形访问

# print(a.__X) #AttributeError: class A has no attribute '__X'

print(a._A__X) # 10  利用变形访问

print(a.get_N()) #0  利用接口函数

print(a.get_X()) #10  利用接口函数

变形需要注意的问题:

1.这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如a._A__N,即这种操作并不是严格意义上的限制外部访问,仅仅只是一种语法意义上的变形,主要用来限制外部的直接访问。

a = A()
print(a._A__N)
print(a._A__X)
print(A._A__N)
--------输出结果--------
0
10
0

2.变形的过程只在类的定义时发生一次,在定义后的赋值操作,不会变形

a = A() #实例化对象a
print(a.__dict__) #打印变形的内容
a.__Y = 20 #新增Y的值,此时加__不会变形
print(a.__dict__) #打印变形的内容
---------输出结果----------
{'_A__X': 10}
{'_A__X': 10, '__Y': 20} #发现后面的Y并没有变形

 

3.在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的

正常情况下:

class A: #这是正常情况
    def fa(self):
        print("from A")
    def test(self):
        self.fa()
 
class B(A):
    def fa(self):
        print("from B")
 
b = B()
b.test()
--------输出结果----------
from B

看一下把fa被定义成私有的情况:

class A: #把fa定义成私有的,即__fa
    def __fa(self): #在定义时就变形为_A__fa
        print("from A")
    def test(self):
        self.__fa() #只会与自己所在的类为准,即调用_A__fa
 
class B(A):
    def __fa(self): #b调用的是test,跟这个没关系
        print("from B")
 
b = B()
b.test()
-------输出结果---------
from A

四,封装不是单纯意义的隐藏

  1:封装数据:将数据隐藏起来这不是目的。隐藏起来然后对外提供操作该数据的接口,然后我们可以在接口附加上对该数据操作的限制,以此完成对数据属性操作的严格控制。

class Teacher:
    def __init__(self,name,age):
        # self.__name=name
        # self.__age=age
        self.set_info(name,age)
 
    def tell_info(self):
        print('姓名:%s,年龄:%s' %(self.__name,self.__age))
    def set_info(self,name,age):
        if not isinstance(name,str):
            raise TypeError('姓名必须是字符串类型')
        if not isinstance(age,int):
            raise TypeError('年龄必须是整型')
        self.__name=name
        self.__age=age
 
 
t=Teacher('egon',18)
t.tell_info()
 
t.set_info('egon',19)
t.tell_info()

2:封装的方法:目的是隔离复杂度

封装方法举例: 

  1. 你的身体没有一处不体现着封装的概念:你的身体把膀胱尿道等等这些尿的功能隐藏了起来,然后为你提供一个尿的接口就可以了(接口就是你的。。。,),你总不能把膀胱挂在身体外面,上厕所的时候就跟别人炫耀:hi,man,你瞅我的膀胱,看看我是怎么尿的。

  2. 电视机本身是一个黑盒子,隐藏了所有细节,但是一定会对外提供了一堆按钮,这些按钮也正是接口的概念,所以说,封装并不是单纯意义的隐藏!!!

  3. 快门就是傻瓜相机为傻瓜们提供的方法,该方法将内部复杂的照相功能都隐藏起来了

  提示:在编程语言里,对外提供的接口(接口可理解为了一个入口),可以是函数,称为接口函数,这与接口的概念还不一样,接口代表一组接口函数的集合体。

隔离复杂度的例子:

#取款是功能,而这个功能有很多功能组成:插卡、密码认证、输入金额、打印账单、取钱
#对使用者来说,只需要知道取款这个功能即可,其余功能我们都可以隐藏起来,很明显这么做
#隔离了复杂度,同时也提升了安全性
 
class ATM:
    def __card(self):
        print('插卡')
    def __auth(self):
        print('用户认证')
    def __input(self):
        print('输入取款金额')
    def __print_bill(self):
        print('打印账单')
    def __take_money(self):
        print('取款')
 
    def withdraw(self):
        self.__card()
        self.__auth()
        self.__input()
        self.__print_bill()
        self.__take_money()
 
a=ATM()
a.withdraw()

注意:

python并不会真的阻止你访问私有的属性,模块也遵循这种约定,如果模块名以单下划线开头,那么from module import *时不能被导入,但是你from module import _private_module依然是可以导入的

其实很多时候你去调用一个模块的功能时会遇到单下划线开头的(socket._socket,sys._home,sys._clear_type_cache),这些都是私有的,原则上是供内部调用的,作为外部的你,一意孤行也是可以用的,只不过显得稍微傻逼一点点

python要想与其他编程语言一样,严格控制属性的访问权限,只能借助内置方法如__getattr__

五、特性(property)

1、什么是特性property

  property是一种特殊的属性,访问它时会执行一段功能(函数)然后返回值(就是一个装饰器)

  注意:被property装饰的属性会优先于对象的属性被使用,而被propery装饰的属性,分成三种:property、被装饰

的函数名.setter、被装饰的函数名.deleter(都是以装饰器的形式)。

class room: #定义一个房间的类
    def __init__(self,length,width,high):
        self.length = length #房间的长
        self.width = width #房间的宽
        self.high = high #房间的高
    @property
    def area(self): #求房间的平方的功能
        return self.length * self.width #房间的面积就是:长x宽
    @property
    def perimeter(self): #求房间的周长的功能
        return 2 * (self.length + self.width) #公式为:(长 + 宽)x 2
    @property
    def volume(self): #求房间的体积的功能
        return self.length * self.width * self.high #公式为:长 x 宽 x 高
 
r1 = room(2,3,4) #实例化一个对象r1
print("r1.area:",r1.area) #可以像访问数据属性一样去访问area,会触发一个函数的执行,动态计算出一个值
print("r1.perimeter:",r1.perimeter()) #同上,就不用像调用绑定方法一样,还得加括号,才能运行
print("r1.volume:",r1.volume) #同上,就像是把运算过程封装到一个函数内部,我们不管过程,只要有结果就行
------------输出结果---------------
r1.area: 6
r1.perimeter: 10
r1.volume: 24

注意:此时的特性arear、perimeter和volume不能被赋值。

r1.area = 8 #为特性area赋值
r1.perimeter = 14 #为特性perimeter赋值
r1.volume = 24 #为特性volume赋值
'''
抛出异常:
    r1.area = 8 #第一个就抛异常了,后面的也一样
AttributeError: can't set attribute
 
'''

2、为什么要用property

  将一个类的函数定义成特性以后,对象再去使用的时候obj.name,根本无法察觉自己的name是执行了一个函数然后

计算出来的,这种特性的使用方式遵循了统一访问的原则。

除此之外,看下

ps:面向对象的封装有三种方式:
【public】
这种其实就是不封装,是对外公开的
【protected】
这种封装方式对外不公开,但对朋友(friend)或者子类(形象的说法是“儿子”,但我不知道为什么大家 不说“女儿”,就像“parent”本来是“父母”的意思,但中文都是叫“父类”)公开
【private】
这种封装对谁都不公开

例子:

class people: #定义一个人的类
    def __init__(self,name,sex):
        self.name = name
        self.sex = sex #p1.sex = "male",遇到property,优先用property
 
    @property #查看sex的值
    def sex(self):
        return self.__sex #返回正真存值的地方
 
    @sex.setter #修改sex的值
    def sex(self,value):
        if not isinstance(value,str): #在设定值之前进行类型检查
            raise TypeError("性别必须是字符串类型") #不是str类型时,主动抛出异常
        self.__sex = value #类型正确的时候,直接修改__sex的值,这是值正真存放的地方
            #这里sex前加"__",对sex变形,隐藏。
 
    @sex.deleter #删除sex
    def sex(self):
        del self.__sex
 
p1 = people("egon","male") #实例化对象p1
print(p1.sex) #查看p1的sex,此时要注意self.sex的优先级
p1.sex = "female" #修改sex的值
print(p1.sex) #查看修改后p1的sex
print(p1.__dict__) #查看p1的名称空间,此时里面有sex
del p1.sex #删除p1的sex
print(p1.__dict__) #查看p1的名称空间,此时发现里面已经没有sex了
-------------------输出结果--------------------
male
female
{'name': 'egon', '_people__sex': 'female'}
{'name': 'egon'}

python并没有在语法上把它们三个内建到自己的class机制中,在C++里一般会将所有的所有的数据都设置为私有的

,然后提供set和get方法(接口)去设置和获取,在python中通过property方法可以实现。

六、 封装与扩展性

封装在于明确区分内外,使得类实现者可以修改封装内的东西而不影响外部调用者的代码;而外部使用用者只知道一

个接口(函数),只要接口(函数)名、参数不变,使用者的代码永远无需改变。这就提供一个良好的合作基础——或者说

,只要接口这个基础约定不变,则代码改变不足为虑。

#类的设计者
class room: #定义一个房间的类
    def __init__(self,name,owner,length,width,high):
        self.name = name
        self.owner = owner
        self.__length = length #房间的长
        self.__width = width #房间的宽
        self.__high = high #房间的高
    @property
    def area(self): #求房间的平方的功能
        return self.__length * self.__width #对外提供的接口,隐藏了内部的实现细节,
                                            # 此时我们想求的是房间的面积就是:长x宽

实例化对象通过接口,调用相关属性得到想要的值:

#类的使用者
r1 = room("客厅","michael",20,30,9) #实例化一个对象r1
print(r1.area) #通过接口使用(area),使用者得到了客厅的面积
-------------输出结果--------------
600 #得到了客厅的面积

扩展原有的代码,使功能增加:

#类的设计者,轻松的扩展了功能,而类的使用者完全不需要改变自己的代码
class room: #定义一个房间的类
    def __init__(self,name,owner,length,width,high):
        self.name = name #房间名
        self.owner = owner #房子的主人
        self.__length = length #房间的长
        self.__width = width #房间的宽
        self.__high = high #房间的高
    @property
    def area(self): #对外提供的接口,隐藏内部实现
        return self.__length * self.__width,
               self.__length * self.__width * self.__high #此时我们增加了求体积,
        # 内部逻辑变了,只需增加这行代码就能简单实现,而且外部调用感知不到,仍然使
        # 用该方法,但是功能已经增加了

对于类的使用者,仍然在调用area接口的人来说,根本无需改动自己的代码,就可以用上新功能:

#类的使用者
r1 = room("客厅","michael",20,30,9) #实例化一个对象r1
print(r1.area) #通过接口使用(area),使用者得到了客厅的面积
--------------输出结果---------------
(600, 5400) #得到了新增的功能的值

参考:https://www.cnblogs.com/wgDream/p/6749643.html

https://blog.csdn.net/ZY3099492099/article/details/81021468

 https://www.cnblogs.com/-abm/p/9930663.html

https://www.cnblogs.com/Michael--chen/p/6740455.html

https://www.cnblogs.com/wj-1314/p/8695557.html

原文地址:https://www.cnblogs.com/zhongguiyao/p/11048687.html