首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >python 动态属性和特性

python 动态属性和特性

作者头像
Michael阿明
发布于 2022-01-07 11:42:52
发布于 2022-01-07 11:42:52
1.1K0
举报

文章目录

learn from 《流畅的python》

1. 使用动态属性转换数据

  • 在 Python 中,数据的属性和处理数据的方法统称属性(attribute)。其实,方法只是可调用的属性
  • 我们还可以创建特性 (property)
代码语言:javascript
复制
from urllib.request import urlopen
import warnings
import os
import json

URL = 'http://www.oreilly.com/pub/sc/osconfeed'
JSON = './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(feed)
print(sorted(feed['Schedule'].keys()))
for key, value in sorted(feed['Schedule'].items()):
    print('{:3} {}'.format(len(value), key))
print(feed['Schedule']['speakers'][-1]['serial'])
# 这种句法太长了。。。如何改进
代码语言:javascript
复制
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)
            # 调用 keys 等方法就是通过这种方式处理的
        else:  # 没有,构建 FrozenJSON
            return FrozenJSON.build(self.__data[name])

    @classmethod  # 备选构造方法,@classmethod 装饰器经常这么用
    def build(cls, obj):
        if isinstance(obj, abc.Mapping):
            return cls(obj)  # 构建 FrozenJSON
        elif isinstance(obj, abc.MutableSequence):
            # 是序列,对每个元素都进行 build
            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()))  
# ['conferences', 'events', 'speakers', 'venues']
print(feed.Schedule.events[-1].name)  
# Why Schools Don't Use Open Source to Teach Programming
p = feed.Schedule.events[-1]
print(type(p))  
# <class '__main__.FrozenJSON'>
print(p.name)  
# Why Schools Don't Use Open Source to Teach Programming
print(p.speakers)  
# [157509]
print(p.age)  
# KeyError: 'age'
  • 处理无效属性名, 例如内置的关键字, keyword.iskeyword
代码语言:javascript
复制
grad = FrozenJSON({'name': 'Jim Bo', 'class': 1982})
# print(grad.class) # invalid syntax
print(getattr(grad, 'class')) # 1982

修改类的构造函数:

代码语言:javascript
复制
def __init__(self, mapping):
    self.__data = {}
    for k,v in mapping.items():
        if keyword.iskeyword(k): # 如果是关键字,则增加下划线后缀
            k += "_"
        self.__data[k] = v
  • 无效的命名,str.isidentifier()
代码语言:javascript
复制
grad = FrozenJSON({'2name': 'Jim Bo', 'class': 1982})
print(grad.2name) # SyntaxError: invalid syntax

改名

代码语言:javascript
复制
def __init__(self, mapping):
    self.__data = {}
    for k,v in mapping.items():
        if keyword.iskeyword(k):
            k += "_"
        if not k.isidentifier(): # 不是合法的命名,改之
            k = "_" + k
        self.__data[k] = v
代码语言:javascript
复制
print(grad._2name) # Jim Bo
  • __init__ 方法其实是 “初始化方法”。真正的构造方法是 __new__(但是几乎不需要自己编写)
代码语言:javascript
复制
# 构建对象的伪代码 
def object_maker(the_class, some_arg): 
	new_object = the_class.__new__(some_arg)  # new 方法也可以返回其它类对象
	if isinstance(new_object, the_class): 
		the_class.__init__(new_object, some_arg) 
	return new_object
代码语言:javascript
复制
class FrozenJSON:
    # 一个只读接口,使用属性表示法访问JSON类对象
    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 arg

    def __init__(self, mapping):
        self.__data = {}
        for k, v in mapping.items():
            if keyword.iskeyword(k):
                k += "_"
            if not k.isidentifier():
                k = "_" + k
            self.__data[k] = v

    def __getattr__(self, name):
        if hasattr(self.__data, name):  # 有属性,获取
            return getattr(self.__data, name)
            # 调用 keys 等方法就是通过这种方式处理的
        else:  # 没有,构建 FrozenJSON
            return FrozenJSON(self.__data[name])

2. @property

https://www.liaoxuefeng.com/wiki/1016959663602400/1017502538658208 请利用 @property 给一个Screen对象加上width和height属性,以及一个只读属性resolution

代码语言:javascript
复制
class Screen(object):
    def __init__(self):
        self._w = 0
        self._h = 0
        self._r = 786432
    @property # 将方法变成属性, 调用不要加()
    def width(self):
        return self._w
    @property
    def height(self):
        return self._h

    @width.setter # 可以设置属性值,没有该方法是只读属性
    def width(self, v):
        self._w = v
    @height.setter
    def height(self, v):
        self._h = v

    @property
    def resolution(self):
        return self._r
代码语言:javascript
复制
s = Screen()
s.width = 1024
s.height = 768
print('resolution =', s.resolution)
if s.resolution == 786432:
    print('测试通过!')
else:
    print('测试失败!')
  • 特性都是类属性,但是特性管理的其实是实例属性的存取
代码语言:javascript
复制
class Class:
    data = "class data attr"
    @property
    def prop(self):
        return "prop value"

obj = Class()
print(vars(obj)) # {}, vars 函数返回 obj 的 __dict__ 属性
print(obj.data) # class data attr
obj.data = "changed"
print(vars(obj)) # {'data': 'changed'}
print(Class.data) # class data attr
# 实例修改了data,但是 类属性没有被修改

print(Class.prop) # <property object at 0x0000021A91E4A680>
print(obj.prop) # prop value
# obj.prop = "changed prop"  # 报错 can't set attribute
obj.__dict__["prop"] = "changed prop1"
print(vars(obj)) # {'data': 'changed', 'prop': 'changed prop1'}
print(obj.prop) # prop value #
# 读取 obj.prop 时仍会运行特性的读值方法。特性没被实例属性遮盖
Class.prop = "haha" # 覆盖 Class.prop 特性,销毁特性对象
print(obj.prop) # changed prop1
# 现在,obj.prop 获取的是实例属性。
# Class.prop 不是特性了,因此不会再覆盖 obj.prop。
代码语言:javascript
复制
print(obj.data) # changed
print(Class.data) # class data attr
Class.data = property(lambda self : "data prop value")
# 使用新特性覆盖 Class.data
print(obj.data) # data prop value
# obj.data 被 Class.data 特性遮盖了
del Class.data # 删除特性
print(obj.data) # changed
# 恢复原样,obj.data 获取的是实例属性 data

obj.attr 这样的表达式不会从 obj 开始寻找 attr,而是从 obj.__class__ 开始,而且,仅当类中没有名为 attr 的特性时,Python 才会在 obj 实例中寻找。

这条规则不仅适用于特性, 还适用于一整类描述符——覆盖型描述符(overriding descriptor)

2.1 help() 文档

使用装饰器创建 property 对象时,读值方法(有 @property 装饰器 的方法)的文档字符串作为一个整体,变成特性的文档

代码语言:javascript
复制
>>> class Foo:
	@property
	def bar(self):
		'''说明文档'''
		return self.__dict__['bar']
	@bar.setter
	def bar(self, val):
		self.__dict__['bar'] = val

		
>>> help(Foo)
Help on class Foo in module __main__:

class Foo(builtins.object)
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  bar
 |      说明文档

>>> help(Foo.bar)
Help on property:

    说明文档
  • 经典写法,传入 doc 参数
代码语言:javascript
复制
weight = property(get_weight, set_weight, doc='weight in kilograms')

3. 特性工厂函数

为了减少编写 getter,setter,可以使用特性工厂函数

代码语言:javascript
复制
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)
代码语言:javascript
复制
class LineItem:
    weight = quantity('weight') # 使用特性工厂函数定义weight类属性
    price = quantity('price') # price 属性

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight # 激活属性,确保不为负数和0
        self.price = price

    def subtotal(self):
        return self.weight * self.price # 使用特性中存储的值

line1 = LineItem("name1", 8, 13.5)
print(line1.weight, line1.price) # 8 13.5
print(sorted(vars(line1).items()))
# [('description', 'name1'), ('price', 13.5), ('weight', 8)]

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

4. 属性删除操作

del 操作,删除属性很少见,但是 python 支持该操作

代码语言:javascript
复制
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 (loses {})\n-- {}'
        print(text.format(self.members.pop(0), self.phrases.pop(0)))

knight = BlackKnight()
print(knight.member)
# next member is:
# an arm
del knight.member
# BLACK KNIGHT (loses an arm)
# -- 'Tis but a scratch.
del knight.member
# BLACK KNIGHT (loses another arm)
# -- It's just a flesh wound.
del knight.member
# BLACK KNIGHT (loses a leg)
# -- I'm invincible!
del knight.member
# BLACK KNIGHT (loses another leg)
# -- All right, we'll call it a draw.
del knight.member
# IndexError: pop from empty list
  • 经典写法,fdel 参数设置删除函数
代码语言:javascript
复制
member = property(member_getter, fdel=member_deleter)
  • 如果不使用特性,还可以实现低层特殊的 __delattr__ 方法处理 删除属性 的操作

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

  • __class__ 对象所属类的引用(即 obj.__class__ 与 type(obj) 的作用相 同) Python 的某些特殊方法,例如 __getattr__,只在对象的类中寻找,而不在实例中寻找
  • __dict__ 一个映射,存储对象或类的可写属性。 有 __dict__ 属性的对象, 任何时候都能随意设置新属性 如果类有 __slots__ 属性,它的实例可能没有 __dict__ 属性
  • __slots__ 类可以定义这个这属性,限制实例能有哪些属性 __slots__ 属性 的值是一个字符串组成的元组,指明允许有的属性 如果 __slots__ 中没有 '__dict__',那么该类的实例没有 __dict__ 属性,实例只允许有指定名称的属性

5.1 处理属性的内置函数

  • dir([object]) 列出对象的大多数属性,dir 函数也不会列出类的几个特殊属性,例如 __mro__、__bases__ 和 __name__
代码语言:javascript
复制
>>> dir(Foo)
['__class__', '__delattr__', '__dict__', '__dir__', 
'__doc__', '__eq__', '__format__', '__ge__', 
'__getattribute__', '__gt__', '__hash__', '__init__',
 '__init_subclass__', '__le__', '__lt__', '__module__',
  '__ne__', '__new__', '__reduce__', '__reduce_ex__', 
  '__repr__', '__setattr__', '__sizeof__', '__str__', 
  '__subclasshook__', '__weakref__', 'bar']
  • getattr(object, name[, default]) 从 object 对象中获取 name 字符串对应的属性 获取的属性可能来自 对象所属的类或超类 如果没有指定的属性,getattr 函数抛出 AttributeError 异常,或者返回 default 参数的值(如果设定了这个参数的话)
  • hasattr(object, name),调用上面的函数,看是否返回异样
  • setattr(object, name, value),可能会创建一个新属性,或者 覆盖现有的属性
  • vars([object]),返回 object 对象的 __dict__ 属性 如果实例所属的类定义了 __slots__ 属性,实例没有 __dict__ 属性,那么 vars 函数不能处理 那个实例

5.2 处理属性的特殊方法

  • 使用点号或内置的 getattr、hasattr 和 setattr 函数存取属性都会 触发下述列表中相应的特殊方法
  • 但是,直接通过实例的 __dict__ 属性读写属性不会触发这些特殊方法,通常会使用这种方式 跳过特殊方法
  • 特殊方法不会被同名实例属性 遮盖
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/09/09 ,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 文章目录
  • 1. 使用动态属性转换数据
  • 2. @property
    • 2.1 help() 文档
  • 3. 特性工厂函数
  • 4. 属性删除操作
  • 5. 处理属性的重要属性和函数
    • 5.1 处理属性的内置函数
    • 5.2 处理属性的特殊方法
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档