Python - 动态属性和特性

动态属性

在Python 中,数据的属性和方法统称为属性(attribute)。其实,方法只是可调用的属性。动态属性(dynamic attribute)的接口与数据属性一样(obj.attr),不过按需计算。

这与 Bertrand Meyer 所说的统一访问原则(Uniform Access Principle)相符:不管服务是由存储还是计算实现的,一个模块提供的所有服务都应该通过统一的方式使用

在Python中,实现动态属性的方式有好几种。本章涵盖最简单的方式:@property装饰器和 __getattr__ 特殊方法。

用户定义的类通过__getattr__ 方法可以实现一种我称之为虚拟属性(virtual attribute)的动态属性。虚拟属性在类的源码中没有显示声明,也不出现在实例属性的__dict__中,但是我们随时都能访问,而且当用户读取不存在的属性(例如obj.no_such_attr) 时,还能即时及计算。

创建动态属性和虚拟属性是一种元编程,框架的作者经常这么做。然后在Python 中,相关的基础技术十分简单,在日常数据装欢任务中也能用到。

使用动态属性访问JSON类数据

示例22-1 osconfeed.json 文件中的记录示例(省略了部分字段内容)

{
  "Schedule": {
    "conferences": [
      {
        "serial": 115
      }
    ],
    "events": [
      {
        "serial": 33451,
        "name": "Migrating to the Web Using Dart and Polymer - A Guide for Legacy OOP Developers",
        "event_type": "40-minute conference session",
        "time_start": "2014-07-23 17:00:00",
        "time_stop": "2014-07-23 17:40:00",
        "venue_serial": 1458,
        "description": "The web development platform is massive. With tons of libraries, frameworks and concepts out there, it might be daunting for the 'legacy' developer to jump into it.\r\n\r\nIn this presentation we will introduce Google Dart & Polymer. Two hot technologies that work in harmony to create powerful web applications using concepts familiar to OOP developers.",
        "website_url": "http://oscon.com/oscon2014/public/schedule/detail/33451",
        "speakers": [
          149868
        ],
        "categories": [
          "Emerging Languages"
        ]
      }
    ],
    "speakers": [
      {
        "serial": 149868,
        "name": "Faisal Abid",
        "photo": "http://cdn.oreillystatic.com/en/assets/1/eventprovider/1/_@user_149868.jpg",
        "url": "http://www.faisalabid.com/",
        "position": "Engineer & Entrepreneur ",
        "affiliation": "Dynamatik, Inc.",
        "twitter": "FaisalAbid",
        "bio": "<p>I am a software engineer, author, teacher and entrepreneur.</p>\n<p>From the hardcore server-side and database challenges to the the front end issues, I love solving problems and strive to create software that can make a difference.</p>\n<p>I&#8217;m an author published by Manning and O&#8217;Reilly and have also appeared in leading publications with my articles on ColdFusion and Flex.</p>\n<p>In my free time I teach Android or Node.js at workshops around the word, speak at conferences such as <span class=\"caps\">OSCON</span>, CodeMotion, <span class=\"caps\">FITC</span> and AndroidTO.</p>\n<p>Currently, I&#8217;m the founder of Dynamatik, a design and development agency in Toronto.</p>\n<p>During the days I am a Software Engineer at Kobo working on OS and app level features for Android tablets.</p>"
      }
    ],
    "venues": [
      {
        "serial": 1448,
        "name": "Portland Ballroom",
        "category": "Conference Venues"
      }
    ]
  }
}

json.load 函数解析JSON文件,返回Python的原生对象,不过feed['Schedule']['events'][40]['name'] 这种句法很冗长。在JavaScript中,可以使用feed.Schedule.events[40].name 获取那个值。在Python中可以实现一个近似字典的类,达到同样的效果。

>>> raw_feed = json.load(open('osconfeed.json'))
>>> from test_13 import FrozenJSON
>>> feed = FrozenJSON(raw_feed)   # 1
>>> len(feed.Schedule.speakers)  # 2
357
>>> feed.keys()
dict_keys(['Schedule'])
>>> sorted(feed.Schedule.keys())  # 3
['conferences', 'events', 'speakers', 'venues']
>>> for key,value in sorted(feed.Schedule.items()): # 4
...     print(f'{len(value):3} {key}')
... 
  1 conferences
484 events
357 speakers
 53 venues
>>> feed.Schedule.speakers[-1].name # 5
'Carina C. Zona'
>>> talk = feed.Schedule.events[40] 
>>> type(talk) # 6
<class 'test_13.FrozenJSON'>
>>> talk.name  
'There *Will* Be Bugs'
>>> talk.speakers # 7
[3471, 5199]
>>> talk.flavor  # 8
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "E:\PyProject\study\test_13.py", line 22, in __getattr__
    return FrozenJSON.build(self.__data[name])
KeyError: 'flavor'
  1. 传入嵌套的字典和列表组成的raw_feed,创建一个FrozenJSON 实例
  2. FrozenJSON 实例能使用属性表示法遍历嵌套的字典,这里我们获取演讲者列表的长度
  3. 也可以使用底层字典的方法获取记录合集的名称
  4. 使用items() 方法获取各个记录集合及其内容,然后显示各个记录合集的长度
  5. 列表仍是列表,但是,如果里面的项是映射,则转换成FrozenJSON 对象
  6. events列表中的第40项是一个JSON对象。现在则变成一个FrozenJSON实例
  7. 事件记录中有一个speakers列表,其中列出了演讲者的编号、
  8. 读取不存在的属性抛出KeyError 异常,而不是常规的AttributeError异常

FrozenJSON 类的关键是__getattr__ 方法。有一点必须记住,仅当无法通过常规方式获取属性(例如,在实例、类或超类中找不到指定的属性)时,解释器才调用特殊方法__getattr__

# 实例22-4 explore0.py :把JSON 数据集转换成嵌套着FrozenJSON 对象、列表和简单类型的FrozenJSON 对象

from collections import abc

class FrozenJSON:
  """一个只读接口,该接口使用属性表示法访问JSON类对象
  """
  def __init__(self, mapping): # 1
      self.__data = dict(mapping)
 
    # def __getattr__(self, name):  # 2
    #     """第一版实现"""
    #     if hasattr(self.__data, name):  # 判断字典是否含有属性:name
    #         return getattr(self.__data, name)  # 返回键的值,如:keys,values      # 3
    #     else:
    #         # 说明是获得的是json中的对象
    #         return FrozenJSON.build(self.__data[name])  # 4

    def __getattr__(self, name):  # 2
        """第二版实现"""
        try:
            return getattr(self.__data, name)  # 3 获取字典的属性值,如 keys,values
        except AttributeError:
            return FrozenJSON.build(self.__data[name])  # 4

  def __dir__(self):   # 5
    return self.__data.keys()

  @classmethod
  def build(cls, obj): # 6
      if isinstance(obj, abc.Mapping):  # 7 如果是映射
          return cls(obj)
      elif isinstance(obj, abc.MutableSequence):  # 8  如果是列表
          return [cls.build(item) for item in obj]
      else: # 9
          return obj

  1. 使用mapping 参数构建一个字典。这么做有两个目的:(1)确保传入的是字典(或者是能转换成字典的对象),(2)安全起见,创建一个副本
  2. 仅当没有指定的名称的属性时才调用__getattr__方法
  3. 如果name 匹配__data字典的某个属性,就返回对应的属性。feed.keys()调用就是这样处理的: keys 方法是__data 字典的一个属性
  4. 否则,从self.__data 中 获取name键对应的值,返回调用FronzeJSON.build 方法得到的结果。
  5. 实现为内置函数dir()提供支持的__dir__ 方法啊。进而支持在Python 标准控制台,以及IPython、Jupyter Notebook 等自动补全。这个方法的代码很简单,将基于self.__data 中的键实现递归自动补全,因为__getattr__ 方法能即时构建FrozenJSON 实列------方便采用交互式方式探索数据。
  6. 这是一个备选构造方法,是@classmethod装饰器的常见用途
  7. 如果obj是一个映射,那么就构建一个FronzenJSON 对象。这里利用了大鹅类型
  8. 如果是一个MutableSequence 对象,则必然是列表,因此,把obj中的每一项递归都传给build()方法,构建一个列表
  9. 如果既不是字典也不是列表,那么原封不动返回项。

处理无效属性名

思考:
1.如果JSON中的键是Python的保留字怎么办? 比如:class

解决:keyword.iskeyword(key):判断是否是Python的关键字,如果是在关键字后面加_

2.如果JSON中的键是Python中的无效标识符怎么办? 比如:数字开头的标识符

解决:
s.isidentifier():判断s是否是Python有效的标识符

  • 抛出异常
  • 把无效的键换成通用成名,例如attr_0,attr_1

使用 new 方式灵活创建对象

我们通常把__init__ 称为构造方法,这是从其他语言借鉴过来的术语。在Python 中,__init__ 的第一个参数是self,可见在解释器调用__init__时,对象已经存在。另外,__init__ 方法什么也不返回。所以,__init__ 其实是初始化方法,不是构造方法。

调用类创建实例时,为了构建实例,Python 调用的特殊方法是__new__。这是一个类方法,以特殊方式对待,因此不必使用@classmethod 装饰器。Python 会把__new__返回的实例传给__init__的第一个参数self。我们几乎不需要编写__new__方法,因为从object 类继承的实现已经可以满足大多数情况。

如果有必要,__new__方法也可以返回其他类的实例。此时,解释器不调用__init__方法。也就是说,Python 构建对象的过程可以使用以下伪代码概括。

# 构建对象的伪代码
def make(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

# 以下两个语句的作用基本等同
x = Foo('bar')
x = make(Foo,'bar')

实例22-6 是FrozenJSON 类的另一个版本,把之前类方法build 中的逻辑移到了__new__ 方法中。

# 实例22-6 explore2.py:使用__new__方法取代 build 方法,构建可能是也可能不是FrozenJSON 实列的新对象

from collections import abc
import keyword

class FrozenJSON:
    """一个只读接口,该接口使用属性表示法访问JSON类对象
    """

    def __new__(cls, args):  # 1
        if isinstance(args, abc.Mapping):
            return super().__new__(cls) # 2
        if isinstance(args, abc.MutableSequence): # 3
            return [cls(arg) for arg in args]
        return args

    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):  
        try:
            return getattr(self.__data, name)  
        except AttributeError:
            return FrozenJSON(self.__data[name]) # 4

    def __dir__(self):
        return self.__data.keys()

OSCON JSON 数据集的结构不太适合交互式探索。例如,索引为40 的事件有两位演讲者,分别是3471和4199,但想找到他们的名字不那么容易,因为提供的是编号,而Schedule.speakers 列表没有使用编号建立索引。因此,如果想找出各个演讲者的名字, 就必须迭代Schedule.spearkers 列表, 直至找到编号对应的记录。我们的下一项任务是调整数据结构,为自动获取所连接的记录做好准备。

[
  {
    "serial": 33950,
    "name": "There *Will* Be Bugs",
    "event_type": "40-minute conference session",
    "time_start": "2014-07-23 14:30:00",
    "time_stop": "2014-07-23 15:10:00",
    "venue_serial": 1449,
    "description": "If you&#x27;re pushing the envelope of programming (or of your own skills)... and even when you’re not... there *will* be bugs in your code.  Don&#x27;t panic!  We cover the attitudes and skills (not taught in most schools) to minimize your bugs, track them, find them, fix them, ensure they never recur, and deploy fixes to your users.\r\n",
    "website_url": "http://oscon.com/oscon2014/public/schedule/detail/33950",
    "speakers": [
      3471,
      5199
    ],
    "categories": [
      "Python"
    ]
  }
]

计算特性

第一步:数据驱动创建属性

指引第一步的doctest,如示例22-8 所示:

>>> records = load()    # 1
>>> records['speaker.3471']  # 2
<Record serial=3471>
>>> sperker = records['speaker.3471'] 
>>> sperker    # 3
<Record serial=3471>
>>> sperker.name,sperker.twitter # 4
('Anna Martelli Ravenscroft', 'annaraven')
>>>

  1. load函数 加载一份字典形式的JSON数据
  2. records 中的键是由记录类型和编号构成的字符串
  3. speaker 是示例22-9 中定义的Record 类的实例
  4. 原JSON数据中的字段可以通过Record 实例属性获取
# 示例22-9 schedule_v1.py:重新组织OSCON 的日程数据

import json

JSON_PATH = '../../osconfeed.json'

class Record:

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs) # 1

    def __repr__(self):
        return f'<{self.__class__.__name__} serial={self.serial!r}>' # 2

    @staticmethod
    def fetch(key):
        if Record.__index is None:
            Record.__index = load()
            return Record.__index[key]


def load(path=JSON_PATH):
    records = {} # 3
    with open(path) as fp:
        raw_data = json.load(fp) # 4
    for collection, raw_records in raw_data['Schedule'].items(): # 5
        record_type = collection.removesuffix('s') # 6
        for raw_record in raw_records:
            key = f'{record_type}.{raw_record["serial"]}'
            records[key] = Record(**raw_record)
        return records
  1. 根据关键字参数构建带属性的实例经常这样简写
  2. 使用serial 字段自定义示例22-8 所示的那种Record 的字符串表示形式
  3. load 函数最终返回一个Record 示例字典
  4. 解析JSON数据,返回Python 原生对象: 列表、字典、字符串、数值等。
  5. 迭代4个顶级列表,即'conferences'、'events'、'speakers' 和 'enevts'
  6. record_type 是列表名称去掉最后一个字符得到的结果,例如speakers 变成speaker 。在pyhton 3.9 及以上版本中,可以使用collection.removesuffix('s'),明确表明意图
  7. '构建speaker.3471 ' 格式的key
  8. 创建一个Record 实例,存在records 中的key名下

Record.__inti__ 方法用到了一个古老的Python 编程技巧。一个对象的__dict__存储着对象的属性,除非类声明了__slots__。因此,使用一个映射更新实例的__dict__可以快速为实例创建一大批属性。

使用特性验证属性

除了计算属性值,特性还可用于实施业务规则,把公开属性变成受读值方法和设值方法保护的属性,而客户端代码不受影响

LineItem 类第一版:表示订单中商品的类

假设有个销售散装有机食物的电商应用程序,客户可以按量订购坚果、干果或杂粮。在这个系统中,每个订单有一系列商品,各个商品可以使用实例22-19中类的实例表示

# 实例22-19 最简单的LineItem类

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

这个类很简单,或许太简单了,实例22-20 揭示了一个问题。

>>> l = LineItem('apple', 1, 2)
>>> l.subtotal()
2
>>> l.price = -20 # 无效输入
>>> l.subtotal() # 无效输出
-20
>>>

LineItem类第二版 :能验证值得特性

如何控制weight,price 不能输入负数?实现特性之后,我们可以使用读值和设值方法,但是LineItem类的接口保持不变。即设置LineItem对象的weight属性依然写成 l.weight = 12


class LineItem:
    def __init__(self,description, weight, price):
        self.description = description,
        self.weight = weight # 1
        self.price = price

    def subtotal(self):
        return self.weight * self.price


    @property # 2
    def weight(self): # 3
        return self.__weight  # 4
 
    @weight.setter # 5
    def weight(self, value):
        if value > 0:
            self.__weight = value # 6
        else:
            raise ValueError('value must be > 0') # 7
  1. 这里已经使用特性得设置方法了,请确保所创建实例得weight 属性不能为负值
  2. @property 装饰读值方法
  3. 实现特性得所有方法,其名称与公开属性得名称一样 -- weight
  4. 真正得值存储在私有属性__weight 中
  5. 被装饰得读值方法有一个.setter 属性,这个属性也是装饰器 -- 把读值方法和设值方法绑定在一起
  6. 如果大于0,就设置私有属性__weight
  7. 否则,抛出 ValueError 异常。

注意,现在不能创建重量为无效值得LineItem 对象了。

>>> l = LineItem('apple', -1 , 20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "E:\PyProject\study\test_17.py", line 5, in __init__
    self.weight = weight
  File "E:\PyProject\study\test_17.py", line 21, in weight
    raise ValueError('value must be > 0')
ValueError: value must be > 0
>>>

如何同时控制多个属性的赋值,而不用手动实现两对几乎一样的读值方法和设置方法?去除重复得良方是抽象。抽象特性得定义有两种方式:一是使用特性工厂函数,二是使用描述符类。后者更灵活。其实,特性本身就是使用描述符类实现得。在实现特性工厂函数之前,需要对特性有深入理解。

特性全解析

虽然内置得property 经常用作装饰器,但它其实是一个类。在Python 中,函数和类通常可以互换,因为二者都是可调用对象,而且Python 没有实例化对象的new 运算符,调用构造函数和调用工厂函数没有区别。此外,只要能返回新的可调用对象,取代被装饰的函数,二者都可以用作装饰器。

property 构造方法的完整签名如下:

property(fget=None, fset=None, fdel=None, doc=None)

property 类型在Python 2.2 中引入, 但是直到Python 2.4 才出现@装饰器语法,因此有那么几年,若想定义特性,只能把存取函数传给前两个参数。

不适用装饰器定义特性的 "经典" 句法如示例所示 22-22所示。

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):  # <1>
        return self.__weight

    def set_weight(self, value):  # <2>
        if value > 0:
            self.__weight = value
        else:
            raise ValueError('value must be > 0')

    weight = property(get_weight, set_weight)  # <3>

在某些情况下,这种经典形式比装饰器句法好,稍后讨论的特性工厂函数就是一例。但是,如果在方法众多的类主体中使用装饰器,则一眼就能看出哪些是读值方法,哪些是设值方法,而不用约定一种惯例,在方法名的前面加上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实例的 pop 特性。

# 示例22-24 实例属性不遮盖类属性
>>> Class.data # 1
'the class data attr'
>>> Class.prop # 2
<property object at 0x00000280461F4B80>
>>> obj.prop
'the prop value'
>>> obj.prop = 'foo' # 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute 'prop'
>>> obj.__dict__['pop'] = 'foo' # 4
>>> vars(obj) # 5
{'data': 'bar', 'prop': 'foo'}
>>> obj.prop # 6
'the prop value'
>>> Class.prop = 'baz' # 7
>>> obj.prop # 8
'foo'
>>>
  1. 直接从Class 中读取 prop特性,获取的是特性对象本身,不运行特性的读值方法
  2. 读取prop.prop 执行特性的读值方法
  3. 尝试设置prop实例属性,结果失败了
  4. 但是可以直接把'prop' 存入obj.dict
  5. 可以看到,obj现在有两个实例属性:data 和 prop
  6. 然而,读取obj.prop 时仍会运行特性的读值方法。特性未被实例属性遮盖
  7. 覆盖Class.prop特性,销毁特性对象
  8. 现在,obj.prop 获取的是实例属性。Class.prop 不是特性了,因此不再遮盖obj.prop

最后在举一个例子,为Class 类增加一个特性,覆盖实例属性

# 示例 22-25 新添的类特性遮盖现有的实例属性 
>>> obj.data
'bar'
>>> Class.data = property(lambda self:'the "data" prop value')
>>> obj.data
'the "data" prop value'
>>> del Class.data
>>> obj.data
'bar'
>>>

本节的主要观点是,像obj.data 这样的表达式不会从obj而是从obj.__class__ 开始寻找data,而且仅类当中没有名为data 的特性时,Python才会在obj实例中寻找

对于 obj.data 的查找顺序:

  1. 类中有没有特性data
  2. 如果类中没有特性,查找实例中有没有data
  3. 实例中没有data,查找类属性data

特性会覆盖实例属性

实例属性会覆盖类的数据属性:

>>> class Clazz:                 
...     data = 'the class data attr'
...     @property  
...     def prop(self):
...             return 'the prop value'  
... 
>>> obj = Clazz()
>>> vars(obj)   # 获得实例的属性信息
{}
>>> obj.data
'the class data attr'
>>> obj.data = 'bar'
>>> vars(obj)         
{'data': 'bar'}
>>> obj.data
'bar'
>>> Clazz.data
'the class data attr'

实例属性不会覆盖类特性:

>>> Clazz.prop
<property object at 0x0000019F2E704680>
>>> obj.prop
'the prop value'
>>> obj.prop = 'foo'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute 'prop'
>>> obj.__dict__['prop'] = 'foo'
>>> vars(obj)
{'data': 'bar', 'prop': 'foo'}
>>> obj.prop
'the prop value'   # 特性没有被实例属性覆盖
>>> Clazz.prop = 'baz'  # 覆盖特性,销毁特性对象
>>> obj.prop 
'foo'  # 没有特性,所以访问到的是实例属性

定义一个特性工厂函数

示例22-27 是LineItem 类的简介版, 用到了quantity 特性的两个实例: 一个用于管理weight 属性,一个用于管理price属性

# 示例 22-27 bulkfood_v2prop.py:使用特性工厂函数 quantity
class LineItem:
    weight = quantity('weight') # 1
    price = quantity('price') # 2

    def __init__(self, descriotion, weight, price):
        self.descriotion = descriotion
        self.weight = weight # 3
        self.price = price 
    def subtotal(self):
        return self.weight * self.price # 4
  1. 使用工厂函数把第一个自定义的特性 weight 定义为类属性
  2. 第二次调用,构建另一个自定义的特性,即price
  3. 这里,特性已经激活,确保不能把weight 设为负数或零
  4. 这里也用到了特性,使用特性获取实例中存储的值

如前说述,特性是类属性。构建各个quantity 特性对象时,要传入LineItem 实例属性的名称,让特性管理。非常不幸,这一行要输入单词quantity 两次。

这里很难避免重复输入,因为特性根本不知道要绑定哪个类属性。记住,赋值语句的右侧先求值,因此调用quantity()时,weight 类属性还不存在。

def quantity(storage_name): #1
    """ 特性工厂函数"""

    def qty_getter(insatance): #2
        return insatance.__dict__[storage_name] # 3

    def qty_setter(instance, value): # 4
        if value > 0:
            instance.__dict__[storage_name] = value  # 5
        else:
            raise ValueError('value must be > 0')
   
    # 不使用装饰器定义特性的"经典" 语法
    return property(qty_getter, qty_setter) # 6
  1. storage_name 参数确定各个特性的数据存储在哪里。对 weight 来说,存储的名称是'weight'
  2. qty_getter 函数的第一个参数可以命名为self,但是这么做很奇怪,因为qty_getter 函数不在类主体中。instance 指代要把属性存储其中的LineItem 实例。
  3. qty_getter 引用了storage_name,把它保存在这个函数的闭包里。值直接从instance.__dict__中获取,以绕过特性,防止无限递归
  4. 定义qty_setter函数,第一个参数也是instance
  5. 将value 直接存入instance.__dict__,这也是为了绕过特性
  6. 构建一个自定义的特性对象,然后将其返

weight 特性覆盖了weight 实例属性,因此对self.weight 及LineItem实例.weight 的每个引用都由特性函数处理,只有直接存取__dict__属性才能绕过特性的处理逻辑

在真实系统中,分散在多个类的多个字段可能要做同样的验证,因此最好把quantity 工厂函数放在实用工具模块中,以便重复实用。

属性描述符参考:

https://www.cnblogs.com/czzz/p/16192660.html

处理属性的重要属性和函数

影响属性处理方式的特殊属性

__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__、__base__和__name__。如果没有指定可选的object参数,dir函数会列出当前作用域中的名称

  • getattr(object, name[, default])

从object对象获取name字符串对应的属性。获取的属性可能来自对象所属的类或超类。如果没有指定的属性 getattr 函数会抛出AttributeError异常,或者返回default参数的值(如果设定了这个参数的话)

  • hasattr(object, name)

如果object对象中存在指定的属性或者能以某种方式(例如继承)通过object对象获取指定的属性,返回Ture。官方文档说到:这个函数的实现方法是调用getattr(object,name)函数,看看是否抛出AttributeError异常

  • setattr(object, name,value)

把object对象指定属性的值设置为value,前提是object对象能接受那个值。这个函数可能会创建一些新属性,或者覆盖现有的属性

  • vars([object])

返回object对象的__dict__属性;如果实例所属的类定义了__slots__属性,实例没有__dict__属性,那么vars函数不能处理那个实例(相反,dir函数能处理这样的实例)如果没有指定参数,那么vars()函数的作用与locals()函数一样,返回表示本地作用域的字典。

处理属性的特殊方法

在用户定义的类中,以下特殊方法用于获取、设置、删除和列出属性。

使用点好表示法或内置的函数getattr、setattr 和 hasattr 存储属性都会触发本节后面列出的相应的特殊方法。但是,直接通过实例的__dict__属性读写属性不会触发这些特殊方法 -- 必要时,这是绕过特殊方法的常用方式。

《Python 语言参考手册》中的3.3.11 节"特殊方法查找" 警告说:

对用于自定义的类,如果隐士调用特殊方法,那么仅当特殊方法在对象所属的类型上而不是在对象的实例字典中定义时,才能确保调用成功。

也就是说,要假定特殊方法从类上获取,即便操作目标是实例也是如此。因此,特殊方法不被同名实例属性覆盖。

在以下示例中,假设有一个名为Class的类,obj是Class类的实例,attr是obj的属性。不管使用点好表示法存储属性,还是使用 getattr、setattr等内置函数,都会触发以下特殊方法中的一个。例如,obj.attr 和
getattr(obj,'attr',42) 都会触发Class._getattr_(obj,'attr') 方法。

__delattr__(self,name)

只要使用del语句删除属性,就会调用这个方法。例如,del obj.attr 语句触发了Class._delattr_(obj,'attr')。如果attr 是一个特性,而且类实现了__delattr__方法。则永不调用特性的删值方法。

__dir__(self)

在对象上调用dir函数时会调用这个方法,以列出属性。例如,dir(obj) 触发Class._dir_(obj)。所有现代的Python控制台在Tab 补全时也使用该方法。

__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__

在Python 代码中尝试直接获取指定名称的属性时始终调用这个方法。点号表示法与内置函数getattr和hasattr会触发这个方法。__getattr__仅在__getattribute__之后,而且仅当__getattribute__抛出AttributeError时调用。为了在获取obj实例的属性时不导致无限递归,__getattribute__方法的实现要使用super()._getattribute_(obj,name)

__setatribute__

尝试设置指定名称的属性时总会调用这个方法。点号表示法和内置函数setattr 会触发这个方法。例如 obj.attr=42 和setattr(obj,'attr',42)都会触发Class._setattr_(obj,'attr',42)

posted @ 2022-05-22 17:22  chuangzhou  阅读(84)  评论(0编辑  收藏  举报