30

I am using a mixin to separate a range of functionality to a different class. This Mixin is only supposed to be mixable with the only child class:

class Mixin:
    def complex_operation(self):
        return self.foo.capitalize()

class A(Mixin):
    def __init__(self):
        self.foo = 'foo'

in my method Mixin.complex_operation PyCharm gives warning 'Unresolved Attribute Reference foo'.

Am I using the mixin pattern correctly? Is there a better way? (I would like to have type hints and autocompletion in my mixins, and I would like to have multiple mixins.)

ikamen
  • 3,175
  • 1
  • 25
  • 47
  • I copy-and-pasted your code and `print (A().complex_operation())` printed "Foo". Is there more code we should see? As for a better way, inheritance is fine for mixins, and type hints + autocompletion is completely dependent on your choice of IDE. – Levi Lesches Mar 17 '19 at 18:01
  • Yes, it works - but as mentioned IDE complains and I have no autocompletion... – ikamen Mar 17 '19 at 18:02
  • Which IDE are you using? It shouldn't be evaluating the variables in a function because even Python doesn't do that -- for this reason exactly. – Levi Lesches Mar 17 '19 at 18:05
  • I am using PyCharm. It does check if I am using existing variables or not, but ofcourse it lets me execute code even if it suspects it has a problem. – ikamen Mar 17 '19 at 18:07
  • I don't know much about PyCharm, but it seems as though it expects `self` to have `foo`, but `Mixin`s don't have it. And it has a point -- not every class that uses this mixin is guarenteed to have a `foo`. Maybe you want `foo` to be a class attribute (or at least set in the `__init__`) of `Mixin` instead of `A`. – Levi Lesches Mar 17 '19 at 18:10
  • I only want this Mixin to work with the class A, because it serves only one purpose - to separate functionality better. Perhaps indeed the solution is to declare the needed attributes inside of the Mixin, but then I am kind of duplicating code. Again, the mixin is not usable on itself or in combination with any other class in my plan. – ikamen Mar 17 '19 at 18:14
  • Well then, is it really a mixin? Mixins by definition are modular and can be added to any class to extend functionality. I understand wanting to have cleaner code but adding mixins here is unnecessary and confusing. `complex_operation` should be a method of `A`. – Levi Lesches Mar 17 '19 at 18:16
  • Where does this definition of Mixins come from? "extending any class" is obviously an overstatement. I made this example just to clarify the situation, in reality I have a core class of a video game, and I am separating AI functionality from it ,e.g. game can make a copy of itself for AI to test actions. This is never needed in the normal game for any other purpose. This will definitely not work with any other class but the game class. – ikamen Mar 17 '19 at 18:19
  • Okay... so I would go about this one of two ways: 1) move the "mixin" class to another file, so you can import it and keep your code DRY. Instead of a class you would have a bunch of regular functions. 2) You could also have an `__init__` for your mixin that accepts an `A` so that instead of referencing `self.foo`, it would reference `self.a.foo`. I have found a combination of both of these to work: have the class from (2) in another file, then import it. – Levi Lesches Mar 17 '19 at 18:27

3 Answers3

22

Declare the necessary fields in the Mixin like:

class Mixin:
    foo: str

    def complex_operation(self):
        return self.foo.capitalize() 

This way the mixin actually declares the fields a class must have to be able to use this mixin. Type hint will create warnings if extending class will put incompatible type into declared field.

edit: Replaced foo = None with foo:str as suggested by @valex

valex
  • 5,163
  • 2
  • 33
  • 40
ikamen
  • 3,175
  • 1
  • 25
  • 47
7

I see few options.

1) Type annotations (i think this is cleanest solution):

class Mixin:
    foo: str

    def complex_operation(self):
        return self.foo.capitalize()

2) Default None (@ikamen option):

class Mixin:
    foo = None

    def complex_operation(self):
        return self.foo.capitalize()

3) Suppress unresolved reference error for class or for specific line (i think this is more dirty way than first two):

# noinspection PyUnresolvedReferences
class Mixin:
    def complex_operation(self):
        return self.foo.capitalize()
class Mixin:
    def complex_operation(self):
        # noinspection PyUnresolvedReferences
        return self.foo.capitalize()
valex
  • 5,163
  • 2
  • 33
  • 40
  • 1
    I think the type annotation is strictly better than the None. I have put it into my answer as well. – ikamen Jul 18 '19 at 08:14
  • 1
    Does the type annotation version set a class variable `foo` with value `""` or is it simply an announcement? And how would this work, if you'd expect a method to be there? – Minix Dec 12 '19 at 14:23
  • @Minix type annotation is just an announcement. – valex Dec 13 '19 at 08:14
3

So just to compiling my thoughts from the comments for everyone else: The problem is keeping the two classes intrinsically connected while separating functionality. Here are my solutions:

1) Make a module

Have another file, say mixin.py, that has complex_operation as a function. Instead of accepting self as a parameter, have it take a string:

# mixin.py

def complex_operation (foo: str) -> str: return foo.capitalize()

# main.py

from ai import complex_operation
class A: 
    def __init__(self): self.foo = "foo"
print (complex_operation (A().foo))

2) Make a class to accept another class as a parameter

In Mixin's __init__ function, add a parameter to accept an A, and then use that in its methods:

# mixin.py

class Mixin: 
    def __init__(self, a: A): self.a = a
    def complex_operation(self): return self.a.foo.capitalize()

# main.py

from mixin import Mixin
class A:
    def __init__(self): self.foo = "foo"

print (Mixin (A()).complex_operation())
Levi Lesches
  • 1,508
  • 1
  • 13
  • 23