en C #, nous devons obtenir / définir des règles, mais je ne sais pas comment faire cela en Python.
Exemple: Les orcs ne peuvent équiper que des haches, les autres armes ne sont pas éligibles Les humains ne peuvent équiper que des épées, les autres armes sont éligibles.
Comment puis-je dire à Python qu'un Orc ne peut pas faire quelque chose comme dans l'exemple ci-dessus?
Merci pour les réponses à l'avance, j'espère que cela a du sens pour vous les gars.
3 Réponses :
Le langage Python n'a pas de mécanisme efficace pour restreindre l'accès à une instance ou une méthode. Il existe cependant une convention pour faire précéder le nom d'un champ / d'une méthode d'un trait de soulignement pour simuler un comportement «protégé» ou «privé».
Mais, tous les membres d'une classe Python sont publics par défaut.
Si vous voulez que la classe Orc
ait certains membres mais pas la classe Human
, et que vous voulez toujours que ces classes soient liées (comme les deux étant des caractères), c'est exactement pour quel héritage. Voici un exemple de hiérarchie d'héritage:
Voici un exemple d'implémentation:
class Weapon: def __init__(self, damage): self.damage = damage class Gun(Weapon): def __init__(self): Weapon.__init__(self, 300) class Grenade(Weapon): def __init__(self): Weapon.__init__(self, 1000) class Knife(Weapon): def __init__(self): Weapon.__init__(self, 50) class Hammer(Weapon): def __init__(self): Weapon.__init__(self, 100) class Character: def __init__(self, name, default_weapon, additional): self.name = name self.default_weapon = default_weapon self.additional = additional def attack(self): pass # some implementation class Heavily_armed(Character): def __init__(self, name): Character.__init__(self, name, Gun(), Grenade()) class Lightly_armed(Character): def __init__(self, name): Character.__init__(self, name, Knife(), Hammer()) class Human(Lightly_armed): def __init__(self, name, age, height): Lightly_armed.__init__(self, name) self.height = height self.age = age class Orc(Heavily_armed): def __init__(self, name, danger): Heavily_armed.__init__(self, name) self.danger = danger
Comme vous peut voir que chaque Personnage
possède des armes, mais ce sont des types d'armes différents. Pour développer cela, vous pouvez créer un ensemble «d'armes disponibles» pour chaque type et créer une instance d'une certaine arme uniquement si elle est dans l'ensemble. Cela peut ou non être un choix de conception viable pour votre implémentation du jeu. Les choix sont infinis
Ne serait-il pas préférable d'avoir dans chaque classe dérivée de Character
une propriété qui spécifie quelles armes sont autorisées? Quelque chose comme allowed_weapons
et appeler une méthode is_weapon_allowed
chaque fois que le personnage tente d'équiper une arme? Cela permettrait également une approche légèrement différente dans laquelle vous pourriez spécifier quelle classe d'armes est autorisée (par exemple, armes à distance, mêlée, etc.)
Le problème avec cette approche est que vous ne pouvez l'utiliser qu'une seule fois. Supposons que nous voulions maintenant «magique» et «banal», et que nous voulions aussi «mal» et «bien», en plus de «lourd» et «léger». Un orc est une créature magique mal armée lourdement armée, un elfe est un bon créateur magique légèrement armé, et un humain est une bonne créature mondaine légèrement armée. À quoi ressemble maintenant la hiérarchie de classes? Les hiérarchies de classes capturent très mal ce type de règles et je le déconseille vivement.
Nous voyons ces problèmes dans d'autres domaines également. Les débutants à l'OO reçoivent souvent "véhicule" comme exemple, puis vous obtenez les sous-classes "véhicule terrestre" et "véhicule nautique". Ensuite, vous demandez "que se passe-t-il si nous voulons classer en fonction des militaires par rapport aux civils également?" et vous découvrez qu'il n'y a pas de place dans la hiérarchie des types qui correspond au "transporteur de troupes amphibies".
@EricLippert Pourquoi ne pas créer des classes abstraites "diaboliques" et "bonnes" et les ajouter à Orc et Human comme classes de base? Python prend en charge l'héritage multiple. Comment aborderiez-vous ce problème?
J'aurais dû dire que les langues à héritage unique supportent particulièrement mal ces concepts. Les langages qui ont plusieurs héritages, comme Python, ou les langages qui ont des traits, peuvent faire un peu mieux, mais il y a encore de nombreux pièges.
@Ayxan: Cela semble étrange d'avoir un humain implémentant à la fois le mal et le bien (et une classe séparée pour EvilHuman et GoodHuman est tout aussi moche). Pour moi, il est plus logique que l'alignement soit un champ ou une propriété mutable, plutôt qu'une classe de base.
@Brian Je pense qu'EvilHuman qui est dérivé à la fois de Human et Evil est un beau design. Ensuite, vous pouvez dériver n'importe quel type de Evil pour le rendre mauvais. Je veux dire bien sûr que ma conception ici est beaucoup plus simple que ce que vous auriez dans un vrai projet, mais quand même
@Ayxan: Cette approche fait un mauvais travail de traitement du cas d'un humain qui change d'alignement. Il double également le nombre de nœuds feuilles sur la hiérarchie d'héritage de classe de personnage de votre joueur.
en C #, nous devons obtenir / définir des règles, mais je ne sais pas comment faire cela en Python.
Non. Getters and Setters ne vous aidera pas ici. Notez que Python aussi a des getters / setters et des dunders (quelque chose comme
self .__ foo
), mais ne suivons pas ce chemin.
À la place , regardons ce que vous avez:
- un tas de choses (comme les orcs et les humains et épées et autres)
- un tas d'actions (ok, actuellement ce n'est qu'une action, maniez , mais peut-être que demain vous déciderez qu'un vampire peut boire du sang, mais pas un humain)
- et un tas de règles (une hache est une arme, une épée est une arme, les orcs ne peuvent utiliser que la hache, les humains peuvent utiliser d'autres armes, ...).
Alors, essayons de modéliser notre jeu de cette façon: avec
Choses
,Actions
etRègles
. p >Parce que nous sommes des enfants sympas, commençons par écrire nos règles sous forme de texte:
rules =[ "Human is Person", "Orc is Person", "Person may wield Weapon", "Human may not wield Axe", "Orc may only wield Axe", "Sword is Weapon", "Bow is Weapon", "Axe is Weapon", "Cow is Animal", "Animal may eat Grass" ] class Rules: alias_list = [] prohibition_list = [] permission_list = [] exclusive_list = [] def parse_rules(rules): for rule in rules: if ' is ' in rule: type, alias = rule.split(' is ') Rules.alias_list.append((type, alias)) elif ' may only ' in rule: obj, rest = rule.split(' may only ') action, second = rest.split(' ') Rules.exclusive_list.append((obj, action, second)) elif ' may not ' in rule: obj, rest = rule.split(' may not ') action, second = rest.split(' ') Rules.prohibition_list.append((obj, action, second)) elif ' may ' in rule: obj, rest = rule.split(' may ') action, second = rest.split(' ') Rules.permission_list.append((obj, action, second)) def resolve_types_inner(types, aliases): for (source_type, alias_type) in aliases[:]: if source_type in types: types.add(alias_type) aliases.remove((source_type, alias_type)) return Rules.resolve_types_inner(types, aliases) return types def resolve_types(thing): types = set(thing.type) return Rules.resolve_types_inner(types, Rules.alias_list[:]) def allowed(action_to_test): a_types = Rules.resolve_types(action_to_test.a) b_types = Rules.resolve_types(action_to_test.b) for (a, action, b) in Rules.exclusive_list: if action == action_to_test.name: if a in a_types and b in b_types: print ('-- allowed by exclusive_list') return True for (a, action, b) in Rules.prohibition_list: if action == action_to_test.name: if a in a_types and b in b_types: print ('-- forbidden') return False for (a, action, b) in Rules.permission_list: if action == action_to_test.name: if a in a_types and b in b_types: if not action in (x for (a2,x,b2) in Rules.exclusive_list if x == action and a2 in a_types): print ('-- allowed') return True else: print ('-- forbidden by exclusive_list') return False print ('-- no rules match') class Action: def __init__(self, name, a, b): self.name = name self.a = a self.b = b def invoke(self): print('You feel a strange sensation...') def forbidden(self): print(f"{self.a.name} tries to {self.name} {self.b.name}, but that is not possible") def __call__(self): if Rules.allowed(self): self.invoke() else: self.forbidden() print('----------') class Wield(Action): def __init__(self, thing, weapon): super().__init__('wield', thing, weapon) def invoke(self): if hasattr(self.a, 'weapon'): print(f'{self.a.name} drops {self.a.weapon.name}') self.a.weapon = self.b print(f'{self.a.name} now wields {self.a.weapon.name}') class Eat(Action): def __init__(self, thing, food): super().__init__('eat', thing, food) def forbidden(self): print(f'{self.a.name} tried to eat {self.b.name}, but did not like it very much...') def invoke(self): print(f'{self.a.name} eats {self.b.name}') class Thing: def __init__(self, name, *type): self.name = name self.type = ['Thing', *type] def action(self, action_class, *args): action_class(self, *args)() if __name__ == '__main__': Rules.parse_rules(rules) Carl_the_Human = Thing('Carl', 'Human') Grump_the_Orc = Thing('Grump', 'Orc') Sandy_the_Cow = Thing('Sandy', 'Cow') Carls_sword = Thing("Carl's Sword of Justice", 'Sword') Grumps_axe = Thing("Grump's rusty Axe", 'Axe') Old_bow = Thing("An old bow", 'Bow') Sandy_the_Cow.action(Wield, Grumps_axe) Sandy_the_Cow.action(Eat, Grumps_axe) Sandy_the_Cow.action(Eat, Thing("a bunch of grass", "Grass")) Carl_the_Human.action(Wield, Carls_sword) Carl_the_Human.action(Wield, Grumps_axe) Carl_the_Human.action(Wield, Old_bow) Grump_the_Orc.action(Wield, Grumps_axe) Grump_the_Orc.action(Wield, Carls_sword)Comme vous pouvez le voir, je parle de vaches, d'animaux et d'herbe, aussi, pour que nous puissions voir que nous allons faire une approche très générique.
Nous savons que nos "choses" ont différents types, un nom et une façon d'invoquer une "action", donc voici notre classe
Thing
:-- forbidden the Archer tries to wield Carl's Sword of Justice, but that is not possible ---------- -- allowed the Archer now wields An old bow ---------- -- allowed the Archer drops An old bow the Archer now wields A golden Spear ---------- -- allowed the old Guy now wields Carl's Sword of Justice ---------- -- forbidden the old Guy tries to wield An old bow, but that is not possible ---------- -- allowed the old Guy drops Carl's Sword of Justice the old Guy now wields A golden Spear ----------Une
Thing
est de type'Thing'
et tout ce que nous transmettons à__init__
, et nous pouvons appeler la fonctionaction
avec une classeAction
(nous la créons sous peu) et quelques arguments que nous transmettons à cette fonction.Pour l'instant, tout simplement.
Voici à quoi pourrait ressembler une
XXXAction
générique:Simplement un nom et deux choses (
a
etb
). Il peut être appelé (par exemple parThing.action
), et il est soit autorisé à être appelé (puis appelerinvoke
) ou non (puis appelerfobidden ).
Ignorons pour l'instant
Rules.allowed
et créons des actions qui font quelque chose:Swordman = Thing('the old Guy', 'Swordman') Archer = Thing('the Archer', 'Archer') Carls_sword = Thing("Carl's Sword of Justice", 'Sword') Old_bow = Thing("An old bow", 'Bow') Spear = Thing("A golden Spear", 'Spear') Archer.action(Wield, Carls_sword) Archer.action(Wield, Old_bow) Archer.action(Wield, Spear) Swordman.action(Wield, Carls_sword) Swordman.action(Wield, Old_bow) Swordman.action(Wield, Spear)Le
Wield définira l '
arme
de l'appelant, mais uniquement si elle est autorisée. L'actionEat
, eh bien, affiche juste un message pour le moment ...Donc, la seule chose qui nous reste à faire maintenant est d'implémenter les
Rules.allowed
, cela signifie analyser les règles que nous avons créées au début et agir en conséquence.
Voici la classe
Rules
:"Ranged is Weapon", "Melee is Weapon", "Bow is Ranged", "Spear is Ranged", "Sword is Melee", "Human is Person", "Archer is Human", "Swordman is Human", "Person may wield Weapon", "Archer may not wield Melee", "Swordman may not wield Bow"
Bien sûr, c'est juste très basique et pas un Moteur de règles à part entière ou langage de programmation logique, mais cela fera l'affaire pour le moment.
Nous prenons déjà en charge 4 fonctionnalités:
- Alias. Nous pouvons dire que quelque chose A est quelque chose B, et toutes les règles de B s'appliquent à A
- Autoriser quelque chose
- Interdire quelque chose
- Autoriser A quelque chose uniquement pour un B spécifique
La fonction
parse_rules
divise simplement les chaînes et ajoute les parties à différentes listes, et dans la fonctionallowed
nous itérons ces listes pour déterminer si quelque chose est autorisé ou non.N'hésitez pas à améliorer ceci ou à ajouter de nouvelles fonctionnalités.
Nous sommes maintenant prêts à partir.
Laissez-nous exécutez ce qui suit:
-- no rules match Sandy tries to wield Grump's rusty Axe, but that is not possible ---------- -- no rules match Sandy tried to eat Grump's rusty Axe, but did not like it very much... ---------- -- allowed Sandy eats a bunch of grass ---------- -- allowed Carl now wields Carl's Sword of Justice ---------- -- forbidden Carl tries to wield Grump's rusty Axe, but that is not possible ---------- -- allowed Carl drops Carl's Sword of Justice Carl now wields An old bow ---------- -- allowed by exclusive_list Grump now wields Grump's rusty Axe ---------- -- forbidden by exclusive_list Grump tries to wield Carl's Sword of Justice, but that is not possible ----------nous obtenons le résultat suivant:
# prepare our simple rule engine Rules.parse_rules(rules) # Let some things exist in the world Carl_the_Human = Thing('Carl', 'Human') Grump_the_Orc = Thing('Grump', 'Orc') Sandy_the_Cow = Thing('Sandy', 'Cow') Carls_sword = Thing("Carl's Sword of Justice", 'Sword') Grumps_axe = Thing("Grump's rusty Axe", 'Axe') Old_bow = Thing("An old bow", 'Bow') # Sandy is hungry Sandy_the_Cow.action(Wield, Grumps_axe) Sandy_the_Cow.action(Eat, Grumps_axe) Sandy_the_Cow.action(Eat, Thing("a bunch of grass", "Grass")) # Carl wants to try some weapons Carl_the_Human.action(Wield, Carls_sword) Carl_the_Human.action(Wield, Grumps_axe) Carl_the_Human.action(Wield, Old_bow) # Grump wants to try some weapons Grump_the_Orc.action(Wield, Grumps_axe) Grump_the_Orc.action(Wield, Carls_sword)Chaque fois que nous avons besoin d'une nouvelle "règle" dans notre monde du jeu, nous pouvons simplement l'ajouter à notre liste de règles sous forme de texte simple et laisser notre moteur de règles simple décider si quelque chose est autorisé (ou même comment quelque chose devrait se passer, si nous étendons notre moteur).
Alors peut-être nous avons des armes à distance et de mêlée, et les épéistes peuvent également utiliser des lances mais pas des arcs, et les archers peuvent utiliser des arcs et des lances mais pas des armes de mêlée?
Pas de problème, écrivez-le simplement dans les règles:
class Rules: alias_list = [] prohibition_list = [] permission_list = [] exclusive_list = [] def parse_rules(rules): for rule in rules: if ' is ' in rule: type, alias = rule.split(' is ') Rules.alias_list.append((type, alias)) elif ' may only ' in rule: obj, rest = rule.split(' may only ') action, second = rest.split(' ') Rules.exclusive_list.append((obj, action, second)) elif ' may not ' in rule: obj, rest = rule.split(' may not ') action, second = rest.split(' ') Rules.prohibition_list.append((obj, action, second)) elif ' may ' in rule: obj, rest = rule.split(' may ') action, second = rest.split(' ') Rules.permission_list.append((obj, action, second)) def resolve_types_inner(types, aliases): for (source_type, alias_type) in aliases[:]: if source_type in types: types.add(alias_type) aliases.remove((source_type, alias_type)) return Rules.resolve_types_inner(types, aliases) return types def resolve_types(thing): types = set(thing.type) return Rules.resolve_types_inner(types, Rules.alias_list[:]) def allowed(action_to_test): a_types = Rules.resolve_types(action_to_test.a) b_types = Rules.resolve_types(action_to_test.b) for (a, action, b) in Rules.exclusive_list: if action == action_to_test.name: if a in a_types and b in b_types: print ('-- allowed by exclusive_list') return True for (a, action, b) in Rules.prohibition_list: if action == action_to_test.name: if a in a_types and b in b_types: print ('-- forbidden') return False for (a, action, b) in Rules.permission_list: if action == action_to_test.name: if a in a_types and b in b_types: if not action in (x for (a2,x,b2) in Rules.exclusive_list if x == action and a2 in a_types): print ('-- allowed') return True else: print ('-- forbidden by exclusive_list') return False print ('-- no rules match')class Wield(Action): def __init__(self, thing, weapon): super().__init__('wield', thing, weapon) def invoke(self): if hasattr(self.a, 'weapon'): print(f'{self.a.name} drops {self.a.weapon.name}') self.a.weapon = self.b print(f'{self.a.name} now wields {self.a.weapon.name}') class Eat(Action): def __init__(self, thing, food): super().__init__('eat', thing, food) def forbidden(self): print(f'{self.a.name} tried to eat {self.b.name}, but did not like it very much...') def invoke(self): print(f'{self.a.name} eats {self.b.name}')
class Action: def __init__(self, name, a, b): self.name = name self.a = a self.b = b def invoke(self): print('You feel a strange sensation...') def forbidden(self): print(f"{self.a.name} tries to {self.name} {self.b.name}, but that is not possible") def __call__(self): if Rules.allowed(self): self.invoke() else: self.forbidden() print('----------')
Voici le code complet exécutable que vous pouvez essayer nous-mêmes:
class Thing: def __init__(self, name, *type): self.name = name self.type = ['Thing', *type] def action(self, action_class, *args): action_class(self, *args)()Notez qu'il existe des langages de programmation pour exactement cela, comme Inform7 .
Si vous voulez en savoir plus, je vous suggère de lire le Série Wizards and warriors d'Eric Lippert, qui parle exactement de ce problème (et ma réponse est inspirée de cette série), et utilise même des exemples similaires (classes et armes fantastiques), mais à mon humble avis écueil courant dans les langages de programmation OO pour modéliser les mauvaises choses avec des objets et essayer de forcer la logique métier dans le système de type des langages.
Eh bien, cela répond à peu près à toutes les questions que j'avais posées et plus encore, merci! :)
@JohnnyNiklasson Eh bien, le tl; dr; est essentiellement: vous ne pouvez pas et ne devez pas modéliser les règles métier avec une hiérarchie de classes.
Je n'aurais pas dit mieux moi même!
@EricLippert Je suis fan de votre blog, et la série Wizard and warrios est l'une de mes préférées. Merci pour cela. Je l'ai recommandé à au moins une douzaine de personnes et j'espère qu'un jour, les gens s'arrêteront avec la classe Cow extend Animal
-nonsense.
Cherchez-vous des membres privés? qu'entendez-vous par "get / set"? Fonctions Getter / Setter?
Disons que je joue à World of Warcraft en version classique, les humains ne pouvaient équiper que des armes certian au début et certaines armes étaient spécifiques à une classe, comment puis-je rendre certaines armes (objets) spécifiques à une race?
Cela dépend de la manière dont les armes sont mises en œuvre. Le moyen le plus simple serait de vérifier le type, mais ce n'est généralement pas recommandé. Pourquoi ne pas créer une classe
Character
et la spécialiser avec des sous-classes commeOrc
et ajouter l'arme que vous voulez qu'elles aient comme membre de données: