简体   繁体   中英

How to handle circular references between related objects from different modules in Python?

In an effort to improve my (beginner) Python skills I started a pet project and now I am having trouble with circular import issues.

The pet project is a little pokemon-esque game that features among other things teams of animals wearing weapons. The relationship chain: team -> animal -> weapon (a team consists of a few animals, each animal wields a weapon). To avoid overly huge classes I decided to spread the very different classes of animal and weapon over two files and I use import to access each other. Coming from Java I like to strong-type variables, arguments, and parameters.

So stripped down a bit, my classes weapons.py and animals.py look like this:

import weapons
class Animal():
  def __init__(self, name: str, level: int):
    self.name: str = name
    self.level: int = int
    self.weapon: Weapon or None = None
  def equip(self, weapon: Weapon) -> None:
    self.weapon = weapon
import animals
from abc import ABC
class Weapon(ABC):
  def __init__(self, type: str, power_level: float):
    self.type: str = type
    self.power_level: float = power_level
    self.wielder: Animal or None = None
  def set_wielder(wielder: Animal) -> None:
    self.wielder = wielder

So when I instantiate animals, I don't want them to wield weapons right away nor do I want weapons to have owners right away. But while the relationship animal -> weapon is rather straight forward in the game, I also want to have a way to point from a weapon to the animal that owns it.

The code above causes circular import issues. When facing a different yet related issue I found the interesting __future__ module. Adding " from __future__ import annotations " resolved my problem.

But while I am happy about my working code, I wonder whether I could have solved this issue in a more elegant manner. Whether this is smelly code. Whether there is a different solution to this all that still allows me to use typing. I am happy about any advice that improves my Python coding style (and my understanding of circular imports)

To get an idea of how to structure your code you could think in terms of composition, aggregation, association.

What is the difference between association, aggregation and composition?

https://www.visual-paradigm.com/guide/uml-unified-modeling-language/uml-aggregation-vs-composition/

Still there are several possibilities, you need to decide which is the most important one (owner HAS A weapon, weapon HAS A owner).

Say every weapon only has one owner at a time, how to you want to access the weapon?

owner.weapon -> then you know the owner

Or you could keep a reference to the owner as attribute of the weapon:

weapon.owned_by -> maybe use an id here not a reference to the actual class, that's what your current problem is, right?

Does a weapon exist without an owner? Then look at Composition :

Composition implies a relationship where the child cannot exist independent of the parent.

Example for a composition: House (parent) and Room (child). Rooms don't exist without a house.

Example for not a composition: Car and Tire. Tires exist without cars.

A general thread on why to better avoid circular references: https://softwareengineering.stackexchange.com/questions/11856/whats-wrong-with-circular-references

You can also try to consider the Dependency Inversion (Injection Principle) (see here or here ). I think you already tried that in your first approach (passing a Weapon instance into Animal). The idea was fine, but maybe you need another layer inbetween.

Another thing, coming from Java you are used to getters and setters. That is not that popular in Python, (but you could do it).

Your approach:

class Weapon(ABC):

  def set_wielder(wielder: Animal) -> None:
    self.wielder = wielder

More Pythonic, use Properties ("descriptors"):

class Weapon(ABC):

    def __init__(self):

        # notice the underscore, it indicates "treat as non-public"
        # but in Python there is no such thing
        self._wielder = None

    @property #this makes it work like a getter
    def wielder(self) -> Animal: # not sure about the annotation syntax
        return self._wielder

    @wielder.setter 
    def wielder(wielder: Animal) -> None:
        self._wielder = wielder   

You can read about descriptors here , here and with a bit more theory here .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM