Python面向对象 - 属性和方法

属性

类属性和实例属性

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

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

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

  • 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限制、通过双下划线开头实现私有化。这里就不再赘述。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容