获取指定成员

Python自身有getattr和setattr方法,作为基本的反射能力,可以用名字访问类成员和实例成员。例如:

class A:
    class_mem_x: int = 8

    def __init__(self, p):
        self.instance_mem_p = p
        # print('self.y in __init__: {}'.format(self.instance_mem_p))


a = A(3)

print('getattr instance_mem_p: {}'.format(getattr(a, 'instance_mem_p')))
print('getattr class_mem_x: {}'.format(getattr(a, 'class_mem_x')))


setattr(a, 'instance_mem_p', 5)
print('a.instance_mem_p after setattr: {}'.format(a.instance_mem_p))

输出:

getattr instance_mem_p: 3
getattr class_mem_x: 8
a.instance_mem_p after setattr: 5

它们既可以访问类成员,也可以访问实例成员,就像直接用'.'访问的效果一样。但这两个方法都必须指定成员名称,如果要写的代码不知道传入的对象有哪些成员,就无法获取了。

遍历实例成员

所以python还为每一个对象提供了__dict__内部变量(类型就是dict),用于获取所有实例成员:

print('a.__dict__:{}'.format(a.__dict__))

输出

a.__dict__:{'instance_mem_p': 5}

可以看出来,其实实例成员就是用一个dict存储的,这一点跟js很像。不过这个方法可取不到类成员,class A的类成员class_mem_x,就没在里面。

遍历类成员

python提供了一个叫dir的方法,可以获取对象的所有属性,返回结果是属性名称的数组:

print('dir(a):{}'.format(dir(a)))

输出

dir(a):['__annotations__', '__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__', 'class_mem_x', 'instance_mem_p']

可以看出类属性和实例属性都在里面了,可讨厌的是, 所有内置属性也都列出来了。我们要想剔除它们,就得用双划线这种字符串前后缀来区分。即使剔除了内置属性,也没法区分类成员和实例成员,当然,结合上面__dict__内置属性的内容,再剔除实例成员,也可以得到类成员。

下面是一个小工具方法:

def get_class_attr_names(obj):
    return [attr_name for attr_name in dir(obj) if (not attr_name.startswith('__') and attr_name not in obj.__dict__)]

print('get_class_attr_names: {}'.format(get_class_attr_names(a)))

输出

get_class_attr_names: ['class_mem_x']

上面的方法有个缺陷:无法排除实例方法,可以下一节获取类型再处理。

获取成员变量类型

貌似没有直接获取类型定义的方法,毕竟python不是强类型,而是所谓duck-typing语言,只能先获取成员属性的值,再用type函数判断类型了。type函数的基本用法如下:

type(a.class_mem_x)

输出

<class 'int'>

返回值是一个Types类型

那么可以用下面的小函数,来获取每个属性的类型:

def get_obj_attr_types(obj):
    return [(attr_name, type(getattr(obj, attr_name))) for attr_name in dir(obj) if not attr_name.startswith('__')]

输出

get_obj_attr_types: [('class_mem_x', <class 'int'>), ('instance_mem_p', <class 'int'>), ('test', <class 'method'>)]

返回值是一个tuple元素组成的数据,tuple由名称,类型对组成。

获取类成员改进

有了type函数判断类型,还可以对之前的get_class_attr_names做一点小改进——因为dir函数其实还会返回类方法,而__dict__内置属性里可没有类方法,要想把方法也剔除掉,就得借助类型判断,于是精确的类成员如下:

import sys
from types import MethodType


class A:
    class_mem_x: int = 8

    def __init__(self, p):
        self.instance_mem_p = p
        # print('self.y in __init__: {}'.format(self.instance_mem_p))

    def test(self):
        pass


a = A(3)

def get_class_attr_names(obj):
    return [
            attr_name
            for attr_name in dir(obj)
            if (
                    not attr_name.startswith('__')
                    and
                    attr_name not in obj.__dict__
                    and
                    type(getattr(obj, attr_name)) != MethodType
            )
    ]


print('get_class_attr_names: {}'.format(get_class_attr_names(a)))

输出

get_class_attr_names: ['class_mem_x']

注意: MethodType不是字符串,而是一个class,需要引入types模块。

get_type_hints获取类信息

从python 3.5以后,typing模块提供了更好的方法get_type_hints,来获取类信息。它的好处在于,可以直接获取类成员定义的类型——如果给定义为str,而赋值为None,也能正确获取类型。下面看一下和自定义实现的对比结果

import sys
from types import MethodType
from typing import get_type_hints


class A:
    class_mem_x: int = 8
    class_mem_y: str = None

    def __init__(self, p):
        self.instance_mem_p = p
        # print('self.y in __init__: {}'.format(self.instance_mem_p))

    def test(self):
        pass


a = A(3)


def get_class_attr_names(obj):
    return [
        attr_name
        for attr_name in dir(obj)
        if (
                not attr_name.startswith('__')
                and
                attr_name not in obj.__dict__
                and
                type(getattr(obj, attr_name)) != MethodType
        )
    ]


class_attr_names = get_class_attr_names(a)


def get_all_types(obj, names):
    return [(name, type(getattr(obj, name))) for name in names]


print('get_all_types: {}'.format(get_all_types(a, class_attr_names)))


print('get_type_hints: {}'.format(get_type_hints(a)))

sys.exit()

输出

get_all_types: [('class_mem_x', <class 'int'>), ('class_mem_y', <class 'NoneType'>)]
get_type_hints: {'class_mem_x': <class 'int'>, 'class_mem_y': <class 'str'>}

从结果可以清晰看出差异:get_all_types 返回的y属性类型是NoneType,而get_type_hints返回的却是str

集合对象的泛型类型

对于集合对象, 比如list和dict,他们类型比较复杂,首先typing模块做了包装,分别为List和Dict,如果有下面的成员定义:

import sys
from typing import get_type_hints, List, Dict


class A:
    class_mem_x: List[int] = [3, 5, 7]
    class_mem_y: Dict[str, int] = {'key1': 9, 'key2': 26}


a = A()
print('get_type_hints: {}'.format(get_type_hints(a)))

sys.exit()    

输出为:

get_type_hints: {'class_mem_x': typing.List[int], 'class_mem_y': typing.Dict[str, int]}

获取集合对象元素的类型

集合对象的type_hint,本身是一个type对象,要想在代码中确切地了解其原始类型,以及其中的元素类型,需要用到两个内部变量:__origin__ 和 __args__。紧接上例的代码:

x_types = get_type_hints(a)['class_mem_x']
print('a.__origin__:{},  a.__args__:{}'.format(x_types.__origin__, x_types.__args__))

输出为:

a.__origin__:<class 'list'>,  a.__args__:(<class 'int'>,)

其中__origin__ 是泛型对应的原始类型,比如list或是dict;__args__ 是泛型参数数组,对于list来说,它只有一个元素,表示了list中保存的是什么类型的数据,如果是dict,它应该有两个参数,分别表示key和value的数据类型


songofhawk
303 声望24 粉丝