Purpose of existence of add for set

when we need to add one element to a set, then, the .add method could be used.
but the same thing could be done by .update method.

isn’t this similar to registering strings of length one as ‘char’, while others as ‘str’, which is done in languages like C, but not in Python.

why does .add exist, if .update is like its superset?
furthermore,

s = set()
set(dir(s.update)) ^ set(dir(s.add))

gives,

set()

To use set.update to add one item, you have to make a temporary new set that exists only for a moment, and then throw it away:

s = set()
# Add a single element.
s.update( {999} ) # Creates a brand new set, then throws it away.

Better to use s.add(999) directly, which needs no temporary data structures.

set.update and set.add are both methods, so of course they have the same
attributes. This applies to any methods of the same kind. Try this:

set(dir(int.bit_length)) ^ set(dir(list.sort))
# returns set()

That doesn’t mean that (95).bit_length() and [].sort() do the same thing, it just means that they are both the same type of object:

type((95).bit_length)  # <class 'builtin_function_or_method'>
type([].sort)  # also <class 'builtin_function_or_method'>

by that argument, to update a dictionary, we need to create a temporary new dictionary.

d = {'a': 1}
d.update({'b': 2})

then there should be an add for a dictionary also, to add a key-value pair, without creating a new dictionary.

d.add('b', 2)

Indeed there is one, it is spelled __setitem__ or, more conventionally,

d['b'] = 2
2 Likes

By tsef via Discussions on Python.org at 23Mar2022 21:59:

by that argument, to update a dictionary, we need to create a temporary
new dictionary.

No. Steven is pointing out that .update() takes an iterable, so to add
single element you need to first convert that element into an
iterable. His example made a single element set, but a 1-tuple would
have done as well. The point is that having only .update() forces you
to wrap any single element add in an additional throw away data
structure just to make it suitable for .update(). And adding a single
element is very common with sets so that approach would:

  • add pointless compute burden to every single elemnt update (“add”)
  • make all the code harder to read than .add(x)
d = {'a': 1}
d.update({'b': 2})

then there should be an add for a dictionary also, to add a key-value pair, without creating a new dictionary.

There is. But it is spelt:

d['b'] = 2

There’s no need for a special named method.

Cheers,
Cameron Simpson cs@cskk.id.au

the same thing applies for list also, using append vs extend, for adding one item vs multiple items.
why did they not go with the same name add, update, for list also, for adding one element vs multiple elements.
or use name append, extend for set.
Isnt it like methods doing similar things named differently.
that would mean, regardless of we have list, set or dict, we use something like,

lst = [1, 2, 3]
st = {1, 2, 3}
dct = {'a': 1, 'b': 2, 'c': 3}
lst.add(4)
st.add(4)
dct.add('d', 4)

or for multiple elements,

lst.update([4, 5])
st.update({4, 5})
dct.update({'d': 4})

or replace add with append, and update with extend.

or the whole one element vs multiple element thing appears similar to the char vs str argument.
then they could make, only one method, which serves the purpose of all four add, update, append and extend, let us call it universal_update so, then,

lst.universal_update(1)
lst.universal_update([1, 2])
st.universal_update(1)
st.universal_update({2, 3})
dct.universal_update({'a': 3})
dct.universal_update({'ab': 3, 'db': 4})

is it not possible to have an universal_update method like this?
so, one does not need to worry about the whole one vs multiple element thing?

list uses append and extend because when you add one item to a list, you append it to the end, and when you add multiple items you’re extending the list. If you add to a list, where are you adding the item? Probably at the end, but maybe it’s the front end? Similarly, if you update a list, are you overwriting some items? dict.update will happily replace items; set.update may or may not actually change the size of the set.

The names are what they are because they describe what they do.

How would you do lst.append([1, 2]) with your universal_update method?

that would be similar to,

lst.extend([[4, 5]])

if we want,

[1, 2, 3, 4, 5]

then, use,

lst.universal_update([4, 5])

if we want,

[1, 2, 3, [4, 5]]

then, we use,

lst.universal_update([[4, 5]])

it would be similar to extend just it needs to be able to accept one int argument also,
so,

lst.universal_update(4)

also works, instead of giving error int object is not iterable

Nothing stops you from defining this as a function. I very much think it wouldn’t be a good idea in the core because it sounds like a bug magnet for developers forgetting to check if iter(something) raises or not before possibly wrapping the object in a one-element list. For example, this will go surprising:

lst.universal_update("aaaa")

Because a string is iterable like a list, so you’ll end up with ["a", "a", "a"].

As the Zen saying goes, “explicit is better than implicit”.

3 Likes

I was working on this more, this is what I implemented,
for list,

class A(list):

  def __init__(self, x):
      self.x = x

  def universal_update(self, *args):
    for i in args:
      self.x.append(i)
    return self.x

for set,

class B(set):
  def __init__(self, x):
      self.x = x
  
  def universal_update(self, *args):
    for i in args:
      if isinstance(i, (list, set)):
        for j in i:
          self.x.add(j)
      else:
        self.x.add(i)
    return self.x

for dict,

class C(dict):
  def __init__(self, x):
      self.x = x
  
  def universal_update(self, *args):
    for i in args:
      for j, k in i.items():
        self.x[j] = k
    return self.x

and then, one can use it like,

a = A([1])
a.universal_update(1)
a.universal_update([1])
a.universal_update('1')
a.universal_update(1, [1], ['1'], '1')
a.universal_update(['aaa'])
a.universal_update('aaa', 'a')
b = B({1, 2})
b.universal_update(1)
b.universal_update([1, 2, 3])
b.universal_update({'aaa', 'bbb'}, {'ccc'})
b.universal_update({'aaa'})
b.universal_update('aaa')
c = C({'a': 1, 'b': 2})
c.universal_update({'c': 3})
c.universal_update({'d': 4}, {'e': 5, 'f': 6})

but how should I make it to accept all three set, list and dict, and make sure other methods work (like items for dict, pop for set…).

No, it is not the same. Characters are strings of length 1, since there can’t possibly be any confusion there: strings can’t contain anything else than characters. The only way for them to contain other strings is as substrings. In a similar way, digits are integers of “digit length” equal to 1, again because ints cannot contain anything else.

Whether I write

result += somestring

or

for character in somestring: result += character

the result will be the same.

Lists, of course, can contain other lists, and it’s fundamentally different whether they contain them as elements or as sublists.

result += somelist

and

for element in somelist: result += element

are not the same at all.

So yeah, at a first glance it might seem similar, but in fact it really is not.

You should test your code before posting it as a solution to a problem.
Your class is full of bugs.

Using your class A, which inherits from list:

# Normally, we should be able to initialise a list from any
# iterable, such as a string
mylist = A('abcd')
mylist.universal_update('e')  # Fails with AttributeError


# Try again with a list argument.
mylist = A([])
result = mylist.universal_update(0)
print(mylist)  # prints []

result.append(999)
print(mylist.universal_update(1))  # prints [0, 999, 1]

mylist.sort()
print(mylist.universal_update(1000))  # prints [0, 999, 1, 1000]
1 Like

I was working on this more, this is what I implemented,
for list,

class A(list):

  def __init__(self, x):
      self.x = list(x)  # now one can pass string also as an argument.
# but I assume the argument would be a list

  def universal_update(self, *args):
    for i in args:
      self.x.append(i)
    return self.x 
# if I change this to return self, then, for the above example, 
# mylist.universal_update(1) would give, [0, 1], 
# but result.append(999) does not work
  
  def __str__(self):
    return str(self.x) # it would print correctly now.
  
  __repr__ = __str__

  def sort(self):
    self.x = sorted(self.x) # sorting works with this, but probably there would be a better way
    return self.x

This already looks very strange. Why do you subclass list while providing a __init__ method that clearly does not initialize self as a list? Or, in other words, is the A class supposed to work like a list? For example, this definition does not support retrieving the list items:

a = A([1,2,3])
a[0]

fails with IndexError.

should I remove self.x, then, the class looks something like this, and it appears that append does not return anything, so, i dont return anything.

class A(list):

  def universal_update(self, *args):
    for i in args:
      self.append(i)

now, if we pass,

a = A('abc')
a.universal_update('d')

it works

a.sort()

also works

result = a.universal_update(1)
result

gives nothing

I want class A to work like a regular list, plus it has the universal_update method.

the set class becomes like this,

class B(set):
  
  def universal_update(self, *args):
    for i in args:
      if isinstance(i, (list, set)):
        for j in i:
          self.add(j)
      else:
        self.add(i)

the dict one becomes like this,

class C(dict):
  
  def universal_update(self, *args):
    for i in args:
      for j, k in i.items():
        self[j] = k

Your “universal_update” is just the list.extend method re-written to take multiple arguments instead of an iterable. That means it doesn’t nothing that the extend method can’t already do, but is slower.

class MyList(list):
    def universal_update(self, *args):
        self.extend(args)

should do what you want.

1 Like

would it be a good idea to rename/alias the existing extend to update to make things appear more consistent.
as is done for the case of len, so, for example, in a language like Java, they use,

text.length()  // String
rates.length  // array of floats
names.size()  // ArrayList

which is inconsistent, whereas, Python uses, len(text), len(rates), …
so, if we say the same thing for list vs set vs dict, should the update keyword be used for all three, again to make things more consistent?

because if order is the concern, so, if we use,

somelist = [1]
somelist.update(1, 2, 3)

then, one might wonder where will these elements be inserted, either at the beginning or at the end.

[1, 1, 2, 3]
[1, 2, 3, 1]

but the same holds true for the existing update keyword for dictionaries also,

d = {'a': 1, 'b': 2}
d.update({'c': 3, 'e: 4})

one could think, would it update these key-value pairs to the front or to the end.

{'a': 1, 'b': 2, 'c': 3, 'e': 4} # this is what appears to be happening
{'c': 3, 'e': 4, 'a': 1, 'b': 2}

if our list.update also does the update to the end of the list thing, then would it be a more consistent method than the existing extend?

tsef suggested:

“would it be a good idea to rename/alias the existing extend to update to make things appear more consistent.”

No, because extending a list and updating a dict or set are different operations.

L = [1, 2, 3]
L.extend([1, 4])
# Now L is [1, 2, 3, 1, 4]

S = {1, 2, 3}
S.update({1, 4})
# Now S is {1, 2, 3, 4}