Одноименные функции в одном классе - есть ли элегантный способ определить, какие функции вызывать?

Я пытаюсь управлять версиями продукта в скриптах 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')

Я думал об использовании декоратор для достижения этой цели:

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')

Однако мне не удалось понять, как ... похоже, что декоратор не может выполнять такую ​​операцию, или я просто не понимаю, как это сделать.

Есть ли элегантный способ сделать это?

«Стандартный» способ решить эту проблему - иметь ProductV1 и ProductV2, тогда ваш класс Product просто имеет атрибут _impl, который назначается ProductV<version>, и все методы передаются как def function(self): return self._impl.function(). В python вы даже можете избежать их определения с помощью __getattr__. Также: ProductVX просто определил бы базовые операции, и вы можете поместить в Product фасадные методы, которые вы можете построить поверх базовых методов.

Bakuriu 10.09.2018 20:16

Я забыл сказать: под «стандартным решением» я имею в виду: это то, что вы бы сделали на большинстве языков программирования, где вы не можете использовать такие вещи, как, например, декораторы. Также: если у вас есть большие классы, использование декораторов делает ваш класс довольно большим и трудным для работы. Проще полностью разделить реализации, зависящие от версии.

Bakuriu 10.09.2018 20:17
35
2
4 746
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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), что проще всего, но эти файлы содержат слишком много "дублированного содержимого" ... поэтому я пытаюсь объединить их в один КЛАСС. Спасибо за совет!

Timmy Lin 10.09.2018 08:30

@TimmyLin В этом случае у вас может быть базовый класс, скажем, Product, который хранит весь дублированный контент, а затем есть Productv1 и Productv2, которые наследуют его и просто содержат то, что отличается. Таким образом, не будет дублированного кода.

Loocid 10.09.2018 08:32

И ГЛАВНАЯ причина в том, что ... слишком много версий (но с небольшими изменениями) в этом продукте, и он все еще растет, поэтому, если я сделаю это, структура папок / файлов в конечном итоге станет некрасивой: |

Timmy Lin 10.09.2018 08:35

Ах, в таком случае мое предложение не может идти дальше. Удачи.

Loocid 10.09.2018 08:37

Собственно, план Б - использовать наследование от основного класса. Но я все еще хочу знать, есть ли какой-нибудь крутой и элегантный способ сделать это. Если нет, отмечу лучший ответ! благодарю вас!

Timmy Lin 10.09.2018 08:38

Вы можете использовать для этого декораторы

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 к исходной функции? не понимаю, как совместить это с моими одноименными функциями .. Орз

Timmy Lin 10.09.2018 08:56

Это ничем не отличается от первого примера, который я написал в описании. Передача версии в v_decorate и вставка в вызываемую функцию - это вроде дополнительных шагов. Измените мою функцию на function(self): print(f'for version {self.version}'), можно сделать то же самое без декоратора ..

Timmy Lin 10.09.2018 09:24

Я не понимаю, что на самом деле делает этот декоратор?

Gloweye 10.09.2018 14:00
Ответ принят как подходящий

Наследование, вероятно, лучший способ сделать это, но поскольку вы спросили конкретно о декораторах, я хотел показать, что вы можете сделать это с помощью декораторов.

Вам нужно будет использовать словарь для хранения ваших функций по версиям, а затем искать, какую версию использовать во время выполнения. Вот пример.

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.function и Ambassadorial.function ...

Amadan 10.09.2018 09:34

Python не жалуется, что в классе есть два метода с одинаковым именем? Какой из них он использует? (Да, я понимаю, что декоратор все равно возвращает ту же функцию, но Python не знает)

Bergi 10.09.2018 22:54

@Amadan Можно ли вместо использования полного имени создать отдельный version_store для каждого класса?

Bergi 10.09.2018 22:56

Методы @Bergi - это просто члены класса. Каждое объявление заменяет последнюю версию, но это не имеет значения, потому что, как вы сказали, они все одинаковые :)

Bi Rico 10.09.2018 23:25

Проблема с «одним хранилищем версий на класс» в том, что его некуда поместить. В идеале вы могли бы подумать, что было бы лучше создать секретный атрибут на Products и поместить туда все методы, связанные с версией продукта, но на момент разрешения аннотации Product еще не существует.

Amadan 11.09.2018 01:15

В качестве другого варианта вы можете использовать какую-нибудь фабрику для создания своего класса.

Создайте свои версионные функции (обратите внимание на параметр 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, но это абсолютно хороший ответ. Спасибо!

Timmy Lin 10.09.2018 09:34

@TimmyLin На самом деле функции не разделены на две переменные. p1 и p2 - это отдельные экземпляры подклассов Product, каждый со своей собственной реализацией function на основе version (который должен быть вашим client.version). По сути, в этом примере создается класс с реализацией function, которая требуется для данной версии, вместо того, чтобы иметь один класс, реализующий все возможные «версии» function. Однако я думаю, что вы выбрали лучший ответ для своего требования, это было добавлено для полноты картины :)

shmee 10.09.2018 10:58

Кстати: вы можете заменить ProductFactor классом Product и определить метод __new__, чтобы делать то, что делает get_product_class. Таким образом, код имеет меньше шаблонов.

Bakuriu 10.09.2018 20:18

В общем, не надо.Перегрузка метода не рекомендуется в Python. Если вам нужно различать на уровне класса, прочтите Ответ Loocid. Не буду повторять его отличный пост.

Если вы хотите использовать уровень метода, потому что разница достаточно мала для этого, попробуйте что-то вроде этого:

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

Обратите внимание:

  1. Использование методов, начинающихся с подчеркивания, чтобы указать, что они реализация.
  2. Поддержание аккуратности и аккуратности методов без Загрязнение между версиями
  3. Вызывает ошибку для неожиданных версий (ранний и тяжелый сбой).
  4. По моему не столь скромному мнению, это будет намного понятнее для других людей, читающих ваш пост, чем использование декораторов.

Или:

class Product:

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

    def method(self):
        # implementation
  1. Если вы хотите полностью отказаться от предыдущего использования и отказаться от поддержки версии 1.0 в будущем.
  2. Не забудьте предупредить об устаревании, чтобы не удивить пользователей библиотеки внезапными изменениями.
  3. Возможно, это лучшее решение, если никто другой не использует ваш код.

Я понимаю, что мой первый метод больше подходит для решения вашей проблемы, но я хотел включить второй для потомков. Если вы отредактируете свой код через 10 лет, он сделает вас счастливее. Если завтра вы отредактируете код, используя текущий код, первый способ сделает вас счастливее.

Спасибо за честный совет! Все здесь мне очень помогли, сложно сказать, чье решение лучше. Скажем ... проект, над которым я работаю, ... чрезвычайно огромен, мы хотим, чтобы разница в версиях была указана очень ЧЕТКО и легко поддерживалась. Вот почему я настаиваю на использовании для этого декоратора, потому что он самый ясный и красивый. Честно говоря, если я работаю над другим меньшим проектом, я обязательно выберу ваш второй вариант, а также вариант @ Loocid.

Timmy Lin 10.09.2018 12:43

В то время мы можем смотреть только на небольшую часть - в огромном проекте только вы можете выбрать правильный вариант из-за стиля остальной части проекта. Лучшее, что мы можем сделать, это перечислить возможности. Удачи с этим!

Gloweye 10.09.2018 13:10

Спроектировать новый фреймворк для разработки сложно, мне очень нравится ваше первое предложение, и я обсудил его с командой, может быть, есть шанс, что мы пойдем по тому же пути или по пути @ BartoszKP. Оба отличные.

Timmy Lin 11.09.2018 04:24

Я не разработчик Python, но не могу не задаться вопросом, почему вы просто не делаете что-то вроде этого:

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

Потому что это всего лишь пример, который помогает людям понять, что я пытался сделать. Эта функция не только печатает, но и выполняет множество операций. На самом деле мне даже не нужно, чтобы self.version передавался в метод функции, он мне просто нужен, чтобы определить, какую функцию я хочу загрузить во время работы программы.

Timmy Lin 11.09.2018 04:21

Или вы можете сделать dict.get для вызова функции и выполнить print в lambda, если все в порядке:

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

Другие вопросы по теме