简体   繁体   中英

How to make instance specific methods in python

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, eg:

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.

Eg 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.

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).

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()

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