同一个类中的同名函数 - 有没有一种优雅的方法来确定调用哪个?

Posted

技术标签:

【中文标题】同一个类中的同名函数 - 有没有一种优雅的方法来确定调用哪个?【英文标题】:Same name functions in same class - is there an elegant way to determine which to call? 【发布时间】:2019-02-14 13:11:23 【问题描述】:

出于特定原因,我尝试在 Python 脚本中进行产品版本控制,但我不知道如何以优雅的方式进行。

目前,我正在做类似下面的事情。但是,当版本内容发生变化时,脚本很难维护。

class Product(object):

    def __init__(client):
        self.version = client.version  # Get client version from another module

    def function():
        if self.version == '1.0':
            print('for version 1.0')
        elif self.version == '2.0':
            print('for version 2.0')
        else:
            print(f'function not support self.version')

因此,我想做类似下面的事情来分隔具有相同名称的函数。

class Product(object):

    def __init__(client):
        self.version = client.version  # Get client version from another module

    def function():
        print('for version 1.0')

    def function():
        print('for version 2.0')

我正在考虑使用 decorator 来实现:

class Product(object):

    def __init__(client):
        self.version = client.version  # Get client version from another module

    @version(1.0)
    def function():
        print('for version 1.0')

    @version(2.0)
    def function():
        print('for version 2.0')

但是,我无法弄清楚如何......装饰器似乎无法进行这种操作,或者我只是不明白如何。

有没有优雅的方法来做到这一点?

【问题讨论】:

解决这个问题的“标准”方法是拥有ProductV1ProductV2 然后你的Product 类只是有一个_impl 属性分配给ProductV<version> 和所有方法像def function(self): return self._impl.function() 一样转发。在 python 中,你甚至可以避免使用__getattr__ 来定义它们。另外:ProductVX 将简单地定义基本操作,您可以在Product 中放入您可以在基本方法之上构建的外观方法。 我忘了说:“标准解决方案”我的意思是:这是你在大多数编程语言中所做的,你不能使用装饰器之类的东西。另外:如果你有大类,使用装饰器会使你的类变得非常大并且难以使用。完全分离特定于版本的实现更容易。 【参考方案1】:

我不是 Python 开发人员,但我不禁想知道为什么你不只是做这样的事情:

class Product(object):
    def __init__(self, version):
        self.version = version
    def function(self):
        print('for version ' + self.version)

【讨论】:

因为这只是一个让人们更容易理解我想要做什么的例子。该功能不仅可以打印东西,还可以进行很多操作。其实我什至不需要将 self.version 传递给函数方法,我只需要它在程序运行时定位我要加载的函数。【参考方案2】:

一般情况下,不要。Method overloading 在 Python 中不鼓励使用。如果您必须区分班级级别,请阅读Loocid's answer。我不会重复他的出色帖子。

如果您想进入方法级别,因为差异足够小,请尝试以下操作:

class Product:

    def method(self):
        if self.version == "1.0":
            return self._methodv1()
        elif self.version == "2.0":
            return self._methodv2()
        else:
            raise ValueError("No appropriate method for version ".format(self.version))

    def _methodv1(self):
        # implementation

    def _methodv2(self):
        # implementation

请注意:

    使用以下划线开头的方法,表示这些方法是 实施。 保持方法整洁 版本之间的污染 为意外版本引发错误(早期和严重崩溃)。 在我看来,这对于阅读您的帖子的其他人来说比使用装饰器要清楚得多。

或者:

class Product:

    def method_old(self):
        # transform arguments to v2 method:
        return self.method()

    def method(self):
        # implementation
    如果您想完全弃用以前的用法,并想在将来放弃对 1.0 版的支持。 请务必发出弃用警告,以免库的突然变化让用户感到惊讶。 如果没有其他人使用您的代码,这无疑是更好的解决方案。

我觉得我的第一种方法更适合您手头的问题,但我想包括第二种方法以供后代使用。如果你在 10 年后编辑你的代码,那会让你更快乐。如果你明天使用当前代码编辑代码,第一种方法会让你更快乐。

【讨论】:

感谢您的诚实建议!这里的每个人都帮了我很多,很难说谁的解决方案是最好的。比方说..我正在做的项目..非常庞大,我们希望可以非常清楚地列出版本差异并且可以轻松维护。这就是为什么我坚持使用装饰器来做这件事,因为它是最清晰和美丽的。老实说,如果我正在从事另一个较小的项目,我肯定会选择您的第二个选项以及@Loocid 的选项。 当时我们只能查看一小部分 - 在一个巨大的项目中,由于项目其余部分的风格,只有您可以选择正确的选项。我们能做的最好的就是列出可能性。祝你好运! 设计一个新的开发框架很难,我真的很喜欢你的第一个建议,我已经和团队讨论过,也许我们有机会走那条路或@BartoszKP 的路。两者都很棒。【参考方案3】:

继承可能是做到这一点的最佳方式,但由于您专门询问了装饰器,我想说明您可以使用装饰器来做到这一点。

您需要使用字典按版本存储函数,然后在运行时查找要使用的版本。这是一个例子。

version_store = 

def version(v):
    def dec(f):
        name = f.__qualname__
        version_store[(name, v)] = f
        def method(self, *args, **kwargs):
            f = version_store[(name, self.version)]
            return f(self, *args, **kwargs)
        return method
    return dec

class Product(object):
    def __init__(self, version):
        self.version = version

    @version("1.0")
    def function(self):
        print("1.0")

    @version("2.0")
    def function(self):
        print("2.0")

Product("1.0").function()
Product("2.0").function()

【讨论】:

您可能希望f.__qualname__ 避免Product.functionAmbassadorial.function 之间的冲突... Python 不抱怨一个类中有两个同名方法?它使用哪一个? (是的,我意识到装饰器无论如何都会返回相同的函数,但 Python 不知道) @Amadan 不使用限定名称,是否可以为每个班级创建一个单独的version_store @Bergi 方法只是类的成员。每个声明都替换了最后一个版本,但这并不重要,因为就像你说的,它们都是一样的:) “每个班级一个版本存储”的问题是 - 无处可放。理想情况下,您认为最好在 Products 上创建一个秘密属性并将所有与产品相关的版本化方法放在那里 - 但在解析注释时,Product 尚不存在。 【参考方案4】:

或者你可以这样做,dict.get 调用一个函数并在 lambda 中执行 print,如果没有任何问题:

def func_1(self):
        print('for version 1.0')

    def func_2(self):
        print('for version 2.0')
    def function(self):
       funcs = "1.0": self.func_1,
                "2.0": self.func_2
       funcs.get(self.version,lambda: print(f'function not support self.version'))()

例子:

class Product(object):

    def __init__(self,version):
        self.version = version

    def func_1(self):
        print('for version 1.0')

    def func_2(self):
        print('for version 2.0')
    def function(self):
       funcs = "1.0": self.func_1,
                "2.0": self.func_2
       funcs.get(self.version,lambda: print(f'function not support self.version'))()
Product('1.0').function()
Product('2.0').function()
Product('3.0').function()

输出:

for version 1.0
for version 2.0
function not support 3.0

【讨论】:

【参考方案5】:

作为另一种选择,你可以去一些工厂来创建你的类。

创建您的版本化函数(注意self 参数)。这可以在不同的模块中完成。另外,添加一些集合来根据版本号获取函数。

def func_10(self):
    print('for version 1.0')

def func_20(self):
    print('for version 2.0')

funcs = "1.0": func_10,
         "2.0": func_20

添加一个包含实现的静态部分的基类和一个用于在其中创建实例的实用程序类:

class Product:
    def __init__(self, version):
        self.version = version

class ProductFactory(type):
    @classmethod
    def get_product_class(mcs, version):
        # this will return an instance right away, due to the (version) in the end
        return type.__new__(mcs, "Product_".format(version.replace(".","")), (Product,), "function": funcs.get(version))(version)
        # if you want to return a class object to instantiate in your code omit the (version) in the end

使用这个:

p1 = ProductFactory.get_product_class("1.0")
p2 = ProductFactory.get_product_class("2.0")
print(p1.__class__.__name__) # Product_10
p1.function() # for version 1.0
print(p1.function) # <bound method func_10 of <__main__.Product_10 object at 0x0000000002A157F0>>
print(p2.__class__.__name__) # Product_20
p2.function() # for version 2.0 
print(p2.function) # <bound method func_20 of <__main__.Product_20 object at 0x0000000002A15860>>

【讨论】:

酷!虽然它有点复杂,但它可以做到这一点。但是我正在寻找一种不需要将函数分成 2 个变量 p1, p2 的方法,但这绝对是一个很好的答案。谢谢! @TimmyLin 实际上,函数并没有分成两个变量。 p1p2Product 的子类的独立实例,每个都有自己的 function 实现,基于 version(应该是你的 client.version)。基本上,此示例使用给定版本所需的function 实现构建类,而不是让一个类实现function 的所有可能“版本”。但是,我认为您选择了满足您要求的最佳答案,为了完整起见,这只是添加了 :) 顺便说一句:您可以用 Product 类替换 ProductFactor 并定义 __new__ 方法来执行 get_product_class 所做的事情。通过这种方式,代码的样板文件更少。【参考方案6】:

你可以为此使用装饰器

def v_decorate(func):
   def func_wrapper(name):
       return func(name)
   return func_wrapper

@v_decorate
def get_version(name):
   return "for version 0 ".format(name)

你可以这样称呼它

get_version(1.0)

   'for version 1.0 '

get_version(2.0)
'for version 2.0 '

【讨论】:

对不起,我不确定我是否理解正确。 v_decorate 是在原始函数中添加一个额外的参数name 吗?不明白如何将它与我的同名函数结合起来.. Orz 这样做与我在描述中写的第一个示例没有什么不同。将版本传递给v_decorate 并放入被调用函数有点额外的步骤。将我的函数修改为function(self): print(f'for version self.version') 可以在没有装饰器的情况下做同样的事情.. 我没有看到这个装饰器实际上完成了什么?【参考方案7】:

你能不能把你的Product 类放到两个模块中,v1 和 v2,然后有条件地导入它们?

例如:

Productv1.py

class Product(object):
    def function():
        print('for version 1.0')

Productv2.py

class Product(object):
    def function():
        print('for version 2.0')

然后在你的主文件中:

main.py

if client.version == '1.0':
    from Productv1 import Product
elif client.version == '2.0':
    from Productv2 import Product
else:
    print(f'function not support self.version')

p = Product
p.function()

【讨论】:

其实,我之前用类似的方法来做版本控制,创建多个py文件(例如v1、v2、v3),这是最简单的,但这些文件包含太多“重复内容”..这就是为什么我正在尝试将它们组合成一个类。谢谢你的建议! @TimmyLin 在这种情况下,你可以有一个基类,比如 Product 保存所有重复的内容,然后有一个 Productv1Productv2 继承它并只保存什么不同的。这样就没有重复的代码。 主要的原因是..这个产品的版本太多(但变化很小)并且它还在增长,所以如果我这样做,文件夹/文件结构最终会变得丑陋:| 啊,在那种情况下,我的建议不能再进一步了。祝你好运。 没错,B方案是使用从主类继承。但我仍然想知道是否有任何酷而优雅的方式来做到这一点。如果没有,我会标记你的最佳答案!谢谢!

以上是关于同一个类中的同名函数 - 有没有一种优雅的方法来确定调用哪个?的主要内容,如果未能解决你的问题,请参考以下文章

同名的静态和实例方法?

c++解决二义性的方法

类中的同名函数关系,重载,覆盖/重写,隐藏

具有相同名称的函数的类中的多重继承[重复]

是否可以强制超类中的非常量虚拟方法优先于子类中同名的 const 方法?

有没有一种优雅的方法来告诉 eslint 以确保我们没有使用任何 ES6 语法/函数?