属性
类属性和实例属性
属性是面向对象的叫法,与变量一样是用来存放程序运行时需要用到的数据。区别在于,属性一定有一个宿主,根据数组的不同,分为类属性和实例属性:
- 类属性:属性的宿主是类对象,类的实例共享这个属性。任何一个类实例对类属性进行修改,其他类实例访问这个类属性的时候,值也相应的发生变化。
- 实例属性:属性的宿主是实例对象,类的实例和实例之间各自保存实例属性,实例属性的修改仅对修改该属性的实例生效。
申明:为了描述上的方便,下文中遵循下面两个规则:
- 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限制、通过双下划线开头实现私有化。这里就不再赘述。