web-dev-qa-db-ja.com

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__の再割り当てが正しくないと感じているためです。これがうまくいったとしても、これを行うためのもっとPython的な方法があるべきだと感じずにはいられません。

ある?


編集:あなたの答えをみんなに感謝します。これが私が彼らから得たものです:

  • __class__に割り当てることによってインスタンスを再分類するという考えは、広く使用されているイディオムではありませんが、ほとんどの回答(執筆時点では6つのうち4つ)はそれを有効なアプローチと見なしています。ある回答者(ojracによる)は、それは「一見かなり奇妙だ」と言っていますが、私は同意します(それが質問をする理由でした)。 1つの回答(Jason Bakerによる、2つの肯定的なコメントと投票)だけが積極的にこれを行うことを思いとどまらせましたが、一般的な手法よりもユースケースの例に基づいてそうしました。

  • 肯定的かどうかにかかわらず、この方法で実際の技術的な問題を見つける答えはありません。小さな例外は、古いスタイルのクラスに注意することに言及しているjlsであり、これはおそらく真実であり、C拡張機能です。 new-style-class-aware C拡張機能は、このメソッドではPython自体(後者が真であると想定)と同じくらい問題ないはずですが、同意しない場合は、答えを続けてください。

これがどのようにPythonicであるかという質問に関しては、いくつかの肯定的な答えがありましたが、本当の理由は示されていません。 Zen(import this)を見ると、この場合の最も重要なルールは「暗黙的よりも明示的の方が優れている」だと思います。ただし、そのルールがこの方法での再分類に賛成か反対かはわかりません。

  • {has,get,set}attrを使用すると、魔法を使用する代わりにオブジェクトに明示的に変更を加えるため、より明示的に見えます。

  • __class__ = newclassの使用は、属性を黙って変更するのではなく、「これはクラス 'newclass'のオブジェクトになりました。異なる動作を期待します」と明示的に言うので、より明確に見えますが、オブジェクトのユーザーは、古いクラス。

要約:技術的な観点から、この方法は問題ないようです。 pythonicityの質問は、「はい」へのバイアスで答えられないままです。

Mercurialプラグインの例は非常に強力であるため(また、まだ自分自身に尋ねていない質問にも答えたため)、MartinGeislerの回答を受け入れました。ただし、pythonicityの質問に議論がある場合は、それでも聞きたいと思います。これまでのすべてに感謝します。

P.S.実際のユースケースは、追加機能を拡張する必要があるUIデータ制御オブジェクトです実行時。ただし、質問は非常に一般的なものです。

56
balpha

このようなインスタンスの再分類は、拡張機能(プラグイン)がローカルリポジトリを表すオブジェクトを変更する場合、 Mercurial (分散リビジョン管理システム)で行われます。オブジェクトはrepoと呼ばれ、最初はlocalrepoインスタンスです。これは各拡張機能に順番に渡され、必要に応じて、拡張機能はrepo.__class__changeのサブクラスである新しいクラスを定義します。この新しいサブクラスへの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__localrepoのサブクラスだけでなくfoo_repoのサブクラスに変更する必要があります。最後に、関数は、コードで行ったように、instanceøのクラスを変更します。

このコードがこの言語機能の合法的な使用を示すことができることを願っています。野生で使われているのを見たのはここだけだと思います。

19
Martin Geisler

この場合、継承の使用が最適かどうかはわかりません(少なくとも「再分類」に関して)。あなたは正しい方向に進んでいるように見えますが、これには作曲または集約が最適であるように思われます。これが私が考えていることの例です(テストされていない、疑似風のコードで):

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 および 任意の引数リスト についてさらに読む必要があるかもしれません。

11
Jason Baker

「状態パターンを使用すると、オブジェクトの内部状態が変化したときに、オブジェクトの動作を変更できます。オブジェクトは、クラスを変更しているように見えます。」 -ヘッドファーストデザインパターン。非常によく似たものがGammaet.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の実行時にクラスを変更することを検討し、時々それを使用します。

3
pillmuncher

これで結構です。私はこのイディオムを何度も使用しました。ただし、覚えておくべきことの1つは、このアイデアは古いスタイルのクラスやさまざまなC拡張機能ではうまく機能しないということです。通常、これは問題にはなりませんが、外部ライブラリを使用しているため、古いスタイルのクラスやC拡張機能を扱っていないことを確認する必要があります。

3
jamesls

ふふ、楽しい例。

「再分類」は一見するとかなり奇妙です。 「コピーコンストラクタ」アプローチはどうですか?これは、Reflectionのようなhasattrgetattrsetattrで行うことができます。このコードは、すでに存在しない限り、あるオブジェクトから別のオブジェクトにすべてをコピーします。メソッドをコピーしたくない場合は、それらを除外できます。コメント付きの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

このすべての反射はきれいではありませんが、クールなことを実現するために醜い反射マシンが必要になることがあります。 ;)

1
ojrac

このテクニックは私にはかなりPythonicのようです。構成も良い選択ですが、__class__への割り当ては完全に有効です(少し異なる方法でそれを使用するレシピについては、 ここ を参照してください)。

1
Kiv

Ojracの回答では、breakforループから抜け出し、それ以上の属性をテストしません。 ifステートメントを使用して各属性を一度に1つずつ処理することを決定し、すべての属性に対してforループを続行する方が理にかなっていると思います。それ以外の場合は、__class__への割り当てがおかしいと思うので、ojracの答えが好きです。 (私はPythonの初心者です。覚えている限り、これはStackOverFlowへの最初の投稿です。すばらしい情報をありがとうございます!!)

だから私はそれを実装しようとしました。 dir()がすべての属性をリストしていないことに気づきました。 http://jedidjah.ch/code/2013/9/8/wrong_dir_function/ そこで、「class」を追加しました。 'doc'、 'module'および 'init 'がまだ存在しない場合に追加するもののリストに(おそらくすべてがすでに存在しているとはいえ)、dirが見逃しているものがもっとあるかどうか疑問に思いました。また、奇妙だと言った後、(潜在的に) 'class'に割り当てていることに気づきました。

0
Linguist