Forum Archive

[Share] List Dialog - Simple

Phuket2

Below, is just a super simple list dialog using a ui.TableView. Pythonista contains the dialog module which handles this way better. This is just another way to look at it. If you needed a little bit more control for example. Anyway, I was just playing around and thought i would post it. I did get a little confused accessing variables outside the delegate closure. I followed a pattern that I found on stackflow for python 2. I could not assign to the outter scoped var my_sel the way that I thought I could based on the docs for py3 using nonlocal.
This is a language construct, so cant believe its a bug. I am just doing something wrong.

import ui

'''
ui.ListDataSource = LDS

Very crude example of a list dialog.
Just to show how easy it can be if you have very simple needs.
last_name is intentionally not displayed. LDS only knows about title,
image and accessory_type.
But the fact the dict contains other keys is not an issue.
'''


def show_list_dialog(items=None, *args, **kwargs):
    '''
    depending on your needs this could be your list dialog, you
    use over and over, although you would not because dialogs
    module has one already.
    '''
    items = items or []  # hmmmm thanks @ccc
    tbl = ui.TableView(**kwargs)
    tbl.data_source = ui.ListDataSource(items)

    # i used this because I could not get nonlocal working
    # as I thought it should work, i wanted to just use my_sel = None
    my_sel = {'value': None}

    class MyTableViewDelegate (object):
        # nonlocal my_sel (does not work as I understand it should)
        def tableview_did_select(self, tableview, section, row):
            my_sel['value'] = tableview.data_source.items[row]
            tableview.close()

    tbl.delegate = MyTableViewDelegate()

    tbl.present(style='sheet')
    tbl.wait_modal()  # This method is what makes this a dialog(modal)
    return my_sel['value']

if __name__ == '__main__':
    f = (0, 0, 400, 300)

    items = [{'title': 'Ian', 'last_name': "Jones"},
             {'title': 'Christian', 'last_name': "Smith"}]
    # uncomment the line below, to see the difference
    # items = ['Ian', 'John', 'Paul', 'Ringo']
    result = show_list_dialog(items, frame=f, name='Select a Name')
    print(result)
zrzka

What exactly didn't work for you with nonlocal? This works ...

import ui

def show_list_dialog(items=None, *args, **kwargs):
    items = items or []  # hmmmm thanks @ccc
    tbl = ui.TableView(**kwargs)
    tbl.data_source = ui.ListDataSource(items)

    selection = None

    class MyTableViewDelegate (object):
        def tableview_did_select(self, tableview, section, row):
            nonlocal selection
            selection = tableview.data_source.items[row]
            tableview.close()

    tbl.delegate = MyTableViewDelegate()

    tbl.present(style='sheet')
    tbl.wait_modal()  # This method is what makes this a dialog(modal)
    return selection

if __name__ == '__main__':
    f = (0, 0, 400, 300)

    items = [{'title': 'Ian', 'last_name': "Jones"},
             {'title': 'Christian', 'last_name': "Smith"}]
    # uncomment the line below, to see the difference
    # items = ['Ian', 'John', 'Paul', 'Ringo']
    result = show_list_dialog(items, frame=f, name='Select a Name')
    print(result)
zrzka

Also you can avoid creating MyTableViewDelegate by using data source as delegate and using data source's action. If you'd like to display something else than just title value, you can create your data source in this way and pass whatever you want as an item.

import ui


class MyDataSource(ui.ListDataSource):
    def tableview_cell_for_row(self, tv, section, row):
        item = self.items[row]
        cell = ui.TableViewCell('default')
        cell.text_label.text = '{} {}'.format(item['first_name'], item['last_name'])
        return cell


def show_list_dialog(items=None, **kwargs):
    result = None

    tbl = ui.TableView(**kwargs)
    tbl.data_source = MyDataSource(items or [])
    tbl.delegate = tbl.data_source

    def did_select(ds):
        nonlocal result
        result = ds.items[ds.selected_row]
        tbl.close()

    tbl.data_source.action = did_select
    tbl.present(style='sheet')
    tbl.wait_modal()

    return result

if __name__ == '__main__':
    f = (0, 0, 400, 300)
    items = [{'first_name': 'Ian', 'last_name': "Jones"},
             {'first_name': 'Christian', 'last_name': "Smith"}]
    result = show_list_dialog(items, frame=f, name='Select a Name')
    print(result)
Phuket2

@zrzka , nice. That's why I dont mind doing these simple examples. You can learn a lot from peoples input. I am still not sure why my nonlocal approached did work and yours does. But its the first time I have ever even used it. In my example, I was trying to simplify to a function instead of a class, because I think when guys are learning the ui module/python beginners, including me it can appear very daunting. I think the classes eventually simplify a lot of it. Anyway, its still all fun. And even though I have gotten away with using closures before, I didn't really understand the scoping. In my example I was trying to understand it.

JonB

@Phuket2 your nonlocal was in the wrong spot -- in the class, not the method.

Phuket2

@JonB , oh thanks. Now you point it out, its very clear. Even I haven't tested yet, but i get it! But happy to find out why

Phuket2

@JonB, just tested it. Works as expected. Thanks.

class MyTableViewDelegate (object):
        # nonlocal my_sel (does not work as I understand it should)
        def tableview_did_select(self, tableview, section, row):
            nonlocal my_sel
            my_sel = tableview.data_source.items[row]
            tableview.close()
zrzka

@Phuket2 you did mention that you were trying to understand scopes. Here're some explanations of how global and nonlocal works. It's simplified and it's still recommended to read Python documentation to fully understand it. But this should help a bit.

global

name = 'Phuket2'  # Global variable

def say_hallo():
    # You're just reading global 'name', thus no need
    # to use 'global' in this case
    print('Hallo {}'.format(name))

def update_without_global(new_name):
    # Here we're writing to 'name', so, new local variable
    # 'name' is created and global 'name' is untouched
    name = new_name
    # What can help here is analyzer warning:
    #  - local variable 'name' is assigned but never used

def update_with_global(new_name):
    # Here we're saying that we would like to modify
    # global variable 'name'
    global name
    name = new_name

say_hallo()  # Prints 'Hallo Phuket2'
update_without_global('Zrzka')
say_hallo()  # Still prints 'Hallo Phuket2' 
update_with_global('Zrzka')
say_hallo()  # Prints 'Hallo Zrzka'

But what about this?

attributes = {'name': 'Phuket2'}

def say_hallo():
    print('Hallo {}'.format(attributes['name']))

def update_without_global(new_name):
    attributes['name'] = new_name

say_hallo()  # Prints 'Hallo Phuket2'
update_without_global('Zrzka')
say_hallo()  # Prints 'Hallo Zrzka'

Ouch, what's going on one can say. I didn't use global here, but it was modified.

What is variable? It's called name in Python and every name refers to an object. This is called binding. You can read more here if you're interested (chapter 4.2.).

Basically everything is an object in Python. Really? What about primitive types for example? Python has no primitive types you know from Java or Objective C. Everything is an object in Python, even bool, int, ...

What plays big role here is mutability vs immutability and if you're mutating in place or assigning new object. Some types are immutable (bool, int, float, tuple, str, frozenset) and some are mutable (list, set, dict). Immutable objects can't be modified after you create them, mutable can.

# Immutable
x = 3
print(id(x)) # 4322496384
x += 1       # New object is created
print(id(x)) # 4322496416 != 4322496384

# Mutable
y = ['a']
print(id(y))  # 4454700936
y.append('b')
print(id(y))  # 4454700936 (equals)
y[:] = ['a', 'b', 'c']
print(id(y))  # 4454700936 (equals)
y = ['a', 'b', 'c']
print(id(y))  # 4461963720 (oops, new object b/o =)

x is of type int. This type is immutable. It looks like mutable type, but it isn't. Whenever you do x += 1, new object is created and x is rebinded to this new object. If type of your variable is immutable and you want to modify it, you have to use global.

y is of type list. This type is mutable. Whenever you do y.append('b'), it's still the same object. .append mutates it in place. Also y[:] = ['a', 'b'] mutates the list in place. It replaces all elements in the list, but it's done in place, no new object is created. So, you don't need global here as well.

But don't forget that simple assignment like y = ['a', 'b', 'c'] rebinds y variable to the new object (you're not mutating it in place) and you must use global in this case.

Let's pretend that you can't access class instance attributes prefixed with _ (actually you can, but let's pretend you can't). Following example shows immutable type Contact. If you want to modify name, you have to create new object (there's no setter for name property).

class Contact():
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

me = Contact('Zrzka')
me = Contact('Phuket2')

And here's mutable type MutableContact.

class MutableContact():
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new_name):
        self._name = new_name

me = Contact('Zrzka')
me.name = 'Phuket2'

Immutable types behaves like Contact (int, bool, ...) and mutable behaves like MutableContact (list, dict, ...). One more example, check __add__ method of MyInt.

class MyInt():
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

    def __add__(self, o):
        if not isinstance(o, MyInt):
            raise ValueError('MyInt can be added with MyInt only')
        return MyInt(self.value + o.value)

    def __str__(self):
        return 'identity: {} value: {}'.format(id(self), self._value)


a = MyInt(10)
print(a)       # identity: 4600148712 value: 10
b = MyInt(20)
print(b)       # identity: 4600148600 value: 20
a += b
print(a)       # identity: 4600150448 value: 30

This is how immutable int behaves, __add__ returns new object with new value instead of adding o.value directly to self._value.

nonlocal

Some quote from nonlocal docs:

The nonlocal statement causes the listed identifiers to refer to previously bound variables in the nearest enclosing scope excluding globals. This is important because the default behavior for binding is to search the local namespace first. The statement allows encapsulated code to rebind variables outside of the local scope besides the global (module) scope.

Example of excluding globals:

name = 'Phuket2'

def update(new_name):
    nonlocal name
    name = new_name

update('Zrzka')
print(name)

It leads to SyntaxError, because no binding for nonlocal name was found. Globals are excluded.

Following example just creates local variable name. Same analyzer warning can help (assigned to, but never read).

def hallo():
    name = 'Phuket2'

    def update(new_name):
        # Local variable 'name', has nothing to do
        # with 'name' defined at the beginning of
        # 'hallo'
        name = new_name

    update('Zrzka')
    print(name)  # Prints 'Phuket2'

hallo()

And here's the correct one.

def hallo():
    name = 'Phuket2'

    def update(new_name):
        nonlocal name
        name = new_name

    update('Zrzka')
    print(name)  # Prints 'Zrzka'

hallo()

name in update refers to the name in hallo. You can rebind it. Same dance with mutable / immutable types can be reused here as well. See following example.

def hallo():
    attributes = {'name': 'Phuket2'}

    def update(new_name):
        attributes['name'] = new_name

    update('Zrzka')
    print(attributes['name'])  # Prints 'Zrzka'

hallo()

I didn't use nonlocal, but attributes were still modified. That's because I did use in place mutation. No new object, no need to rebind.

Also nonlocal search enclosing scopes (not just one scope) until it finds the right variable.

def level1():
    name = 'Phuket2'

    def level2():
        def level3():
            nonlocal name  # name in level1
            name = 'Zrzka'
        level3()

    level2()    
    print(name)  # Prints 'Zrzka'

level1()

And the nearest enclosing scope is used.

import console
console.clear()

def level1():
    name = 'Phuket2'

    def level2():
        name = 'Ole'        
        def level3():
            nonlocal name  # name in level2
            name = 'Zrzka'
        level3()

    level2()    
    print(name)  # Prints 'Phuket2'

level1()

These are contrived examples. Just to demostrate how it works. You do not want these multilevel functions where each has name and nonlocal somewhere :)

But the most important thing about nonlocal is that this lexical scoping applies to function namespaces only.

def classy():
    name = 'Phuket2'

    class Hallo():
        name = 'Batman'

        def __init__(self):
            self.name = 'Ole'

        def update(self, new_name):
            nonlocal name  # name in classy
            name = new_name

    h = Hallo()

    print(name)       # Prints 'Phuket2'
    print(Hallo.name) # Prints 'Batman'
    print(h.name)     # Prints 'Ole'

    h.update('Zrzka')

    print(name)       # Prints 'Zrzka'
    print(Hallo.name) # Prints 'Batman'
    print(h.name)     # Prints 'Ole'    

classy()

As I already wrote, this is simplified explanation with contrived examples. Anyway, hope it helps to understand what's going on.

Phuket2

@zrzka, wow. Thanks for the fantastic explanation. I haven't gone though it in detail yet, but I will. I was a pascal/c/vb programmer many many years ok. Even though its so long ago, I often do things how I remember I think they should be done. Maybe global is ok to use in Python for example, but i would hate myself if I found I had too use it for some reason. The nonlocal seemed to make sense to me when I wanted to start to use closures and be able to interact with the outter scopes vars in a somewhat correct manner (but its possibly also considered spaghetti code, not sure) . But I have been cheating :) eg, i would add a runtime attr to an object to read and write because on not understanding these scope rules.
I find it a little funny how they say Python is one of the easiest languages to learn. I am sure its correct in some contexts. But the richness of the language makes it somewhat challenging in my mind. C was hard, but it was fairly rigid. In someways that was nice once you got the hang of it. Anyway, I am waffling on.
Really thanks again. I will go though your sample code carefully. Hopefully other newbies here will also.

zrzka

@Phuket2 said:

Maybe global is ok to use in Python for example, but i would hate myself if I found I had too use it for some reason.

Why? Sometimes it's perfectly ok to use it. There're other ways how to achieve your goal sometimes. Depends. Globals are here and I'm not fan of globals are evil and also not fan of globals are perfect, let's use them everywhere. Just be pragmatic and use the simplest way.

The nonlocal seemed to make sense to me when I wanted to start to use closures and be able to interact with the outter scopes vars in a somewhat correct manner (but its possibly also considered spaghetti code, not sure) . But I have been cheating :) eg, i would add a runtime attr to an object to read and write because on not understanding these scope rules.

In the end the only thing which matters is readability / maintainability. If it's clear what the code is doing, it's fine. Everyone has it's own style, patterns, ... Just don't be too clever or you (or others) will not understand your code after week, month, ...

I find it a little funny how they say Python is one of the easiest languages to learn. I am sure its correct in some contexts. But the richness of the language makes it somewhat challenging in my mind.

Yes, it's easy. The problem here is discipline. Python allows you to do everything and you have to maintain discipline to be a good citizen. And sometimes people don't maintain it, stuff brakes, ... Sometimes they have no clue why, because they didn't read documentation, ...

C was hard, but it was fairly rigid.

I don't consider C as hard, but I think it's harder compared to Python. Not because it's really hard itself, but because code can contain lot of pointer arithmetics, manual memory management, ... and it's unclear what it does when you look at it for the first time. Anyway it was one of my first languages (when I skip BASIC, ...), I like it and I still use it. Remember good old times with Watcom C, protected mode, ... fun without documentation & internet :)