3

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)

Joe
  • 6,758
  • 2
  • 26
  • 47
dps_kane
  • 43
  • 5

1 Answers1

1

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.

Joe
  • 6,758
  • 2
  • 26
  • 47
  • Thanks for the links, I already studied them a bit. Since weapons can indeed exist without owners (can be handed over to other animals or simply lie around) I am more inside the aggregation case for that relationship. (teams seem more complicated as animals can be without teams, but teams always need a *set* of animals). "that's what your current problem is, right?" - exactly. I want to be able to go in an *efficient* way (O(1)) from weapons to their owners. I am not sure how to reference by id, because then I need to reference a dictionary of animals inside weapons -> same problem, or not? – dps_kane Apr 07 '20 at 11:59
  • Dictionaries are fine for now. There could be a dictionary with all weapon instances. The key is a unique weapon id. And there is a dictionary with all animals instances, with a unique animal id (or player id). Then you could just assign the weapon id to instances of the animal class: `animal.has_weapon = 51`. And you could also maintain the weapons dictionary: `weapon.used_by = 43`. Then you could use these ids to look up the users or weapons in the dictionaries. Looking up the attributes is a quite cheap operation: `damage = weapons_dict[animal.has_weapon].damage` – Joe Apr 07 '20 at 16:12
  • But if I use in my weapon class a dictionary id->animal - once I want to strong-type things I will have to import the type "animal" into my weapons file. Even if that dictionary is elsewhere, to access it I need to access a file that points directly or indirectly back to weapons, creating circular references. Am I missing something in the grand picture? – dps_kane Apr 08 '20 at 10:28
  • Hm, maybe I am missing something.The id is just some number, strong-typed as integer maybe and you use it to identify instances of both, weapons and animals. If you don't want the dictionary you can create a handling object around the animals or weapons, e.g. a registry. https://martinfowler.com/eaaCatalog/registry.html – Joe Apr 08 '20 at 10:59
  • Considering that I am not an expert beginner I see it more likely that I am missing something :) What I meant is, that the dictionary has keys and values. while the keys are integers, the values will again be objects. So inside Weapons, I would somewhere declare a variable that references to the animal_dict, and that variable would be of type Dict[int, Animal]. Likewise inside Animals, the weapon_dict, closing a loop. But I can imagine that maybe a circular reference is not what I understand as a circular reference.... – dps_kane Apr 08 '20 at 14:49
  • The weapons and animals dictionary could live a the global scope. And inside Weapons there is an attribute `wielder` which is just of type `int`. That int is a key to the animals dictionary. Same the other way round. Your understanding of circular reference seems to be fine. To look up the damage of a weapon you could use something like `damage = weapons_dict[animal.weapon_id].damage` – Joe Apr 08 '20 at 17:58
  • Oh..... finally I think I understand. That would thus not allow me to use the animal type inside weapons, but I can at least define the wielder method indirectly and use it everywhere else! Thanks, that made sense to me now! – dps_kane Apr 09 '20 at 06:54
  • I think you need to pick one of animals and weapons to be your "favourite". What you could think of is responsibilities. Which is more important. Also read about `Dependency Inversion`, that might help to find a good solution.Also, coming from Java, don't use something like `set_wielder` in Python. I will add another approach to the answer. – Joe Apr 09 '20 at 08:38
  • DI is something that I did read about before and I would like to live by it, but atm I have no real experience. In Java I would know much better how to use abstractions (like interfaces or ABCs), but maybe this is a good start to get into python interfaces ;) thanks for the adjusted answer, I never knew what variables starting with "_" were supposed to mean, that is good to know. Also I know very little about annotations atm. Thanks for your patience and all your help! – dps_kane Apr 14 '20 at 08:15