浅谈Python中kwargs、动态属性和元类

有些Python的开源库会用到类的动态属性,其中关键字参数是主要实现方式之一。结合使用__init__方法和**kwargs,可以定义一个支持任意属性的类模板。此外,使用元类可以定义一个更加规范的类模版,为类型提供统一管理和规范定义。

这些语言特性在我们自己的业务代码中不一定会用到。但是熟悉了这些特性,会有助于阅读很多三方源码库。

关键字参数

在Python中,**kwargs是一个特殊语法,用于传递关键字参数(keyword arguments)给函数。这里的kwargs是对关键字参数字典的惯用命名,你可以将其理解为"k eyw ord arg uments "的缩写。当你在函数定义中使用**kwargs时,Python会自动将传入的所有关键字参数打包成一个字典。这样,你可以在函数内部通过字典的方式访问这些参数,而无需预先知道参数的具体名称,这为函数提供了很大的灵活性。

python 复制代码
In [1]: def foo(**kwargs):
   ...:     for key, value in kwargs.items():
   ...:         print(f"{key}: {value}")
   ...:

In [2]: foo(name="Ziggy", age=18, addr="Beijing")
name: Ziggy
age: 18
addr: Beijing

In [3]: foo(gender="male", age=18, hobit="Girl")
gender: male
age: 18
hobit: Girl

setattr

在讨论定义类的属性之前,我们看下setattr方法:

setattr 是 Python 中的一个内置函数,用于设置对象的任意属性值。这个函数允许你在运行时动态地给对象添加属性或者修改已有属性的值。setattr 函数接受三个参数:

  1. 对象实例:你想要修改或添加属性的那个对象。
  2. 属性名(字符串形式):你想要设置或修改的属性的名称。
  3. 属性值:你想要赋予该属性的新值。
python 复制代码
In [1]: class MyClass:
   ...:     pass
   ...:

In [2]: obj = MyClass()

In [3]: setattr(obj, 'name', 'ziggy')

In [4]: obj.name
Out[4]: 'ziggy'

类的动态参数

结合setattr和``**kwargs可以在类的初始化方法__init__中,类实例化时,接受任意数量的属性初始化,从而创建出高度可定制的对象:

python 复制代码
In [11]: class MyClass:
    ...:     def __init__(self, **kwargs):
    ...:         for key, value in kwargs.items():
    ...:             setattr(self, key, value)# Python内置函数
    ...:

In [12]: obj1 = MyClass(name="Ziggy", age=18, addr="Beijing")

In [13]: obj2 = MyClass(gender="male", age=18, hobit="Girl")

In [14]: print(obj1.name)
Ziggy

In [15]: print(obj2.gender)
male

In [16]: print(obj2.name)
AttributeError: 'MyClass' object has no attribute 'name'

In [18]: print(obj1.gender)
AttributeError: 'MyClass' object has no attribute 'gender'

上述代码的一个不足之处是这个类可以接受任意的属性,但是很多时候我们更希望可以为具体的类型规范一些允许接收的属性,这个功能可以使用简单的代码实现:

python 复制代码
In [11]: class MyClass:
    ...:     params = ['gender', 'name']
    ...: 
    ...:     def __init__(self, **kwargs):
    ...:         for key, value in kwargs.items():
    ...:             if key not in self.params:
    ...:                raise KeyError()
    ...:             setattr(self, key, value)# Python内置函数
    ...:

上述实现当然也可以做到为每个属性提供默认值,从而类的初始化参数完全可选。此点本文不再赘述。

元类浅述

上面讨论了关于kwargs和动态类属性的内容,虽然与前文并没有密切的联系,不过我们可以额外聊一下Python的元类。因为使用上述功能的部分三方库也很喜欢元类来生成内部类型。

Python中的元类负责生成类对象,有点类似于类的工厂。当你定义一个类时,默认情况下使用的是内置的type元类。通过自定义元类,可以在类创建时插入额外的逻辑,一个简单的示例如下:

python 复制代码
In [28]:class MetaBase(type):
    ...:     def __new__(cls, name, bases, dct):
    ...:         dct['_id'] = uuid.uuid4()
    ...:         dct['created_at'] = dt.datetime.now()
    ...:         return super().__new__(cls,name, bases, dct)
    ...:

In [29]:class MyClass(metaclass=MetaBase):
    ...:     pass
    ...:

In [30]: my_instance = MyClass()

In [31]: my_instance._id
Out[31]: UUID('ee9e88b2-213a-422d-96c6-b67747b6de98')

In [32]: my_instance.created_at
Out[32]: datetime.datetime(2024, 6, 25, 17, 28, 26, 664044)

如上代码为每个通过MetaBase生成的类型都会增加一个id和一个createdAt属性。

为什么要使用元类

在定义上述class MetaBase(type)时,实际上是创建了一个继承自type的新元类MetaBase。这样做有几个关键目的:

  1. 定制类的创建过程 :通过覆盖元类中的方法(如__new____init__),可以在类实例化之前或之后执行特定操作,比如例子中的添加属性、或者检查类的合规性等。

  2. 增强类的功能:元类可以为所有继承自它的类自动添加方法、属性或者实现特定的设计模式,类似单键可以用元类来定义一个模版。

  3. 代码复用与规范:在大型项目或框架中,元类可以作为统一管理和规范类定义的标准手段,确保所有子类遵循相同的规则或拥有共同的行为。这正是一些三方库选择使用元类的主要原因。

相关推荐
hccee16 分钟前
C# IO文件操作
开发语言·c#
hummhumm21 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
杜小满25 分钟前
周志华深度森林deep forest(deep-forest)最新可安装教程,仅需在pycharm中完成,超简单安装教程
python·随机森林·pycharm·集成学习
J老熊31 分钟前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
zmd-zk1 小时前
flink学习(2)——wordcount案例
大数据·开发语言·学习·flink
好奇的菜鸟1 小时前
Go语言中的引用类型:指针与传递机制
开发语言·后端·golang
Alive~o.01 小时前
Go语言进阶&依赖管理
开发语言·后端·golang
花海少爷1 小时前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
手握风云-1 小时前
数据结构(Java版)第二期:包装类和泛型
java·开发语言·数据结构
databook1 小时前
『玩转Streamlit』--布局与容器组件
python·机器学习·数据分析