Reputation: 73
So I've come across this problem, it's kind of hard to explain so i'll try with a pizza analogy:
We have the following classes:
class Storage:
# this seems like i should use a dict, but let's assume there is more functionality to it
def __init__(self, **kwargs):
self.storage = kwargs
# use like: Storage(tomato_cans=50, mozzarella_slices=200, ready_dough=20)
def new_item(self, item_name: str, number: int):
self.storage[item_name] = number
def use(self, item_name: str, number: int):
self.storage[item_name] = self.storage.get(item_name) - number
def buy(self, item_name: str, number: int):
self.storage[item_name] = self.storage.get(item_name) + number
class Oven:
def __init__(self, number_parallel):
# number of parallel pizzas possible
self.timers = [0] * number_parallel
def ready(self):
return 0 in self.timers
def use(for_mins):
for i, timer in enumerate(self.timers):
if timer == 0:
self.timers[i] = for_mins
break
def pass_time(mins):
for i in range(len(self.timers)):
self.timers[i] = max(0, self.timers[i]-mins)
class Pizza:
def __init__(self, minutes=6, dough=1, tomato_cans=1, mozzarella_slices=8, **kwargs):
self.ingredients = kwargs
self.ingredients['dough'] = dough
self.ingredients['tomato_cans'] = tomato_cans
self.ingredients['mozzarella_slices'] = mozzarella_slices
self.minutes = minutes
def possible(self, oven, storage):
if not oven.ready():
return False
for key, number in self.ingredients:
if number > storage.storage.get(key, 0):
return False
return True
def put_in_oven(self, oven, storage):
oven.use(self.minutes)
for key, number in self.ingredients:
storage.use(key, number)
We can make Pizzas now, e.g.:
storage = Storage()
oven = Oven(2)
margherita = Pizza()
prosciutto = Pizza(ham_slices=7)
if margherita.possible():
margherita.put_in_oven()
storage.new_item('ham_slices', 20)
if prosciutto.possible():
prosciutto.put_in_oven()
And now my question (sorry if this was too detailed):
Can I create a Pizza instance and change it's put_in_oven method?
Like for example a Pizza where you'd have to cook some vegetables first or check if it's the right season in the possible method.
I imagine something like:
vegetariana = Pizza(paprika=1, arugula=5) # something like that i'm not a pizzaiolo
def vegetariana.put_in_oven(self, oven, storage):
cook_vegetables()
super().put_in_oven() # call Pizza.put_in_oven
I hope this question is not too cumbersome!
Edit:
So let's suppose we would use inheritance:
class VeggiePizza(Pizza):
def put_in_oven(self, oven, storage):
self.cut_veggies()
super().put_in_oven(oven, storage)
def cut_veggies(self):
# serves purpose of explaining
# analogy has its limits
pass
class SeasonalPizza(Pizza):
def __init__(self, season_months, minutes=6, dough=1, tomato_cans=1, mozzarella_slices=8, **kwargs):
self.season_months # list of month integers (1 - 12)
super().__init__()
def possible(self, oven, storage):
return super().possible(oven, storage) and datetime.datetime.now().month in self.season_months
My Problem with that is, because I might make a Seasonal Veggie Pizza or other Subclasses or again different combinations of them or even Subclasses which may serve only one instance.
E.g. For a PizzaAmericano
(has French Fries on top), I'd use a Subclass like VeggiePizza and put fry_french_fries()
in front of super().put_in_oven()
and I'd definitely not use that Subclass for any other instance than the pizza_americano
(unlike the VeggiePizza
, where you can make different vegetarian pizze).
Is that ok? For me it seems to contradict to the principle of classes.
EDIT:
Okay, thanks to your answers and this recommended question I now know how to add/change a method of an instance. But before I close this question as a duplicate; Is that generally something that's totally fine or rather advised against? I mean it seems pretty unnatural for the simplicity of it's nature, having an instance specific method, just like instance specific variables.
Upvotes: 0
Views: 192
Reputation: 343
2 possibilities:
either create a case like structure using dicts:
def put_in_oven1(self, *args):
# implementation 1
print('method 1')
def put_in_oven2(self, *args):
# implementation 2
print('method 2')
class pizza:
def __init__(self, method, *args):
self.method = method
pass
def put_in_oven(self, *args):
handles = {
1: put_in_oven1,
2: put_in_oven2}
handles[self.method](self, *args)
my_pizza1 = pizza(1) # uses put_in_oven1
my_pizza1.put_in_oven()
my_pizza2 = pizza(2) # uses put_in_oven2
my_pizza1.put_in_oven()
my_pizza2.put_in_oven()
Or you can change methods dynamically with the setattr
so for example:
from functools import partial
def put_in_oven1(self, *args):
# implementation 1
print('method 1')
def put_in_oven2(self, *args):
# implementation 2
print('method 2')
class pizza:
def __init__(self, *args, **kwargs):
# init
pass
def put_in_oven(self, *args):
# default method
print('default')
pizza1 = pizza()
setattr(pizza1, 'put_in_oven', partial(put_in_oven, self=pizza1))
pizza2 = pizza()
setattr(pizza2, 'put_in_oven', partial(put_in_oven, self=pizza2))
pizza1.put_in_oven()
pizza2.put_in_oven()
or without using partial and defining the methods inside the pizza class
#!/usr/bin/env python
# -*- coding: utf-8 -*-
class pizza:
def put_in_oven1(self, *args):
# implementation 1
print('method 1')
def put_in_oven2(self, *args):
# implementation 2
print('method 2')
def __init__(self, *args, **kwargs):
pass
def put_in_oven(self, *args):
# default
print('default')
pizza1 = pizza()
setattr(pizza1, 'put_in_oven', pizza1.put_in_oven1)
pizza1.put_in_oven()
pizza2 = pizza()
setattr(pizza2, 'put_in_oven', pizza2.put_in_oven2)
pizza2.put_in_oven()
Upvotes: 0
Reputation: 77912
You can define per instance "methods" indeed (nb: py3 example) - python's "methods" are basically just functions - the only trick is to make sure the function has access to the current instance. Two possible solutions here: use a closure, or explicitely invoke the descriptor protocol on the function.
1/ : with a closure
class Foo:
def __init__(self, x):
self.x = x
def foo(self, bar):
return bar * self.x
def somefunc():
f = Foo(42)
def myfoo(bar):
# myfoo will keep a reference to `f`
return bar * (f.x % 2)
f.foo = myfoo
return f
2/ with the descriptor protocol
# same definition of class Foo
def somefunc()
f = Foo()
def myfoo(self, bar):
return bar * (self.x % 2)
# cf the link above
f.foo = myfoo.__get__(f, type(f))
return f
but the more general solution to your issue are the strategy pattern and possibly the state pattern for the case of SeasonalPizza.possible()
Since your example is a toy exemple I won't bother giving an example with those solution, but they are very straightforward to implement in Python.
Also note that since the goal is mainly to encapsulate those details so the client code doesn't have to bother about which kind of pizza it's dealing with, you'll need some [creational pattern] to deal with this. Note that python classes are already factories, due to the two-stages instanciation process - the constructor __new__()
creates an empty uninitialized instance, which is then passed to the initializer __init__()
. This means that you can override __new__()
to return whatever you want... And since Python's classes are objects themselves, you can extend this further by using a custom metaclass
As a last note: just make sure you keep compatible signatures and return types for all your methods, else you'll break the Liskov subsitution principle and loose the first and main benefit of OO which is to replace conditionals by polymorphic dispatch (IOW: if you break LSP, your client code can no more handle all pizzas type uniformly and ends up full of typechecks and conditionals, which is exactly what OO tries to avoid).
Upvotes: 1