Подтвердить что ты не робот

Переклассификация экземпляра в Python

У меня есть класс, который предоставляется мне внешней библиотекой. Я создал подкласс этого класса. У меня также есть экземпляр исходного класса.

Теперь я хочу превратить этот экземпляр в экземпляр моего подкласса без изменения каких-либо свойств, которые уже имеет экземпляр (кроме тех, которые мой подкласс все равно переопределяет).

Кажется, что работает следующее решение.

# This class comes from an external library. I don't (want) to control
# it, and I want to be open to changes that get made to the class
# by the library provider.
class Programmer(object):
    def __init__(self,name):
        self._name = name

    def greet(self):
        print "Hi, my name is %s." % self._name

    def hard_work(self):
        print "The garbage collector will take care of everything."

# This is my subclass.
class C_Programmer(Programmer):
    def __init__(self, *args, **kwargs):
        super(C_Programmer,self).__init__(*args, **kwargs)
        self.learn_C()

    def learn_C(self):
        self._knowledge = ["malloc","free","pointer arithmetic","curly braces"]

    def hard_work(self):
        print "I'll have to remember " + " and ".join(self._knowledge) + "."

    # The questionable thing: Reclassing a programmer.
    @classmethod
    def teach_C(cls, programmer):
        programmer.__class__ = cls # <-- do I really want to do this?
        programmer.learn_C()


joel = C_Programmer("Joel")
joel.greet()
joel.hard_work()
#>Hi, my name is Joel.
#>I'll have to remember malloc and free and pointer arithmetic and curly braces.

jeff = Programmer("Jeff")

# We (or someone else) makes changes to the instance. The reclassing shouldn't
# overwrite these.
jeff._name = "Jeff A" 

jeff.greet()
jeff.hard_work()
#>Hi, my name is Jeff A.
#>The garbage collector will take care of everything.

# Let magic happen.
C_Programmer.teach_C(jeff)

jeff.greet()
jeff.hard_work()
#>Hi, my name is Jeff A.
#>I'll have to remember malloc and free and pointer arithmetic and curly braces.

Однако я не уверен, что это решение не содержит никаких оговорок, о которых я не думал (извините за тройственное отрицание), тем более, что переназначение магического __class__ просто не кажется правильным. Даже если это сработает, я не могу не почувствовать, что должен быть более питонический способ сделать это.

Есть ли?


Изменить: Спасибо всем за ваши ответы. Вот что я получаю от них:

  • Хотя идея переклассификации экземпляра путем присвоения __class__ не является широко используемой идиомой, большинство ответов (4 из 6 на момент написания) считают ее действительным подходом. Один anwswer (by ojrac) говорит, что это "довольно странно на первый взгляд", с которым я согласен (это было причиной для того, чтобы задать вопрос). Только один ответ (Джейсон Бейкер, с двумя положительными комментариями и голосами) активно отговаривал меня от этого, однако делал это на примере примера использования примера moreso, чем в технике вообще.

  • Ни один из ответов, будь то положительный или нет, находит фактическую техническую проблему в этом методе. Небольшим исключением является jls, который упоминает о том, чтобы остерегаться классов старого стиля, что, скорее всего, верно, и расширений C. Я полагаю, что C-расширения, поддерживающие новый стиль, должны быть такими же прекрасными, как и сам Python (предполагая, что последнее верно), хотя, если вы не согласны, продолжайте отвечать.

Что касается вопроса о том, как это pythonic это, было несколько положительных ответов, но реальных причин не было. Глядя на Zen (import this), я думаю, что наиболее важным правилом в этом случае является "Явное лучше, чем неявное". Я не уверен, однако, говорит ли это правило за или против переклассификации таким образом.

  • Использование {has,get,set}attr кажется более явным, поскольку мы явно вносим изменения в объект вместо использования магии.

  • Использование __class__ = newclass кажется более явным, потому что мы явно говорим: "Это теперь объект класса newclass," ожидают другого поведения "вместо молчащих изменений атрибутов, но оставляя пользователей объекта, полагая, что они имеют дело с обычный объект старого класса.

Подводя итог: с технической точки зрения, метод выглядит нормально; вопрос о питоничности остается без ответа с предубеждением к "да".

Я принял ответ Мартина Гейслера, потому что пример плагина Mercurial является довольно сильным (а также потому, что он ответил на вопрос, который я даже еще не задал себе). Однако, если есть какие-либо аргументы по вопросу о pythonicity, я все равно хотел бы их услышать. Спасибо всем до сих пор.

P.S. Фактический прецедент - это объект управления данными UI, который требует увеличения функциональности во время выполнения. Однако вопрос должен быть очень общим.

4b9b3361

Ответ 1

Повторяющиеся экземпляры, подобные этому, выполняются в Mercurial (распределенная система контроля версий), когда расширения (плагины) хотят изменить объект, который представляют собой локальный репозиторий. Объект называется repo и изначально является экземпляром localrepo. Он передается каждому расширению по очереди и, при необходимости, расширения будут определять новый класс, который является подклассом repo.__class__ и изменить класс repo на этот новый подкласс!

Он выглядит как этот в коде:

def reposetup(ui, repo):
    # ...

    class bookmark_repo(repo.__class__): 
        def rollback(self):
            if os.path.exists(self.join('undo.bookmarks')):
                util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
            return super(bookmark_repo, self).rollback() 

        # ...

    repo.__class__ = bookmark_repo 

Расширение (я взял код из расширения закладок) определяет функцию уровня модуля под названием reposetup. Mercurial вызовет это при инициализации расширения и передаст аргумент ui (пользовательский интерфейс) и repo (репозиторий).

Затем функция определяет подкласс любого класса repo. Недостаточно просто подкласса localrepo, поскольку расширения должны иметь возможность расширять друг друга. Поэтому, если первое расширение изменяет repo.__class__ на foo_repo, следующее расширение должно изменить repo.__class__ на подкласс foo_repo, а не просто подкласс localrepo. Наконец, функция меняет класс instanceø, как и в вашем коде.

Я надеюсь, что этот код может показать законное использование этой языковой функции. Я думаю, что это единственное место, где я видел его в дикой природе.

Ответ 2

Я не уверен, что использование наследования лучше в этом случае (по крайней мере, в отношении "переклассификации" ). Похоже, вы на правильном пути, но похоже, что композиция или агрегация были бы лучше для этого. Вот пример того, о чем я думаю (в непроверенном, псевдоэкспективном коде):

from copy import copy

# As long as none of these attributes are defined in the base class,
# this should be safe
class SkilledProgrammer(Programmer):
    def __init__(self, *skillsets):
        super(SkilledProgrammer, self).__init__()
        self.skillsets = set(skillsets)

def teach(programmer, other_programmer):
    """If other_programmer has skillsets, append this programmer's
       skillsets.  Otherwise, create a new skillset that is a copy
       of this programmer's"""
    if hasattr(other_programmer, skillsets) and other_programmer.skillsets:
        other_programmer.skillsets.union(programmer.skillsets)
    else:
        other_programmer.skillsets = copy(programmer.skillsets)
def has_skill(programmer, skill):
    for skillset in programmer.skillsets:
        if skill in skillset.skills
            return True
    return False
def has_skillset(programmer, skillset):
    return skillset in programmer.skillsets


class SkillSet(object):
    def __init__(self, *skills):
        self.skills = set(skills)

C = SkillSet("malloc","free","pointer arithmetic","curly braces")
SQL = SkillSet("SELECT", "INSERT", "DELETE", "UPDATE")

Bob = SkilledProgrammer(C)
Jill = Programmer()

teach(Bob, Jill)          #teaches Jill C
has_skill(Jill, "malloc") #should return True
has_skillset(Jill, SQL)   #should return False

Возможно, вам придется больше узнать о sets и списки произвольных аргументов, если вы не знакомы с ними, чтобы получить этот пример.

Ответ 3

Это нормально. Я много раз использовал эту идиому. Одно дело иметь в виду, однако, состоит в том, что эта идея не очень хорошо работает со старыми классами и различными расширениями C. Обычно это не проблема, но поскольку вы используете внешнюю библиотеку, вам просто нужно убедиться, что вы не имеете дело со старыми классами или расширениями C.

Ответ 4

"Шаблон состояния позволяет объекту изменять его поведение, когда изменяется его внутреннее состояние. Объект, как представляется, изменит его класс." - Нарисуйте первый шаблон дизайна. Что-то очень похожее пишет Gamma et.al. в их книге "Шаблоны дизайна". (У меня это у меня на другом месте, поэтому никаких цитат). Я думаю, что весь смысл этой схемы дизайна. Но если я могу изменить класс объекта во время выполнения, большую часть времени мне не нужен шаблон (бывают случаи, когда State Pattern делает больше, чем имитирует изменение класса).

Кроме того, изменение класса во время выполнения не всегда работает:

class A(object):
    def __init__(self, val):
        self.val = val
    def get_val(self):
        return self.val

class B(A):
    def __init__(self, val1, val2):
        A.__init__(self, val1)
        self.val2 = val2
    def get_val(self):
        return self.val + self.val2


a = A(3)
b = B(4, 6)

print a.get_val()
print b.get_val()

a.__class__ = B

print a.get_val() # oops!

Кроме того, я рассматриваю изменение класса во время исполнения Pythonic и время от времени использую его.

Ответ 5

Хехех, забавный пример.

"Переклассификация" довольно странно, на первый взгляд. Как насчет подхода "конструктор копий"? Вы можете сделать это с помощью Reflection-like hasattr, getattr и setattr. Этот код будет копировать все из одного объекта в другой, если он уже не существует. Если вы не хотите копировать методы, вы можете их исключить; см. комментарий if.

class Foo(object):
    def __init__(self):
        self.cow = 2
        self.moose = 6

class Bar(object):
    def __init__(self):
        self.cat = 2
        self.cow = 11

    def from_foo(foo):
        bar = Bar()
        attributes = dir(foo)
        for attr in attributes:
            if (hasattr(bar, attr)):
                break
            value = getattr(foo, attr)
            # if hasattr(value, '__call__'):
            #     break # skip callables (i.e. functions)
            setattr(bar, attr, value)

        return bar

Все это отражение не очень красивое, но иногда вам нужна уродливая машина для отражения, чтобы сделать классные вещи.;)

Ответ 6

Этот метод кажется мне по-видимому Pythonic. Композиция также была бы хорошим выбором, но присвоение __class__ совершенно справедливо (см. здесь для рецепта, который использует его в несколько иной путь).

Ответ 7

В ответе ojrac break выходит из for -loop и не тестирует никаких дополнительных атрибутов. Я думаю, что имеет смысл просто использовать if -statement, чтобы решить, что делать с каждым атрибутом по одному, и продолжить через for -loop по всем атрибутам. В противном случае мне нравится ответ ojrac, так как я тоже вижу, что присваиваю __class__ как-то странно. (Я начинаю с Python, и насколько я помню, это мое первое сообщение в StackOverFlow. Спасибо за всю отличную информацию!)

Поэтому я попытался реализовать это. Я заметил, что dir() не перечисляет все атрибуты. http://jedidjah.ch/code/2013/9/8/wrong_dir_function/ Поэтому я добавил ' класс', ' doc', ' module и init "в список добавляемых вещей, если их там уже нет (хотя они, вероятно, все уже есть), и задались вопросом, было ли еще вещи dir промахи. Я также заметил, что я (потенциально) назначил" класс" после того, как сказал, что это было странно.

Ответ 8

Я скажу, что это прекрасно, если оно работает для вас.