特性至关重要的地方在于,特性的存在使得开发者可以非常安全并且确定可行地将公共数据属性作为类的公共接口的一部分开放出来。
--- Alex Martelli(Python 贡献者和图书作者)
在 Python 中,数据的属性和处理数据的方法统称属性(attribute)。其实,方法只是可调用的属性。除了这二者之外,我们还可以创建特性(property),在不改变类接口的前提下,使用存取方法(即读值方法和设值方法)修改数据属性。
除了特性,Python 还提供了丰富的 API,用于控制属性的访问权限,以及实现动态属性。使用点号访问属性时(如 obj.attr
),Python 解释器会调用特殊的方法(如 __getattr__
和 __setattr__
) 计算属性。用户自己定义的类可以通过 __getattr__
方法实现“虚拟属性”,当访问不存在的属性时(如 obj.no_such_attribute
),即时计算属性的值。
在接下来的几个示例中,我们要使用动态属性处理一个 JSON 格式数据源。下例是哪个数据源中的4个记录。
{
"Schedule": {
"conferences": [
{
"serial": 115
}
],
"events": [
{
"serial": 34505,
"name": "Why Schools Don ́t Use Open Source to Teach Programming",
"event_type": "40-minute conference session",
"time_start": "2014-07-23 11:30:00",
"time_stop": "2014-07-23 12:10:00",
"venue_serial": 1462,
"description": "Aside from the fact that high school programming...",
"website_url": "http://oscon.com/oscon2014/public/schedule/detail/34505",
"speakers": [
157509
],
"categories": [
"Education"
]
}
],
"speakers": [
{
"serial": 157509,
"name": "Robert Lefkowitz",
"photo": null,
"url": "http://sharewave.com/",
"position": "CTO",
"affiliation": "Sharewave",
"twitter": "sharewaveteam",
"bio": "Robert ́r0ml ́ Lefkowitz is the CTO at Sharewave, a startup..."
}
],
"venues": [
{
"serial": 1462,
"name": "F151",
"category": "Conference Venues"
}
]
}
}
可以看出,整个数据集是一个JSON对象,里面有一个键,名为“Schedule”;这个键对应的值也是一个映像,有4个键:“conferences”, “events”, “speakers” 和 “venues”。这4个键对应的值都是一个记录列表。在上例中,各个列表中只有一条记录。然而,在完整的数据集中,列表中有成百上千条记录。不过,“conferences” 键对应的列表只有一条记录,如上述示例所示。这4个列表中每个元素都有一个名为“serial”的字段,这是元素在各个列表中的唯一标识符。
第一个脚本只用于下载这个数据源。
from urllib.request import urlopen
import warnings
import os
import json
URL = 'http://www.oreilly.com/pub/sc/osconfeed'
JSON = './data/osconfeed.json'
def load():
if not os.path.exists(JSON):
msg = 'downloading {} to {}'.format(URL, JSON)
warnings.warn(msg)
with urlopen(URL) as remote, open(JSON, 'wb') as local:
local.write(remote.read())
with open(JSON) as fp:
return json.load(fp)
feed = load()
print(sorted(feed['Schedule'].keys()))
for key, value in sorted(feed['Schedule'].items()):
print('{:3} {}'.format(len(value), key))
print(feed['Schedule']['speakers'][-1]['name'])
print(feed['Schedule']['speakers'][-1]['serial'])
print(feed['Schedule']['events'][40]['name'])
print(feed['Schedule']['events'][40]['speakers'])
"""
output:
['conferences', 'events', 'speakers', 'venues']
1 conferences
494 events
357 speakers
53 venues
Carina C. Zona
141590
There *Will* Be Bugs
[3471, 5199]
"""
使用动态属性访问 JSON 类数据
feed['Schedule']['events'][40]['name']
这种句法很冗长,我们可以实现一个近似字典的类,来达到 feed.Schedule.events[40].name
这种效果。下例实现了 FrozenJSON
类,只支持读取,即只能访问数据。不过,这个类能递归,自动处理嵌套的映射和列表。
from urllib.request import urlopen
import warnings
import os
import json
URL = 'http://www.oreilly.com/pub/sc/osconfeed'
JSON = './data/osconfeed.json'
def load():
if not os.path.exists(JSON):
msg = 'downloading {} to {}'.format(URL, JSON)
warnings.warn(msg)
with urlopen(URL) as remote, open(JSON, 'wb') as local:
local.write(remote.read())
with open(JSON) as fp:
return json.load(fp)
from collections import abc
class FrozenJSON:
"""一个只读接口,使用属性表示法访问JSON类对象
"""
def __init__(self, mapping):
self.__data = dict(mapping)
def __getattr__(self, name):
if hasattr(self.__data, name):
return getattr(self.__data, name)
else:
return FrozenJSON.build(self.__data[name])
@classmethod
def build(cls, obj):
if isinstance(obj, abc.Mapping):
return cls(obj)
elif isinstance(obj, abc.MutableSequence):
return [cls.build(item) for item in obj]
else:
return obj
raw_feed = load()
feed = FrozenJSON(raw_feed)
print(len(feed.Schedule.speakers))
print(sorted(feed.Schedule.keys()))
"""
output:
357
['conferences', 'events', 'speakers', 'venues']
"""
处理无效属性名
FrozenJSON
类有个缺陷:没有对名称为 Python 关键字的属性做特殊处理,比如像下面这样构建一个对象:
grad = FrozenJSON({'name': 'Jim Bo', 'class': 1982})
print (grad.class)
此时无法读取 grad.class
的值,因为在 Python 中 class 是保留字。
但是,FrozenJSON
类的目的是为了便于访问数据,因此更好的方法是检查传给 FrozenJSON.__init__
方法的映射中是否有键的名称为关键字,如果有,那么在键名后加上 _,然后透过下述方式读取:
print(grad.class_)
"""
output:
1982
"""
为此,我们可以修改 __init__
方法:
def __init__(self, mapping):
self.__data = {}
for key, value in mapping.items():
if keyword.iskeyword(key):
key += '_'
self.__data[key] = value
对动态属性的名称做了一些处理之后,我们要分析 FrozenJSON
类的另一个重要功能 — 类方法 build 的逻辑。这个方法的嵌套结构转换成 FrozenJSON
实例或 FrozenJSON
实例列表,因此 __getattr__
方法使用这个方法访问属性时,能为不同的值返回不同类型的对象。
除了在类方法中实现这样的逻辑之外,还可以在特殊的 __new__
方法中实现。
使用 __new__
方法以灵活的方式创建对象
我们通常把 __init__
称为构造方法。其实,用于构建实例的是特殊方法 __new__
: 这是个类方法(使用特殊方式处理,因此不必使用 @classmethod
装饰器),必须返回一个实例。返回的实例会作为第一个参数(即 self) 传给 __init__
方法。因为调用 __init__
方法时要传入实例,而且禁止返回任何值,所以 __init__
方法其实是“初始化方法”。真正的构造方法是 __new__
。我们几乎不需要自己编写 __new__
方法,因为从 object
类继承的实现已经足够了。
刚才说明的过程,即从 __new__
方法到 __init__
方法,是最常见的,但不是唯一的。__new__
方法也可以返回其他类的实例,此时,解释器不会调用 __init__
方法。
也就是说,Python构建对象的过程可以使用下述伪代码概括:
# 构建对象的伪代码
def object_maker(the_class, some_arg):
new_object = the_class.__new__(some_arg)
if isinstance(new_object, the_class):
the_class.__init__(new_object, some_arg)
return new_object
下例是 FrozenJSON
类的另一个版本,把之前在类方法 build
中的逻辑移到了 __new__
方法中。
class FrozenJSON:
"""一个只读接口,使用属性表示法访问JSON类对象
"""
def __init__(self, mapping):
self.__data = {}
for key, value in mapping.items():
if keyword.iskeyword(key):
key += '_'
self.__data[key] = value
def __getattr__(self, name):
if hasattr(self.__data, name):
return getattr(self.__data, name)
else:
return FrozenJSON.build(self.__data[name])
def __new__(cls, arg):
if isinstance(arg, abc.Mapping):
return super().__new__(cls)
elif isinstance(arg, abc.MutableSequence):
return [cls(item) for item in arg]
else:
return obj
这个 JSON 数据源有一个明显的缺点:索引为 40 的事件,即名为’There Will Be Bugs‘ 的那个,有两位演讲者,3471和5199,但却不容易找到他们,因为提供的是编号,而 Schedule.speakers
列表没有使用编号建立索引。此外,每条事件记录中都有 venue_serial
字段,存储的值也是编号,但是如果想找到对应的记录,那就要线性搜索 Schedule.venues
列表。接下来的任务hi,调整数据结构,以便自动获取所链接的记录。
使用 shelve
模块调整数据源的结构
shelve.open
高阶函数返回一个 shelve.Shelf
实例,这是简单的键值对象数据库,背后由 dbm
模块支持,具有下述特点。
shelve.Shelf
是abc.MutableMapping
的子类,因此提供了处理映射类型的重要方法。- 此外,
shelve.Shelf
类还提供了几个管理 I/O 的方法,如sync
和close
;他也是一个上下文管理器。 - 只要把新值赋予键,就会保存键和值。
- 键必须是字符串
- 值必须是
pickle
模块能处理的对象。
shelve
模块为识别 OSCON 的日程数据提供了一种简单有效的方式。我们将从 JSON 文件中读取所有记录,将其存在一个 shelve.Shelf
对象中,键由记录类型和编号组成(例如,event.33950
或 speaker.3471
), 而值是我们即将定义的 Record
类的实例。
import warnings
DB_NAME = './data/schedule1_db'
CONFERENCE = 'conference.115'
class Record:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def load_db(db):
raw_data = load()
warnings.warn('loading ' + DB_NAME)
for collection, rec_list in raw_data['Schedule'].items():
record_type = collection[:-1]
for record in rec_list:
key = '{}.{}'.format(record_type, record['serial'])
record['serial'] = key
db[key] = Record(**record)
import shelve
db = shelve.open(DB_NAME)
if CONFERENCE not in db:
load_db(db)
speaker = db['speaker.3471']
print(type(speaker))
print(speaker.name, speaker.twitter)
db.close()
为什么之前不用 Record
类而是用更复杂的 FrozenJSON
类。原因有两个。第一,FrozenJSON
类要递归转换嵌套的映射和列表;而 Record
类不需要这么做,因为转换好的数据集中没有嵌套的映射和列表,记录中只有字符串,整数,字符串列表和整数列表。第二,FrozenJSON
类要访问内嵌的 __data
属性(值是字典,用于调用 keys
等方法),而现在我们也不需要这么做了。
像上面那样调整日程数据集之后,我们可以扩展 Record
类,让它提供一个有用的服务:自动获取 event
记录引用的 venue
和 speaker
记录。这与 Django ORM
访问 models.ForeignKey
字段时所做的事类似:得到的不是键,而是链接的模型对象。
使用特性获取链接的记录
首先将指出本节几个重要的类:
-
Record
__init__
方法与上面的脚本一样;为了辅助测试,增加了__eq__
方法。 -
DbRecord
Record 类的子类,添加了
__db
类属性,用于设置和获取__db
属性的set_db
和get_db
静态方法,用于从数据库中获取记录的fetch
类方法,以及辅助调试和测试的__repr__
实例方法。 -
Event
DbRecord 类的子类,添加了用于获取所链接记录的
venue
和speakers
属性,以及特殊的__repr__
方法。
DbRecord.__db
类属性的作用是存储打开的 shelve.Shelf
数据库引用,以便在需要使用数据库的 DbRecord.fetch
方法及 Event.venue
和 Event.speakers
属性中使用。我把 __db
设为私有类属性,然后定义了普通的读值方法和设值方法,以防不小心覆盖 __db
属性的值,基于一个重要的原因,我没有使用特性去管理 __db
属性:特性是用于管理实例属性的类属性。
import warnings
DB_NAME = './data/schedule2_db'
CONFERENCE = 'conference.115'
class Record:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __eq__(self, other):
if isinstance(other, Record):
return self.__dict__ == other.__dict__
else:
return NotImplemented
class MissingDatabaseError(RuntimeError):
"""需要数据库但没指定数据库时抛出 """
class DbRecord(Record):
__db = None
@staticmethod
def set_db(db):
DbRecord.__db = db
@staticmethod
def get_db():
return DbRecord.__db
@classmethod
def fetch(cls, ident):
db = cls.get_db()
try:
return db[ident]
except TypeError:
if db is None:
msg = "database not set; call '{}.set_db(my_db)'"
raise MissingDatabaseError(msg.format(cls.__name__))
else:
raise
def __repr__(self):
if hasattr(self, 'serial'):
cls_name = self.__class__.__name__
return '<{} serial={!r}>'.format(cls_name, self.serial)
else:
return super().__repr__()
class MissingDatabaseError(RuntimeError):
"""需要数据库但没指定数据库时抛出 """
class DbRecord(Record):
__db = None
@staticmethod
def set_db(db):
DbRecord.__db = db
@staticmethod
def get_db():
return DbRecord.__db
@classmethod
def fetch(cls, ident):
db = cls.get_db()
try:
return db[ident]
except TypeError:
if db is None:
msg = "database not set; call '{}.set_db(my_db)'"
raise MissingDatabaseError(msg.format(cls.__name__))
else:
raise
def __repr__(self):
if hasattr(self, 'serial'):
cls_name = self.__class__.__name__
return '<{} serial={!r}>'.format(cls_name, self.serial)
else:
return super().__repr__()
def load_db(db):
raw_data = load()
warnings.warn('loading ' + DB_NAME)
for collection, rec_list in raw_data['Schedule'].items():
record_type = collection[:-1]
cls_name = record_type.capitailze()
cls = globals().get(cls_name, DbRecord)
if inspect.isclass(cls) and issubclass(cls, DbRecord):
factory = cls
else:
factory = DbRecord
for record in rec_list:
key = '{}.{}'.format(record_type, record['serial'])
record['serial'] = key
db[key] = factory(**record)
import shelve
db = shelve.open(DB_NAME)
if CONFERENCE not in db:
load_db(db)
DbRecord.set_db(db)
event = DbRecord.fetch('event.33950')
print(event)
print(event.venue)
print(event.venue.name)
for spkr in event.speakers:
print('{0.serial}: {0.name}'.format(spkr))
"""
output:
<Event 'There *Will* Be Bugs'>
<DbRecord serial='venue.1449'>
Portland 251
speaker.3471: Anna Ravenscroft
speaker.5199: Alex Martelli
"""
特性经常用于把公开的属性变成使用读值方法和设值方法管理的属性,且不在影响客户端代码的前提下实施业务规则。
使用特性验证属性
目前,我们只介绍了如何使用 @property
装饰器实现只读特性。本节要创建一个可读写的特性。
LineItem
类第1版:表示订单中商品的类
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
这个类很精简,不过或许太简单了,用户能把 weight
设置成负数,以致计算后的subtotal
也是负数。
LineItem
类第2版:能验证值的特性
实现特性之后,我们可以使用读值方法和设值方法,但是 LineItem
类的接口保持不变(即,设值 LineItem
对象的 weight
属性依然写成 raisins.weight = 12
)
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
@property
def weight(self):
return self.__weight
@weight.setter
def weight(self, value):
if value > 0:
self.__weight = value
else:
raise ValueError('value must be > 0')
现在,我们禁止用户为 weight
属性提供负值或零。但工作人员可能犯错,导致 LineItem
对象的 price
属性为负值。为了防止出现这种情况,我们也可以把 price
属性变成特性,但这样我们的代码中就存在一些重复。
这里,我们要实现一个特性工厂函数。但是,在之前,我们要深入理解特性。
特性全解析
虽然内置的 property
经常用作装饰器,但是它其实是一个类,在 Python 中,函数和类通常可以互换,因为二者都是可调用的对象,而且没有实例化对象的 new
运算符,所以调用构造方法与调用工厂函数没有区别。所以调用构造方法与调用工厂函数没有区别,此外,只要能返回新的可调用对象,代替被装饰的函数,二者都可以用作装饰器。
property
构造方法的完整签名如下:
property(fget=None, fset=None, fdel=None, doc=None)
不适用装饰器定义特性的“经典”句法如下:
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
def get_weight(self):
return self.__weight
def set_weight(self, value):
if value > 0:
self.__weight = value
else:
raise ValueError('value must be > 0')
weight = property(get_weight, set_weight)
某些情况下,这种经典形式比装饰器句法好;稍后讨论的特性工厂函数就是一例。但是,在方法众多的类定义体重使用装饰器的话,一眼就能看出哪些是读值方法,哪些是设值方法,而不用按照惯例,在方法名的前面加上 get
和 set
。
特性会覆盖实例属性
特性都是类属性, 但是特性惯例的其实是实例属性的存取。
如果实例和所属的类有同名数据属性,那么实例属性会覆盖(或称遮盖)类属性。
>>> class Class:
... data = 'the class data attr'
... @property
... def prop(self):
... return 'the prop value'
...
>>> obj = Class()
>>> vars(obj)
{}
>>> obj.data
'the class data attr'
>>> obj.data = 'bar'
>>> vars(obj)
{'data': 'bar'}
>>> obj.data
'bar'
>>> Class.data
'the class data attr'
下面尝试覆盖 obj
实例的 prop
特性。
>>> Class.prop
<property object at 0x10626d860>
>>> obj.prop
'the prop value'
>>> obj.prop = 'foo'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> obj.__dict__['prop'] = 'foo'
>>> vars(obj)
{'data': 'bar', 'prop': 'foo'}
>>> obj.prop
'the prop value'
>>> Class.prop = 'baz'
>>> obj.prop
'foo'
最好再举一个例子,为 Class
类新添一个特性,覆盖实例属性。
>>> obj.data
'bar'
>>> Class.data
'the class data attr'
>>> Class.data = property(lambda self: 'the "data" pro value')
>>> obj.data
'the "data" pro value'
>>> del Class.data
>>> obj.data
'bar'
定义一个特性工厂函数
我们将定义一个名为 quantity
的特性工厂函数。
class LineItem:
weight = quantity('weight')
price = quantity('price')
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
前文说过,特性是类属性。构建各个 quantity
特性对象时,要传入 LineItem
实例属性的名称,让特性管理。可惜,这一行要两次输入单词 weight
:
weight = quantity('weight')
这里很难避免重复输入,因为特性根本不知道要绑定哪个类属性名。赋值语句的右边先计算,因此调用 quantity()
时,weight
类属性还不存在。
def quantity(storage_name):
def qty_getter(instance):
return instance.__dict__(storage_name)
def qty_setter(instance, value):
if value > 0:
instance.__dict__[storage_name] = value
else:
raise ValueError('value must be > 0')
return property(qty_getter, qty_setter)
处理属性删除操作
对象的属性可以使用 del
语句删除:
del my_object.an_attribute
定义特性时,可以使用 @my_propety.deleter
装饰器包装一个方法,负责删除特性管理的属性。
class BlackKnight:
def __init__(self):
self.members = ['an arm', 'another arm',
'a leg', 'another leg']
self.phrases = ['Tis but a scratch.',
"It's just a flesh wound.",
"I'm invincible!",
"All right, we'll call it a draw."]
@property
def member(self):
print('next member is:')
return self.members[0]
@member.deleter
def member(self):
text = 'BLACK KNIGHT (lose {})\n-- {}'
print(text.format(self.members.pop(0), self.phrases.pop(0)))
knight = BlackKnight()
print(knight.member)
del knight.member
del knight.member
del knight.member
del knight.member
"""
output
next member is:
an arm
BLACK KNIGHT (lose an arm)
-- Tis but a scratch.
BLACK KNIGHT (lose another arm)
-- It's just a flesh wound.
BLACK KNIGHT (lose a leg)
-- I'm invincible!
BLACK KNIGHT (lose another leg)
-- All right, we'll call it a draw.
"""
特性是个强大的功能,不过有时更适合使用简单或底层的替代方案。在本章的最后一节,我们将回顾 Python 为动态属性编程提供的部分核心 API。
处理属性的重要属性和函数
影响属性处理方式的特殊属性
-
__class__
: 对象所属类的引用(即obj.__class__
与type(obj)
的作用相同)。Python 的某些特殊方法,例如__getattr__
,只在对象的类中寻找,而不在实例中寻找。 -
__dict__
: 一个映射,存储对象或类的可写属性。有__dict__
属性的对象,任何时候都能随意设置新属性。如果类有__slots__
属性,它的实例可能没有__dict__
属性。 -
__slots__
: 类可以定义这个属性,限制实例能有哪些属性。__slots__
属性的值是一个字符串组成的元组,指明允许有的属性。如果__slots__
中没有__dict__
, 那么该类的实例没有__dict__
属性,实例只允许有指定名称的属性。
处理属性的内置函数
-
dir([object])
: 列出对象的大多数属性。官方文档说,dir
函数的目的是交互式使用,因此没有提供完整的属性列表,只列出一组“重要的”属性名。dir
函数能审查有或没有__dict__
属性的对象。dir
函数不会列出__dict__
属性本身,但会列出其中的键。dir
函数也不会列出类的几个特殊属性。例如__mro__
,__bases__
和__name__
。如果没有指定可选的object
参数,dir
函数会列出当前作用域中的名称。 -
getattr(object, name[, default])
: 从object
对象中获取name
字符串对应的属性。获取的属性可能来自对象所属的类或超类。如果没有指定的属性,getattr
函数抛出AttributeError
异常,或者返回default
参数的值(如果设定了这个参数的话)。 -
hasattr(object, name)
:如果object
对象中存在指定的属性,或者能以某种方式(例如继承)通过object
对象获取指定的属性,返回True
。文档 (https://docs.python.org/3/library/functions.html#hasattr)说道:“这个函数的实现方法是调用getattr(object, name)
函数,看看是否抛出AttributeError
异常。” -
setattr(object, name, value)
: 把object
对象指定属性的值设为value
,前提是object
对象能接受那个值。这个函数可能会创建一个新属性,或者覆盖现有的属性。 -
vars([object])
: 返回object
对象的__dict__
属性;如果实例所属的类定义了__slots__
属性,实例没有__dict__
属性,那么vars
函数不能处理那个实例(相反,dir
函数能处理这样的实例)。如果没有指定参数, 那么vars()
函数的作用与locals()
函数一样:返回表示本地作用域的字典。
处理属性的特殊方法
在用户自己定义的类中,下述特殊方法用于获取、设置、删除和列出属性。
使用点号或内置的 getattr
、hasattr
和 setattr
函数存取属性都会 触发下述列表中相应的特殊方法。但是,直接通过实例的 __dict__
属性读写属性不会触发这些特殊方法——如果需要,通常会使用这种方式 跳过特殊方法。
对用户自己定义的类来说,如果隐式调用特殊方法,仅当特殊方法在对象所属的类型上定义,而不是在对象的实例字典中定义时,才 能确保调用成功。
也就是说,要假定特殊方法从类上获取,即便操作目标是实例也是如此。因此,特殊方法不会被同名实例属性遮盖。
-
__delattr__(self, name)
: 只要使用del
语句删除属性,就会调用这个方法。例如,del obj.attr
语句触发Class.__delattr__(obj, 'attr')
方法。 -
__dir__(self)
: 把对象传给dir
函数时调用,列出属性。例如,dir(obj)
触发Class.__dir__(obj)
方法。 -
__getattr__(self, name)
: 仅当获取指定的属性失败,搜索过 obj、Class 和超类之后调用。 表达式obj.no_such_attr
、getattr(obj, 'no_such_attr')
和hasattr(obj, 'no_such_attr')
可能会触发Class.__getattr__(obj, 'no_such_attr')
方法,但是,仅当在 obj、Class 和超类中找不到指定的属性时才会触发。 -
__getattribute__(self, name)
: 尝试获取指定的属性时总会调用这个方法,不过,寻找的属性是特殊属性或特殊方法时除外。点号与getattr
和hasattr
内置函数会触 发这个方法。调用__getattribute__
方法且抛出AttributeError
异常时,才会调用__getattr__
方法。为了在获取 obj 实例的属性时 不导致无限递归,__getattribute__
方法的实现要使用super().__getattribute__(obj, name)
。 -
__setattr__(self, name, value)
: 尝试设置指定的属性时总会调用这个方法。点号和setattr
内置函数会触发这个方法。例如,obj.attr = 42
和setattr(obj, 'attr', 42)
都会触发Class.__setattr__(obj, ‘attr’, 42)
方法。