web-dev-qa-db-ja.com

python循環インポートをもう一度(別名、この設計の問題)

python(3.x)スクリプトを考えてみましょう:

main.py:

from test.team import team
from test.user import user

if __name__ == '__main__':
    u = user()
    t = team()
    u.setTeam(t)
    t.setLeader(u)

test/user.py:

from test.team import team

class user:
    def setTeam(self, t):
        if issubclass(t, team.__class__):
            self.team = t

test/team.py:

from test.user import user

class team:
    def setLeader(self, u):
        if issubclass(u, user.__class__):
            self.leader = u

もちろん、もちろん、循環インポートと素晴らしいImportErrorがあります。

ですから、pythonistaではなく、3つの質問があります。まず第一に:

私。これを機能させるにはどうすればよいですか?

そして、誰かが「循環インポートは常に設計上の問題を示している」と必然的に言うことを知って、2番目の質問が来ます:

ii。なぜこのデザインは悪いのですか?

そして最後に、3番目のもの:

iii。より良い代替手段は何でしょうか?

正確には、上記のタイプチェックは単なる例であり、クラスに基づくインデックスレイヤーも存在します。 1つのチームのメンバーであるすべてのユーザーを検索する(ユーザークラスには多くのサブクラスがあるため、ユーザー全体および特定のサブクラスごとにインデックスが2倍になります)またはユーザーをメンバーとして指定したすべてのチーム

編集:

より詳細な例が私が達成しようとしていることを明確にすることを願っています。読みやすくするために省略されたファイル(ただし、300kbのソースファイルが1つあるため、どういうわけか怖いので、すべてのクラスが異なるファイルにあると想定してください)

# ENTITY

class Entity:
    _id    = None
    _defs  = {}
    _data  = None

    def __init__(self, **kwargs):
        self._id   = uuid.uuid4() # for example. or randint(). or x+1.
        self._data = {}.update(kwargs)

    def __settattr__(self, name, value):
        if name in self._defs:
            if issubclass(value.__class__, self._defs[name]):
                self._data[name] = value

                # more stuff goes here, specially indexing dependencies, so we can 
                # do Index(some_class, name_of_property, some.object) to find all   
                # objects of some_class or its children where
                # given property == some.object

            else:
                raise Exception('Some misleading message')
        else:
            self.__dict__[name] = value    

    def __gettattr__(self, name):
        return self._data[name]

# USERS 

class User(Entity):
    _defs  = {'team':Team}

class DPLUser(User):
    _defs  = {'team':DPLTeam}

class PythonUser(DPLUser)
    pass

class PerlUser(DPLUser)
    pass

class FunctionalUser(User):
    _defs  = {'team':FunctionalTeam}

class HaskellUser(FunctionalUser)
    pass

class ErlangUser(FunctionalUser)
    pass

# TEAMS

class Team(Entity):
    _defs  = {'leader':User}

class DPLTeam(Team):
    _defs  = {'leader':DPLUser}

class FunctionalTeam(Team):
    _defs  = {'leader':FunctionalUser}

そして今いくつかの使用法:

t1 = FunctionalTeam()
t2 = DLPTeam()
t3 = Team()

u1 = HaskellUser()
u2 = PythonUser()

t1.leader = u1 # ok
t2.leader = u2 # ok
t1.leader = u2 # not ok, exception
t3.leader = u2 # ok

# now , index

print(Index(FunctionalTeam, 'leader', u2)) # -> [t2]
print(Index(Team, 'leader', u2)) # -> [t2,t3]

したがって、この不気味な循環インポートのほかに、それはうまく機能します(実装の詳細は省略されていますが、複雑ではありません)。

46
ts.

循環インポートは本質的に悪いことではありません。 teamuserで何かを実行している間、userコードがteamに依存するのは自然なことです。

ここでの悪い習慣はfrom module import memberです。 teamモジュールはインポート時にuserクラスを取得しようとしています。userモジュールはteamクラスを取得しようとしています。ただし、teamクラスはまだ存在しません。これは、team.pyの実行時にuser.pyの最初の行にいるためです。

代わりに、モジュールのみをインポートしてください。これにより、名前空間がより明確になり、後からのモンキーパッチが可能になり、インポートの問題が解決されます。 moduleをインポート時にインポートするだけなので、内部のclassがまだ定義されていないことを気にする必要はありません。クラスを使い始める頃には、そうなります。

したがって、test/users.py:

import test.teams

class User:
    def setTeam(self, t):
        if isinstance(t, test.teams.Team):
            self.team = t

test/teams.py:

import test.users

class Team:
    def setLeader(self, u):
        if isinstance(u, test.users.User):
            self.leader = u

test lessと書きたい場合は、from test import teams、次にteams.Teamでも構いません。これは、モジュールメンバーではなく、まだモジュールをインポートしています。

また、TeamUserが比較的単純な場合は、同じモジュールに配置します。 Java one-class-per-fileイディオムに従う必要はありません。isinstanceテストおよびsetメソッドもunpythonic-Java-wartを叫びます私には、あなたが何をしているのかに応じて、型チェックされていないプレーンな@propertyを使用するほうがよいでしょう。

79
bobince

私。これを機能させるには、遅延インポートを使用できます。 1つの方法は、user.pyをそのままにし、team.pyを次のように変更することです。

class team:
    def setLeader(self, u):
        from test.user import user
        if issubclass(u, user.__class__):
            self.leader = u

iii。別の方法として、チームとユーザーのクラスを同じファイルに入れてみませんか?

3
snapshoe

悪い習慣/臭いは次のことです:

  • おそらく不必要な型チェック( ここも参照 )。ユーザー/チームとして取得したオブジェクトを使用し、それが壊れたときに例外を発生させます(ほとんどの場合、追加のコードを必要とせずに発生します)。これを残しておけば、循環インポートは(少なくとも今のところ)消えてしまいます。取得するオブジェクトがユーザー/チームのように動作する限り、オブジェクトは何でもかまいません。 ( ダックタイピング
  • 小文字のクラス(これは多かれ少なかれ好みの問題ですが、一般に受け入れられている標準( PEP 8 )は異なる方法で行います
  • セッターは必要ありません。あなたはただ言うことができます:my_team.leader=user_bおよびuser_b.team=my_team
  • データの整合性に関する問題:もし(my_team.leader.team!=my_team)
2
knitti

依存関係グラフを修正するだけです。たとえば、ユーザーはチームの一部であることを知る必要がない場合があります。ほとんどの循環依存関係は、このようなリファクタリングを認めています。

# team -> user instead of team <-> user
class Team:
    def __init__(self):
        self.users = set()
        self.leader = None

    def add_user(self, user):
        self.users.add(user)

    def get_leader(self):
        return self.leader

    def set_leader(self, user):
        assert user in self.users, 'leaders must be on the team!'
        self.leader = user

循環依存は、リファクタリングを大幅に複雑にし、コードの再利用を禁止し、テストにおける分離を減らします。

Python=では、実行時にインポートしたり、モジュールレベルにインポートしたり、ここで説明したその他のトリックを使用したりすることで、ImportErrorを回避することが可能ですが、これらの戦略は設計上の欠陥を克服するために紙を使用します。可能な限り循環インポートを避ける価値があります。

0
chadlagore

これは私がまだ見たことがないものです。 sys.modulesを直接使用するのは悪いアイデア/デザインですか? @bobinceソリューションを読んだ後、インポートビジネス全体を理解していると思いましたが、 question に似た問題に遭遇しました。これはこれにリンクしています。

これがソリューションの別の見方です:

# main.py
from test import team
from test import user

if __== '__main__':
    u = user.User()
    t = team.Team()
    u.setTeam(t)
    t.setLeader(u)

# test/team.py
from test import user

class Team:
    def setLeader(self, u):
        if isinstance(u, user.User):
            self.leader = u

# test/user.py
import sys
team = sys.modules['test.team']

class User:
    def setTeam(self, t):
        if isinstance(t, team.Team):
            self.team = t

ファイルtest/__init__.pyファイルが空です。これが機能する理由は、test.teamが最初にインポートされるためです。 pythonがファイルをインポート/読み取りしているときに、モジュールがsys.modulesに追加されます。test/user.pyをインポートすると、モジュールtest.teamはすでに定義されているため、 main.pyにインポートしています。

非常に大きくなるが、相互に依存する関数とクラスがあるモジュールについては、このアイデアが好きになり始めています。 util.pyというファイルがあり、このファイルに相互に依存する多くのクラスが含まれているとします。おそらく、互いに依存する異なるファイル間でコードを分割することができます。循環インポートを回避するにはどうすればよいですか?

まあ、util.pyファイルでは、他の「プライベート」ファイルからすべてのオブジェクトをインポートするだけです。これらのファイルは直接アクセスするためのものではなく、元のファイルからアクセスするため、プライベートと言います。

# mymodule/util.py
from mymodule.private_util1 import Class1
from mymodule.private_util2 import Class2
from mymodule.private_util3 import Class3

次に、他の各ファイルで:

# mymodule/private_util1.py
import sys
util = sys.modules['mymodule.util']
class Class1(object):
    # code using other classes: util.Class2, util.Class3, etc

# mymodule/private_util2.py
import sys
util = sys.modules['mymodule.util']
class Class2(object):
    # code using other classes: util.Class1, util.Class3, etc

sys.modulesの呼び出しは、mymodule.utilを最初にインポートしようとする限り機能します。

最後に、これは読みやすさ(短いファイル)をユーザーに提供するために行われていることだけを指摘します。したがって、循環インポートが「本質的に」悪いとは言えません。すべてを同じファイルで実行することもできますが、これを使用して、巨大なファイルをスクロールしながらコードを分離し、混乱しないようにします。

0
jmlopez