首页 > 编程语言 >c语言模拟Python的命名参数

c语言模拟Python的命名参数

时间:2024-07-28 08:57:18浏览次数:10  
标签:int Python text fa length 参数 arguments func 模拟

最近在书里看到的,让c语言去模拟其他语言里有的命名函数参数。觉得比较有意思所以记录一下。

目标

众所周知c语言里是没有命名函数参数这种东西的,形式参数虽然有自己的名字,但传递的时候并不能通过这个名字来指定参数的值。

而支持命名参数的语言,比如python里,我们能让代码达到这种效果:

def k_func(a, b, c):
    print(f'{a=}, {b=}, {c=}')

k_func(1, 2, 3)        # Output: a=1, b=2, c=3
k_func(c=1, b=3, a=2)  # Output: a=2, b=3, c=1

我们想要的类似于k_func(c=1, b=3, a=2)的效果,至少表现形式上要接近。虽说c的语法并不支持这样的表达式,但我们有办法模拟。

实现

我们假设有这样一个c语言的函数,现在我们想模拟命名参数传递:

int func(const char *text, unsigned int length, int width, int height, double weight)
{
    int printed = 0;
    printed += printf("text: %s\n", text);
    printed += printf("length: %d\n", length);
    printed += printf("width X height: %d X %d\n", width, height);
    printed += printf("weight: %g\n", weight);
    return printed;
}

模拟的关键在于如何完成名字到参数的映射。而且我们函数的五个参数有四种不同的类型,所以这个映射还得是异构的。

在不借助第三方库的情况下,第一个能想到的应该是enum加void*数组。enum可以完成名字到数组索引的映射,void*可以保存异构的数据。

这个方案的缺点很多:如果想要在length和width之间加个参数,我们很可能就需要修改所有的映射代码;比如void*可以接受任何数据类型的指针,所以我们几乎没有办法确保类型安全,想象一下如果有人给text传了个int的指针会发生什么。所以这个方案并不推荐。

能容纳异构的数据,同时还能给这些数据名字的东西,实际上在c里非常常见,那就是结构体。我们要选择的就是基于结构体的方案。

首先我们来定义这个结构体:

typedef struct func_arguments {
    const char *text;
    unsigned int length;
    int width;
    int height;
    double weight;
} func_arguments;

字段的顺序是无所谓的,你可以根据情况来任意调整。现在我们可以根据字段名来设置值了。现在还缺一环,只有结构体是没用的,我们需要把结构体的字段传递给函数才行。所以我们要写一个帮助函数:

int func_wrapper(func_arguments fa)
{
    // 根据需要还可以加入参数校验等逻辑
    return func(fa.text, fa.length, fa.width, fa.height, fa.weight);
}

我们需要的工具基本上都在这了,然而现在和命名参数传递还有不少差距,因为我们需要这样写代码:

func_arguments fa;
fa.text = "text";
fa.length = 4;
fa.width = fa.height = 8;
fa.weight = 10.0;
func_wrapper(fa);

不仅形式上差远了,代码还很繁琐,所以我们还得借助一下c99的复合字面量+指定初始化器的新语法来模拟命名参数传递:

func_wrapper((func_arguments){ .text = "text", .length = 4, .width = 8, .height = 8, .weight = 10.0 });

c99允许在初始化时使用.字段名的形式给字段设置值,没有指定的字段则初始化成零值,c99还允许符合要求的字面量类型转换成数组/结构体。利用新语法我们就能写出上面的代码了。

现在形式上确实很接近了,但还是显得有点啰嗦。这时候就得依赖宏了。c的宏可以实现文本替换和简单的类型分发,所以可以用它来把一些看起来不合法的表达式转换成正常的c语言代码。

首先声明,不要滥用宏,尤其是像下面那样,这里只是充当一下记录而不是教你生产实践。

用宏可以这样写:

#define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })

这里用了另一个c99的新语法,可变长参数宏,三个点意味着宏可以接受用逗号分隔的任意个参数,而__VA_ARGS__会原样替换成这些参数。因此我们只需要让宏的参数里是正确的指定初始化器就行了:

k_func(.text="text", .length=4);
// 宏完成替换后等价于 func_wrapper((func_arguments){ .text = "text", .length = 4 });

是不是很神奇?

有人可能会担心我们参数传递时复制了整个结构体,这会不会带来效率问题?通常这不会带来什么问题,现在编译器一般都能做到省略大部分不必要的复制,另外如果对象比较小的话复制通常也不会带来太大的开销,什么叫小很难定义,以我个人的经验来看尺寸比两个cacheline小的通常都可以算是“小”。

如果还是不放心,也可以简单得把参数类型改成结构体指针:

int func_wrapper(const func_arguments *fa)
{
    return func(fa->text, fa->length, fa->width, fa->height, fa->weight);
}

#define k_func(...) func_wrapper(&(func_arguments){ __VA_ARGS__ })

使用方法是一样的。注意宏里的&,这会分配一个auto生命周期的func_arguments变量(通常在栈上)然后再取它的指针。现在你可以不用担心了。不过我一般不推荐这么写,除非你经过性能测试后发现参数复制真的导致了性能问题。

缺陷

奇迹和魔法都不是免费的,所以上面像变魔术的代码也是有代价的。

第一个缺陷比较小,那就是字段名字前必须加上点。如果这么写:printf("%d\n", k_func(.text="text", length=4)),注意length前我们不小心把点给漏了。编译器会爆出一个不明所以的报错:

test.c: In function ‘main’:
test.c:31:45: error: ‘length’ undeclared (first use in this function)
   31 |         printf("%d\n", k_func(.text="text", length=4));
      |                                             ^~~~~~
test.c:27:52: note: in definition of macro ‘k_func’
   27 | #define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })
      |                                                    ^~~~~~~~~~~
test.c:31:45: note: each undeclared identifier is reported only once for each function it appears in
   31 |         printf("%d\n", k_func(.text="text", length=4));
      |                                             ^~~~~~
test.c:27:52: note: in definition of macro ‘k_func’
   27 | #define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })
      |                                                    ^~~~~~~~~~~

它会告诉你length这个名字从来没被定义过而不是告诉你漏了个.。平时看东西不认真的人就要受罪了,因为要找这个点得花上一番功夫。

第二个缺陷是没有语法提示和自动补全。毕竟宏可以把任何符合规则的文本替换进去,至于替换完怎么样就管不到了,所以想要对k_func的参数进行提示和补全是不太容易的,而且实验下来目前没有ide和编辑器能帮我把字段名自动补全。不过问题倒也不大,因为万一写错了的话编译器会给出准确的报错,只不过开发效率会降低一些。

第三个缺陷在于可以写出这样的东西:printf("%d\n", k_func(.length=10, .text="text", .length=4))。我们指定了两次length字段的值,语法上这是允许的,length的值会被最右边的那个覆盖掉。但这显然不符合我们的要求,而且前面说了因为没有自动补全和语法提示,我们不小心把height写成了weight也很难察觉到。更糟糕的是这种覆盖在gcc下需要指定-Wextra才能看到一个不痛不痒的警告。同样的情况在python下会直接收到一个语法错误的异常keyword argument repeated

前两个缺陷还有办法克服,最后一个是没有任何办法的,只能靠更高级别的警告设置和人力检查了。

总结

正常来说我们做到func_wrapper那步就足够,后面的宏没啥意义纯粹是为了在形式上模拟python的命名参数而做的。

除了用结构体包装,写一个包装函数并调换参数的顺序或者给出默认值也是常见的做法,但这种做法很快会让接口的数量失控,大量相似的接口会让代码变得臭不可闻,所以我更推荐用结构体。

最后学习新语法还是很有用的,因为很多新语法好好利用的话可以有效提升扩展性和你的开发效率。

标签:int,Python,text,fa,length,参数,arguments,func,模拟
From: https://www.cnblogs.com/apocelipes/p/18327870

相关文章

  • 乌尔都语 Tts 可与 python 一起使用
    我想为乌尔都语创建TTS有什么帮助吗?我发现很少有模特拥抱着脸TheUpperCaseGuy/Guy-Urdu-TTSpocketmonkey/speecht5_tts_urduTalha185/speecht5_finetuned_urdu_TTS但我无法从文本创建或生成高质量的语音任何人都可以帮忙吗???importtorchfromtransformersimp......
  • 有没有办法在Python中使用OpenCV在边界框后面画一条线?
    我正在为我的本科论文编写一个应用程序,该应用程序从根本上允许使用OpenCV库跟踪视频源中的多个对象。为了进一步发展这个想法,我希望能够在屏幕上画一条线,显示边界框在我正在跟踪的对象周围的历史记录。我注意到没有排序用于执行此操作的内置函数,因此任何有关制作此类内容的......
  • 如何向 python pandas 数据透视表添加过滤器? (pd.read_excel)
    我正在尝试使用pandas库在python中重新创建我在Excel中制作的数据透视表。我试图用时间段作为过滤器来总结超过500k行的OD总行程数据。在Excel上,我只会执行行(O)、列(D)、值(行程)、过滤器(时间)。到目前为止,在python上我只有索引、列、值、aggfunc,但我不知道如何过滤。有......
  • 标量添加中的 Python 溢出警告
    我想执行一个简单的操作:b+g+rb,g,r变量是整数并且值低于256。我收到溢出警告和错误的结果。问题是操作很简单(结果不高于765),我不应该收到溢出警告这是我的代码:importcv2ascvimportnumpyasnpimgname='r_hh_classic.png'img=cv.imread(imgname)h,w,_=......
  • 在 Python 中读取部分 MP3 文件时处理“对于可用位计数来说太大”错误
    我正在尝试读取MP3文件的特定部分,但遇到错误:[src/libmpg123/layer3.c:INT123_do_layer3():1771]error:part2_3_length(1376)toolargeforavailablebitcount(760)可以访问音频文件此处我的环境是使用此Docker映像设置的:pytorc......
  • dataclass_transform() 得到了意外的关键字参数“field_specifiers”
    我正在尝试使用这个G2P包但我收到dataclass_transform()gotanunexpectedkeywordargument'field_specifiers'这个错误。我正在尝试运行他们在我的jupyter笔记本中运行后提供的示例代码。下面附有我收到错误的示例代码。有人可以帮我解决这个问题吗?......
  • Python SQLAlchemy 2.0 使用 dataclass_transform 非必需字段类型
    我刚刚在一个新项目上安装了SQLAlchemy2.0,我正在尝试使我的模型尽可能类型安全。通过使用@typing_extensions.dataclass_transform,我已经能够实现我想要实现的大部分目标类型检查,但是当前所有字段都被标记为不需要。例如:@typing_extensions.dataclass_tran......
  • 当读取字节时,Python 将 \n 解释为 0xd0
    换行符被解释为Ð(0xd0)而不是(0x0a),我真的不知道如何解决这个问题。我也不知道从哪里开始。当尝试使用此代码时,它将0x0a读为0xd0。withopen(path,"rb")asroot:b_arr=root.read()+b""print("Processing...")buffer=""b_arr......
  • 在Python 3中获取范围的特定形式
    当获取range(1,100)时,我得到:[1,2,3,4,5...99]我需要像这个范围的zip之类的东西:[50,49,51,48,52,47,53...99]如何获取它?背景:这都是关于比特币谜题66的。首先我做了对过去已知的私钥进行线性回归预测,直到第65题为止。我......
  • Python:如何使用pyaudio或sounddevice等库进行自动录音?
    我想做一个项目,需要满足以下录音要求:程序启动后,会在后台不断检测麦克风的声音,当声音分贝大于一定值时打开录音流级别,当分贝低于一定级别时关闭录音流并保存为wav文件。我知道原理,但我无法使用这些库来实现。我想实现以上结果使用Python实现自动录音以下代......