美文网首页
Python面向对象 - 属性和方法

Python面向对象 - 属性和方法

作者: 大聖Jonathan | 来源:发表于2018-03-15 08:51 被阅读0次

    属性

    类属性和实例属性

    属性是面向对象的叫法,与变量一样是用来存放程序运行时需要用到的数据。区别在于,属性一定有一个宿主,根据数组的不同,分为类属性和实例属性:

    • 类属性:属性的宿主是类对象,类的实例共享这个属性。任何一个类实例对类属性进行修改,其他类实例访问这个类属性的时候,值也相应的发生变化。
    • 实例属性:属性的宿主是实例对象,类的实例和实例之间各自保存实例属性,实例属性的修改仅对修改该属性的实例生效。

    申明:为了描述上的方便,下文中遵循下面两个规则:

    • class.xxxxx - 表示类属性
    • obj.xxxxx - 表示实例属性

    类属性和实例属性的定义方式分别有两种,一种在类内部添加,另一种是在类外部添加,如下面代码所示:

    class ChinesePeople:
        # Class Attribute
        country = 'China'
    
        def __init__(self, name):
            # Instance Attribute
            self.name = name
    
    p1 = ChinesePeople('Bill')
    print(p1.name, p1.country, ChinesePeople.country)
    
    p1.age = 18
    ChinesePeople.color = 'yellow'
    print(p1.age, p1.color, ChinesePeople.color)
    
    #### Outputs ###
    Bill China China
    18 yellow yellow
    

    上面的示例代码涉及到属性的创建和访问。对于属性的删除,用关键字del即可,即del class.attr/del obj.attr。实例属性只能通过实例对象来访问,但是类属性即可以通过类对象也可以通过实例对象来访问。之所以这样,这和属性的存储和查找是关联的。

    在Python中,属性和方法都是保存在dict这个内置的字典中。相应的,类属性则保存在class.dict中,实例属性保存在obj.dict中。因此,对于类属性和实例属性的访问,遵循以下规则:

    • class.attr:通过类对象访问类属性,那么直接去class.dict中查找,找不到则抛出异常。
    • obj.attr:通过实例对象访问实例属性或者类属性,遵循一样的顺序。也就是,先从obj.dict中查找,如果找不过则从class.__dict__(实际是obj.__class__.__dict__)中查找,如果还是找不到,则抛出异常。
    • 相应的,当适用del关键字删除实例属性或者类属性的时候,对应的从相应的dict中删除对应项。

    我们可以通过打印出dict的值来了解上面的规则:

    print(p1.__dict__)
    print(ChinesePeople.__dict__)
    
    del p1.name
    del ChinesePeople.country
    
    print(p1.__dict__)
    print(ChinesePeople.__dict__)
    
    #### Outputs ###
    # p1.__dict__
    {'name': 'Bill', 'age': 18} 
    
    #ChinesePeople.__dict__
    {'__module__': '__main__', 'country': 'China', '__init__': <function ChinesePeople.__init__ at 0x10df72a60>, '__dict__': <attribute '__dict__' of 'ChinesePeople' objects>, '__weakref__': <attribute '__weakref__' of 'ChinesePeople' objects>, '__doc__': None, 'color': 'yellow'} 
    
    # p1.__dict__
    {'age': 18}
    
    #ChinesePeople.__dict__
    {'__module__': '__main__', '__init__': <function ChinesePeople.__init__ at 0x10d6c5a60>, '__dict__': <attribute '__dict__' of 'ChinesePeople' objects>, '__weakref__': <attribute '__weakref__' of 'ChinesePeople' objects>, '__doc__': None, 'color': 'yellow'}
    

    上面提到,通过实例对象访问类属性是完全没有问题的,而且实际中也经常这么做。那可不可以通过实例对象来修改类属性呢?答案是不可以的,这样做相当于添加了一个实例属性,这一点也可以通过查看obj.dict得以验证。

    print(p1.__dict__)
    p1.country = 'Great China'
    print(p1.__dict__)
    
    #### Outputs ###
    {'name': 'Bill'}
    {'name': 'Bill', 'country': 'Great China'}
    
    限制属性的添加

    python中添加一个属性很灵活,但有时候作为类的创建者,并不希望类的使用者在对类添加额外的属性,或者对添加的属性进行限制,这种情况下我们只需要对slots列表赋值即可:

    • 如果slots赋值成空列表,那么就不允许在类的内部或者外部添加任何属性。
    • 如果允许添加特定名字的属性,那只需要把这些名字存放在slots中。

    需要注意的是:

    • slots只能对实例属性起限制的作用
    • 定义了slots以后,实例属性的读取就不再通过obj.dict来获取
    • 如果类属性与slots中的变量同名,则该类属性被设置为readonly,并且会覆盖同名的实例属性
    class ChinesePeople:
        # Class Attribute
        country = 'China'
    
        def __init__(self, name):
            # Instance Attribute
            self.name = name
    
        __slots__ = ['name']
    
    p1.age = 18 # AttributeError: 'ChinesePeople' object has no attribute 'age'
    
    属性的访问权限

    与C#, Java不一样,python中并没有像private/protect/public这样的关键字来修饰属性活着方法的访问权限。那在python中如何实现属性的私有化和只读?

    在python中,如果一个属性是以双下划线开头(例如__name),那么这属性就是私有的(注意,这里要和类内置的属性区分开,例如前面提到的dict)。来看看下面的代码:

    class ChinesePeople:
        # Class Attribute
        country = 'China'
        __province = 'Taiwan'
    
        def __init__(self, name):
            # Instance Attribute
            self.name = name
            self.__salary = 5000
    
    p1 = ChinesePeople('Bill')
    print(ChinesePeople.__province) # AttributeError: type object 'ChinesePeople' has no attribute '__province'
    print(p1.__salary) # AttributeError: 'ChinesePeople' object has no attribute '__salary'
    

    下面,我们先分别来看看class.dict和obj.dict的值,然后再来谈谈python中的私有化属性。

    print(p1.__dict__)
    print(ChinesePeople.__dict__)
    
    #### Outputs ###
    {'name': 'Bill', '_ChinesePeople__salary': 5000}
    {'__module__': '__main__', 'country': 'China', '_ChinesePeople__province': 'Taiwan', '__init__': <function ChinesePeople.__init__ at 0x1073e3a60>, '__dict__': <attribute '__dict__' of 'ChinesePeople' objects>, '__weakref__': <attribute '__weakref__' of 'ChinesePeople' objects>, '__doc__': None}
    

    通过上面代码的输出,我们一定会产生一个疑问,_ChinesePeople__salary和_ChinesePeople__province是什么鬼,我们明明没有定义这个两个属性,我们定义的是__salary和__province,这是怎么回事?

    其实,python中没有私有化属性的概念,上面提到的私有化,实际上是"伪私有化"。当python解释器遇到以双下划线开头的属性,那么会对这个属性进行重命名,也就是在这个属性前面添加classname_。例如,将__salary重命名为_ChinesePeople__salary。这样,当我们通过__xxxx访问一个属性的时候:

    • 如果是在类内部访问,那么解释也会做相应的转化。也就是说,访问obj.__xxxx/class.__xxxx时,会转换成访问obj._classname__xxxx/class.__classname__。很显然,被重命名过的属性,是可以在dict中找到。
    • 如果是在类外部访问,那么解释器就不做转化,所以也就不能从dict中找到__xxxx。

    对于属性的只读限制,这里先提供一下实现的方式,由于涉及到装饰器和描述器的概念,后面将会有特定的笔记来说明。实现只读属性,大概的思路有下面三种:

    • 将属性设置为私有属性,而后通过get方法进行读取
    • 通过@property这个装饰器来实现
    • 通过描述器来实现

    方法

    方法和函数

    比起属性和变量,方法和函数好像更复杂一点。从本质上讲,方法和函数都是可调用的对象,它们的区别在于:

    • 函数(function):没有隐式的参数。简单点说,如果一个函数需要传递进去两个参数,那么调用者必须传递两个参数,解释器不会帮忙隐式传递
    • 方法(method):方法的第一个参数是self或者cls,分别表示类的实例对象和类对象。对于调用者而言,第一个不需要显示传递,因为解释器已经帮忙传递了第一个参数

    来看看下面的代码:

    def hello(self, name):
        print(self,',', name)
    
    class ChinesePeople:
         # Class Attribute
        country = 'China'
        
        def __init__(self, name):
            # Instance Attribute
            self.name = name
       
        def say_hi(self, name):
            print(self,',', name)
        
    p1 = ChinesePeople('Bill')
    
    print(hello, p1.say_hi)
    hello(1000, 'Jim')
    p1.say_hi('Jim')
    
    #### Outputs ###
    <function hello at 0x00000245A3FC6D08> <bound method ChinesePeople.say_hi of <__main__.ChinesePeople object at 0x00000245A3FF6E48>>
    1000 , Jim
    <__main__.ChinesePeople object at 0x00000245A3FF6E48> , Jim
    

    上面的代码中,hello就是一个function,而类中定义的say_hi则是一个method。通过实例对象调用say_hi的时候,解释器帮忙传递了self,因此我们只需要显示的传递一个参数。(其实这里参数的名字定义为self,只是为了好理解,其他的名字也是没有问题,但建议使用self)

    实例方法,类方法和静态方法

    上面提到了方法和函数的差别在于解释器是否帮忙传递第一个参数。那根据解释器传递的第一个参数的值的不同,又分为下面三种方法:

    • 实例方法:第一个参数传递的是实例对象
    • 类方法:第一个参数传递的是类对象
    • 静态方法:不存在隐式传递的第一个参数 (这个其实就等价于函数了)

    以上三种方法,都是需要通过各自的装饰器来定义,如下面的代码:

    def hello(self, name):
        print(self,',', name)
    
    class ChinesePeople:
         # Class Attribute
        country = 'China'
        
        def __init__(self, name):
            # Instance Attribute
            self.name = name
       
        def say_hi(self, name):
            print(self,',', name)
        
        @classmethod
        def class_say_hi(cls, name):
            print(cls,',', name)
        
        @staticmethod
        def static_say_hi(name):
            print(name)
        
    p1 = ChinesePeople('Bill')
    
    print(p1.say_hi, ChinesePeople.class_say_hi, ChinesePeople.static_say_hi)
    print(p1.say_hi, p1.class_say_hi, p1.static_say_hi)
    p1.say_hi('Jim')
    p1.class_say_hi('Jim')
    p1.static_say_hi('Jim')
    
    ChinesePeople.class_say_hi('Jim')
    ChinesePeople.static_say_hi('Jim')
    
    #### Outputs ###
    <bound method ChinesePeople.say_hi of <__main__.ChinesePeople object at 0x00000245A4036710>> <bound method ChinesePeople.class_say_hi of <class '__main__.ChinesePeople'>> <function ChinesePeople.static_say_hi at 0x00000245A403F510>
    <bound method ChinesePeople.say_hi of <__main__.ChinesePeople object at 0x00000245A4036710>> <bound method ChinesePeople.class_say_hi of <class '__main__.ChinesePeople'>> <function ChinesePeople.static_say_hi at 0x00000245A403F510>
    <__main__.ChinesePeople object at 0x00000245A4036710> , Jim
    <class '__main__.ChinesePeople'> , Jim
    Jim
    <class '__main__.ChinesePeople'> , Jim
    Jim
    

    从上面代码可知,不论是实例方法、类方法和静态方法,都是可以通过实例对象来访问,并且解释器都是会正确的传递一个参数;但是对于类对象而言,是无法调用实例方法。

    动态添加方法

    上文属性的部分,我们了解到属性是可以在类外部动态来添加。那对于方法而言,同样的方式是否适用?

    def hello(self, name):
        print(self,',', name)
        
        
    def hello1(self, name):
        print(self,',', name)
        
    
    class ChinesePeople:
         # Class Attribute
        country = 'China'
        
        def __init__(self, name):
            # Instance Attribute
            self.name = name
        
        def say_hi(self, name):
            print(self, ',', name)
    
        
    p1 = ChinesePeople('Bill')
    
    p1.hello = hello
    ChinesePeople.hello1 = hello1
    
    print(hello, p1.hello)
    print(p1.hello1, ChinesePeople.hello1)
    print(p1.say_hi, ChinesePeople.say_hi)
    
    #### Outputs ###
    <function hello at 0x00000245A40702F0> <function hello at 0x00000245A40702F0>
    
    <bound method hello1 of <__main__.ChinesePeople object at 0x00000245A406A0F0>> <function hello1 at 0x00000245A40701E0>
    
    <bound method ChinesePeople.say_hi of <__main__.ChinesePeople object at 0x00000245A406A0F0>> <function ChinesePeople.say_hi at 0x00000245A4070378>
    

    从上面的输出,可以得出以下结论:

    • 如果直接在实例对象上通过一个函数来赋值,那么这个函数不会转化成实例方法
    • 直接在类对象上通过一个函数来赋值,接着通过实例对象来调用,那么相当于是实例方法(类似于在类内部定义了一个实例方法)。

    对于类方法和静态方法的添加,也是类似,前提是要在相应的方法上加上@classmethod和@staticmethod装饰器即可。

    思考一个问题,在上述代码基础上,再创建一个p2实例,而后分别调用hello和hello1,是什么结果?(自己动手,丰衣足食)。

    上面的添加的实例方法,是作用到类的所有实例中。如果我们只想对特定的实例添加方法,可以通过types.MethodType把一个函数绑定到特定的实例上:

    import types
    
    def hello1(self, name):
        print(self,',', name)
    
    class ChinesePeople:
        pass
    
    p1 = ChinesePeople()
    p2 = ChinesePeople()
    
    p1.hello1 = types.MethodType(hello1, p1)
    
    
    p1.hello1('name') # ok
    p2.hello1('name') # AttributeError: 'ChinesePeople' object has no attribute 'hello1'
    
    方法的添加限制和私有化

    方法的添加限制和私有化与属性类似,即通过slots限制、通过双下划线开头实现私有化。这里就不再赘述。

    相关文章

      网友评论

          本文标题:Python面向对象 - 属性和方法

          本文链接:https://www.haomeiwen.com/subject/rxfqqftx.html